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
|
//! Right-click titlebar window menu - the one titlebar interaction
//! virtually every desktop WM has always offered that srdwm never did.
//! Right-click on a titlebar previously did nothing at all (the only
//! right-button behaviour anywhere was the SUPER+right-drag resize
//! gesture, which needs the modifier held); this gives plain right-click
//! a real, discoverable action.
//!
//! Deliberately minimal: four fixed actions, no submenus, no live hover
//! highlight (a nice-to-have that would need the render buffer rebuilt on
//! every pointer-motion event over the menu - not worth the extra
//! per-frame cost for a first pass). See `decoration::render_context_menu`
//! for the actual pixels.
use srdwm_core::{WindowId, WindowManager, TITLEBAR_HEIGHT};
#[derive(Clone, Copy)]
pub(crate) enum MenuAction {
Minimize,
ToggleMaximize,
ToggleAlwaysOnTop,
Close,
}
pub(crate) struct ContextMenu {
pub(crate) window: WindowId,
/// Top-left corner, in global (output-independent) space - same frame
/// `Window.geometry` and every other `custom_elements` position uses.
pub(crate) pos: (i32, i32),
pub(crate) width: u32,
pub(crate) row_height: u32,
pub(crate) items: Vec<(&'static str, MenuAction)>,
}
const MENU_WIDTH: u32 = 170;
impl ContextMenu {
/// Builds the menu for `window`, opening with its top-left corner at
/// `pos` (wherever the right-click landed). Labels reflect the
/// window's *current* state - "Maximize" flips to "Restore", "Always
/// on Top" gets a checkmark prefix once pinned - same convention
/// every native window menu uses, rather than a static label that
/// silently means the opposite of what it says half the time.
pub(crate) fn open(wm: &WindowManager, window: WindowId, pos: (i32, i32)) -> Option<Self> {
let w = wm.window(window)?;
let maximize_label = if w.maximized { "Restore" } else { "Maximize" };
let pin_label = if w.always_on_top { "\u{2713} Always on Top" } else { "Always on Top" };
let items = vec![
("Minimize", MenuAction::Minimize),
(maximize_label, MenuAction::ToggleMaximize),
(pin_label, MenuAction::ToggleAlwaysOnTop),
("Close", MenuAction::Close),
];
Some(Self { window, pos, width: MENU_WIDTH, row_height: TITLEBAR_HEIGHT, items })
}
pub(crate) fn height(&self) -> i32 {
self.row_height as i32 * self.items.len() as i32
}
/// Which row (if any) global-space point `(x, y)` falls on.
pub(crate) fn row_at(&self, x: i32, y: i32) -> Option<usize> {
if x < self.pos.0 || x >= self.pos.0 + self.width as i32 {
return None;
}
let rel_y = y - self.pos.1;
if rel_y < 0 || rel_y >= self.height() {
return None;
}
Some((rel_y / self.row_height as i32) as usize)
}
}
#[cfg(test)]
mod tests {
use super::*;
use srdwm_core::Window;
fn wm_with_window() -> (WindowManager, WindowId) {
let mut wm = WindowManager::new();
wm.set_monitors(vec![srdwm_core::Monitor::new(0, "primary", srdwm_core::Rect::new(0, 0, 1920, 1080))]);
let id = wm.alloc_window_id();
wm.add_window(Window::new(id, "a"));
(wm, id)
}
#[test]
fn open_labels_maximize_action_by_current_state() {
let (mut wm, id) = wm_with_window();
let menu = ContextMenu::open(&wm, id, (100, 100)).unwrap();
assert_eq!(menu.items[1].0, "Maximize");
wm.toggle_maximize(id);
let menu = ContextMenu::open(&wm, id, (100, 100)).unwrap();
assert_eq!(menu.items[1].0, "Restore");
}
#[test]
fn open_marks_pinned_state_on_the_always_on_top_row() {
let (mut wm, id) = wm_with_window();
let menu = ContextMenu::open(&wm, id, (100, 100)).unwrap();
assert_eq!(menu.items[2].0, "Always on Top");
wm.toggle_always_on_top(id);
let menu = ContextMenu::open(&wm, id, (100, 100)).unwrap();
assert!(menu.items[2].0.starts_with('\u{2713}'), "pinned state must be visible on the label itself");
}
#[test]
fn open_returns_none_for_a_window_that_no_longer_exists() {
let (wm, id) = wm_with_window();
assert!(ContextMenu::open(&wm, id + 999, (0, 0)).is_none());
}
#[test]
fn row_at_maps_a_point_to_the_right_row() {
let (wm, id) = wm_with_window();
let menu = ContextMenu::open(&wm, id, (100, 100)).unwrap();
assert_eq!(menu.row_at(150, 100), Some(0), "top of the first row");
assert_eq!(menu.row_at(150, 100 + TITLEBAR_HEIGHT as i32 - 1), Some(0), "bottom of the first row");
assert_eq!(menu.row_at(150, 100 + TITLEBAR_HEIGHT as i32), Some(1), "top of the second row");
assert_eq!(menu.row_at(150, 100 + menu.height() - 1), Some(3), "last row, last pixel");
}
#[test]
fn row_at_is_none_outside_the_menus_bounds() {
let (wm, id) = wm_with_window();
let menu = ContextMenu::open(&wm, id, (100, 100)).unwrap();
assert_eq!(menu.row_at(99, 110), None, "just left of the menu");
assert_eq!(menu.row_at(100 + MENU_WIDTH as i32, 110), None, "just right of the menu");
assert_eq!(menu.row_at(150, 99), None, "just above the menu");
assert_eq!(menu.row_at(150, 100 + menu.height()), None, "just below the menu");
}
}
|