srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state/menu.rs
blob: d5e91cbbc34e23738965695a6eea5b180c3e6047 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
use super::*;

impl CompState {

    /// Opens the right-click titlebar window menu for `window`, top-left
    /// corner at `pos` (global space, wherever the click landed). Rebuilds
    /// and caches the rasterised buffer once here rather than per frame --
    /// same reasoning as `redraw_decoration_buffer`.
    pub(crate) fn open_context_menu(&mut self, window: WindowId, pos: (i32, i32)) {
        let Some(mut menu) = ({
            let wm = self.wm.borrow();
            crate::context_menu::ContextMenu::open(&wm, window, pos)
        }) else {
            return;
        };
        // `ContextMenu::width` is a backend-agnostic placeholder - `core`
        // has no font of its own to measure real text against, so it can
        // only ever pick a fixed guess. Widened here to whatever this
        // menu's own widest real label actually needs, or reported live
        // as "text goes out of view": the old fixed width comfortably fit
        // every label back when this menu only listed short ones
        // ("Minimize", "Close"), but a longer one added since ("Button
        // Style: Traffic Lights", or a user-configurable workspace name)
        // just ran past the panel's own right edge, silently cut off
        // mid-character by `render_context_menu`'s own overflow guard.
        // Only ever grows the width, never shrinks it below the built-in
        // minimum `ContextMenu::open` already picked.
        let font = decoration::find_system_font();
        let widest_label = menu.items.iter().map(|&(label, _)| decoration::measure_text_width(&font, label, decoration::FONT_PIXELS)).fold(0.0_f32, f32::max);
        let content_width = (widest_label + decoration::TEXT_LEFT_PADDING * 2.0).ceil() as u32;
        menu.width = menu.width.max(content_width);
        let theme = self.wm.borrow().theme;
        let rows: Vec<(&str, bool, u32, decoration::MenuRowKind)> = menu
            .items
            .iter()
            .enumerate()
            .map(|(i, &(label, action))| {
                let kind = match action {
                    crate::context_menu::MenuAction::Separator => decoration::MenuRowKind::Separator,
                    crate::context_menu::MenuAction::Header => decoration::MenuRowKind::Header,
                    _ => decoration::MenuRowKind::Item,
                };
                (label, false, menu.row_height_for(i), kind)
            })
            .collect();
        let data = decoration::render_context_menu(menu.width, &rows, theme.titlebar_bg, theme.titlebar_fg_focused, theme.titlebar_fg_unfocused, theme.default_border_color);
        let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (menu.width as i32, menu.height()), 1, Transform::Normal, None);
        self.context_menu_buffer = Some(buffer);
        self.context_menu = Some(menu);
    }

    pub(crate) fn close_context_menu(&mut self) {
        self.context_menu = None;
        self.context_menu_buffer = None;
    }

    /// Runs whichever action a click on `row` of the currently-open context
    /// menu selected. Takes the menu's `window`/action rather than reading
    /// `self.context_menu` itself so the caller can close the menu first
    /// (clearing the borrow) before this runs - several of these actions
    /// (`sync_geometry`, `redraw_decoration_buffer`) need `&mut self` in
    /// ways that would otherwise conflict with an active `self.context_menu`
    /// borrow.
    pub(crate) fn run_context_menu_action(&mut self, window: WindowId, action: crate::context_menu::MenuAction) {
        use crate::context_menu::MenuAction;
        match action {
            MenuAction::Minimize => {
                self.wm.borrow_mut().minimize_window(window);
                foreign_toplevel::send_state(self, window);
            }
            MenuAction::ToggleMaximize => {
                self.wm.borrow_mut().toggle_maximize(window);
                self.sync_geometry(window);
                foreign_toplevel::send_state(self, window);
            }
            MenuAction::ToggleFullscreen => {
                self.wm.borrow_mut().toggle_fullscreen(window);
                self.sync_geometry(window);
                foreign_toplevel::send_state(self, window);
            }
            MenuAction::ToggleFloating => {
                self.wm.borrow_mut().toggle_floating(window);
                self.sync_geometry(window);
            }
            MenuAction::ToggleAlwaysOnTop => {
                self.wm.borrow_mut().toggle_always_on_top(window);
            }
            MenuAction::MoveToWorkspace(workspace) => {
                self.wm.borrow_mut().move_window_to_workspace(window, workspace);
            }
            MenuAction::CycleButtonStyle => {
                let mut wm = self.wm.borrow_mut();
                wm.theme.traffic_light_buttons = !wm.theme.traffic_light_buttons;
                drop(wm);
                self.redraw_every_decoration();
            }
            MenuAction::CycleButtonSide => {
                let mut wm = self.wm.borrow_mut();
                wm.theme.buttons_left = !wm.theme.buttons_left;
                drop(wm);
                self.redraw_every_decoration();
            }
            MenuAction::Close => {
                if let Some(w) = self.id_to_window.get(&window) {
                    crate::input::close_dwindow(w);
                }
            }
            // Never actually reached: the click-dispatch site
            // (`input/pointer.rs`) intercepts `Separator`/`Header` before
            // calling this function at all. Handled here too so this
            // match stays exhaustive without a catch-all that would
            // silently swallow a real future variant added without
            // updating this function.
            MenuAction::Separator | MenuAction::Header => {}
        }
    }

    /// Rebuilds every open window's titlebar/border bitmap against
    /// whatever `wm.theme` currently holds - what `CycleButtonStyle`/
    /// `CycleButtonSide` need to actually show their effect immediately.
    ///
    /// `srd set button_style`/`button_side` (`crates/platform/src/ipc/
    /// dispatch.rs`) change the exact same `ThemeConfig` fields but are
    /// deliberately scoped to "only affects windows created (or
    /// redecorated) after this call" - that crate is backend-agnostic
    /// and has no way to reach into a Wayland-specific redraw. A titlebar
    /// menu action has no such excuse: it's *this* backend's own code,
    /// already holding `&mut self`, and a "customize" action that doesn't
    /// visibly change the very titlebar you clicked would be exactly the
    /// kind of "doesn't make sense" this menu was reported for in the
    /// first place. `redraw_decoration_buffer` already no-ops on any
    /// window whose decoration signature didn't actually change, so
    /// calling it for every window here costs nothing for the (common)
    /// case where most of them don't use server-side decoration at all.
    fn redraw_every_decoration(&mut self) {
        let ids: Vec<WindowId> = self.wm.borrow().windows().map(|w| w.id).collect();
        for id in ids {
            self.redraw_decoration_buffer(id);
        }
    }

    /// Opens the Snap-Layouts flyout for `window`, top-left corner at `pos`
    /// (global space, by convention the maximize button's own titlebar
    /// position). Same build-once-on-open pattern as `open_context_menu`.
    pub(crate) fn open_snap_flyout(&mut self, window: WindowId, pos: (i32, i32)) {
        let mut flyout = crate::snap_flyout::SnapFlyout::open(window, pos);
        let theme = self.wm.borrow().theme;
        let labels: Vec<&str> = flyout.cells().iter().map(|z| z.label()).collect();
        // Grow each cell to fit the widest label, exactly as
        // `open_context_menu` above already does for menu rows - never
        // shrinking below the built-in minimum `SnapFlyout::open` picked.
        //
        // The flyout was the one labelled surface in this compositor that
        // never got this treatment: at the fixed 90px cell width "Bottom
        // Right" was cut off mid-word and "Top Left" ran into its
        // neighbour, the same "text goes out of view" fault already
        // reported and fixed for the context menu.
        let font = decoration::find_system_font();
        let widest = labels.iter().map(|l| decoration::measure_text_width(&font, l, decoration::FONT_PIXELS)).fold(0.0_f32, f32::max);
        flyout.cell_width = flyout.cell_width.max((widest + decoration::TEXT_LEFT_PADDING * 2.0).ceil() as u32);
        let data = decoration::render_snap_flyout(3, flyout.cell_width, flyout.cell_height, &labels, theme.titlebar_bg, theme.titlebar_fg_focused, theme.default_border_color);
        let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (flyout.width() as i32, flyout.height() as i32), 1, Transform::Normal, None);
        self.snap_flyout_buffer = Some(buffer);
        self.snap_flyout = Some(flyout);
    }

    pub(crate) fn close_snap_flyout(&mut self) {
        self.snap_flyout = None;
        self.snap_flyout_buffer = None;
    }

    /// Applies `zone` to `window` - the flyout's own equivalent of
    /// `run_context_menu_action`, taking the target explicitly rather than
    /// reading `self.snap_flyout` for the same borrow-conflict reason
    /// documented on that function.
    pub(crate) fn run_snap_flyout_action(&mut self, window: WindowId, zone: SnapZoneKind) {
        self.wm.borrow_mut().apply_snap_zone(window, zone);
        self.sync_geometry(window);
        foreign_toplevel::send_state(self, window);
    }

    /// True when this titlebar press is the second of a double-click on the
    /// same window. Threshold is the usual 400ms.
    pub(crate) fn is_double_click(&mut self, id: WindowId, time: u32) -> bool {
        const DOUBLE_CLICK_MS: u32 = 400;
        let doubled = match self.last_titlebar_click {
            Some((last_id, last_time)) => last_id == id && time.saturating_sub(last_time) <= DOUBLE_CLICK_MS,
            None => false,
        };
        // Reset after a double, so a third click starts a fresh pair rather
        // than counting as another double.
        self.last_titlebar_click = if doubled { None } else { Some((id, time)) };
        doubled
    }
}