1use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
7use crate::geometry::{Point, Size};
8
9use super::geometry::{hit_test, item_at_path, stack_layout, MenuHit, PopupLayout};
10use super::model::{MenuEntry, MenuSelection};
11
12#[derive(Clone, Copy, Debug, PartialEq, Eq)]
13pub enum MenuAnchorKind {
14 Context,
15 Bar,
16}
17
18#[derive(Clone, Debug)]
19pub struct PopupMenuState {
20 pub anchor: Point,
21 pub anchor_kind: MenuAnchorKind,
22 pub open: bool,
23 pub open_path: Vec<usize>,
24 pub hover_path: Option<Vec<usize>>,
25 suppress_next_mouse_up: bool,
26 activate_on_mouse_up: bool,
27}
28
29#[derive(Clone, Debug, PartialEq, Eq)]
30pub enum MenuResponse {
31 None,
32 Action(String),
33 Closed,
34}
35
36impl Default for PopupMenuState {
37 fn default() -> Self {
38 Self {
39 anchor: Point::ORIGIN,
40 anchor_kind: MenuAnchorKind::Context,
41 open: false,
42 open_path: Vec::new(),
43 hover_path: None,
44 suppress_next_mouse_up: false,
45 activate_on_mouse_up: false,
46 }
47 }
48}
49
50impl PopupMenuState {
51 pub fn open_at(&mut self, anchor: Point, anchor_kind: MenuAnchorKind) {
52 self.anchor = anchor;
53 self.anchor_kind = anchor_kind;
54 self.open = true;
55 self.open_path.clear();
56 self.hover_path = None;
57 self.suppress_next_mouse_up = false;
58 self.activate_on_mouse_up = false;
59 }
60
61 pub fn close(&mut self) {
62 self.open = false;
63 self.open_path.clear();
64 self.hover_path = None;
65 self.activate_on_mouse_up = false;
66 }
67
68 pub fn arm_mouse_up_activation(&mut self) {
69 self.activate_on_mouse_up = true;
70 }
71
72 pub fn is_mouse_up_activation_armed(&self) -> bool {
73 self.activate_on_mouse_up
74 }
75
76 pub fn handle_shortcut(
77 &mut self,
78 items: &mut [MenuEntry],
79 key: &Key,
80 modifiers: Modifiers,
81 ) -> MenuResponse {
82 let Some(path) = shortcut_path(items, key, modifiers) else {
83 return MenuResponse::None;
84 };
85 let Some(item) = item_at_path(items, &path) else {
86 return MenuResponse::None;
87 };
88 let Some(action) = item.action.clone() else {
89 return MenuResponse::None;
90 };
91 let close_on_activate = item.close_on_activate;
92 let (_, response) = self.activate_action(items, &path, action, close_on_activate, false);
93 response
94 }
95
96 pub fn should_suppress_mouse_up(&self) -> bool {
97 self.suppress_next_mouse_up
98 }
99
100 pub fn take_suppress_mouse_up(&mut self) -> bool {
101 let suppress = self.suppress_next_mouse_up;
102 self.suppress_next_mouse_up = false;
103 suppress
104 }
105
106 pub fn layouts(&self, items: &[MenuEntry], viewport: Size) -> Vec<PopupLayout> {
107 if self.open {
108 stack_layout(
109 items,
110 self.anchor,
111 self.anchor_kind,
112 &self.open_path,
113 viewport,
114 )
115 } else {
116 Vec::new()
117 }
118 }
119
120 pub fn handle_event(
121 &mut self,
122 items: &mut [MenuEntry],
123 event: &Event,
124 viewport: Size,
125 ) -> (EventResult, MenuResponse) {
126 if !self.open {
127 return (EventResult::Ignored, MenuResponse::None);
128 }
129 match event {
130 Event::MouseMove { pos } => {
131 let changed = self.update_hover(items, *pos, viewport);
132 if changed {
133 crate::animation::request_draw_without_invalidation();
134 }
135 (EventResult::Consumed, MenuResponse::None)
136 }
137 Event::MouseDown {
138 pos,
139 button: MouseButton::Left,
140 ..
141 } => self.handle_left_down(items, *pos, viewport),
142 Event::MouseUp {
143 pos,
144 button: MouseButton::Left,
145 ..
146 } if self.activate_on_mouse_up => {
147 self.activate_on_mouse_up = false;
148 self.handle_release_activation(items, *pos, viewport)
149 }
150 Event::MouseUp {
151 button: MouseButton::Left,
152 ..
153 } if self.take_suppress_mouse_up() => (EventResult::Consumed, MenuResponse::None),
154 Event::KeyDown { key, modifiers } => {
155 let response = self.handle_shortcut(items, key, *modifiers);
156 if response != MenuResponse::None {
157 (EventResult::Consumed, response)
158 } else {
159 self.handle_key(items, key.clone())
160 }
161 }
162 _ => (EventResult::Ignored, MenuResponse::None),
163 }
164 }
165
166 pub fn update_hover(&mut self, items: &[MenuEntry], pos: Point, viewport: Size) -> bool {
167 let layouts = self.layouts(items, viewport);
168 let next_hover = match hit_test(&layouts, pos) {
169 Some(MenuHit::Item(path)) => {
170 if let Some(item) = item_at_path(items, &path) {
171 if !item.enabled {
172 if !self.open_path.starts_with(&path) {
173 self.open_path.truncate(path.len().saturating_sub(1));
174 }
175 return self.set_hover_path(None);
176 }
177 if item.enabled && item.has_submenu() {
178 self.open_path = path.clone();
179 } else if !self.open_path.starts_with(&path) {
180 self.open_path.truncate(path.len().saturating_sub(1));
181 }
182 }
183 Some(path)
184 }
185 _ => None,
186 };
187 if self.hover_path != next_hover {
188 self.hover_path = next_hover;
189 true
190 } else {
191 false
192 }
193 }
194
195 fn set_hover_path(&mut self, hover_path: Option<Vec<usize>>) -> bool {
196 if self.hover_path != hover_path {
197 self.hover_path = hover_path;
198 true
199 } else {
200 false
201 }
202 }
203
204 fn handle_left_down(
205 &mut self,
206 items: &mut [MenuEntry],
207 pos: Point,
208 viewport: Size,
209 ) -> (EventResult, MenuResponse) {
210 let layouts = self.layouts(items, viewport);
211 match hit_test(&layouts, pos) {
212 Some(MenuHit::Item(path)) => {
213 let Some(item) = item_at_path(items, &path) else {
214 return (EventResult::Consumed, MenuResponse::None);
215 };
216 let enabled = item.enabled;
217 let has_submenu = item.has_submenu();
218 let action = item.action.clone();
219 let close_on_activate = item.close_on_activate;
220 if !enabled {
221 self.hover_path = None;
222 return (EventResult::Consumed, MenuResponse::None);
223 }
224 self.hover_path = Some(path.clone());
225 if has_submenu {
226 self.open_path = path;
227 crate::animation::request_draw();
228 (EventResult::Consumed, MenuResponse::None)
229 } else if let Some(action) = action {
230 self.activate_action(items, &path, action, close_on_activate, true)
231 } else {
232 (EventResult::Consumed, MenuResponse::None)
233 }
234 }
235 Some(MenuHit::Panel) => (EventResult::Consumed, MenuResponse::None),
236 None => {
237 self.close();
238 self.suppress_next_mouse_up = true;
239 crate::animation::request_draw();
240 (EventResult::Consumed, MenuResponse::Closed)
241 }
242 }
243 }
244
245 fn handle_release_activation(
246 &mut self,
247 items: &mut [MenuEntry],
248 pos: Point,
249 viewport: Size,
250 ) -> (EventResult, MenuResponse) {
251 let layouts = self.layouts(items, viewport);
252 match hit_test(&layouts, pos) {
253 Some(MenuHit::Item(path)) => {
254 self.hover_path = Some(path.clone());
255 let Some(item) = item_at_path(items, &path) else {
256 return (EventResult::Consumed, MenuResponse::None);
257 };
258 let enabled = item.enabled;
259 let has_submenu = item.has_submenu();
260 let action = item.action.clone();
261 let close_on_activate = item.close_on_activate;
262 if !enabled || has_submenu {
263 return (EventResult::Consumed, MenuResponse::None);
264 }
265 if let Some(action) = action {
266 self.activate_action(items, &path, action, close_on_activate, false)
267 } else {
268 (EventResult::Consumed, MenuResponse::None)
269 }
270 }
271 Some(MenuHit::Panel) | None => (EventResult::Consumed, MenuResponse::None),
272 }
273 }
274
275 fn activate_action(
276 &mut self,
277 items: &mut [MenuEntry],
278 path: &[usize],
279 action: String,
280 close_on_activate: bool,
281 suppress_mouse_up: bool,
282 ) -> (EventResult, MenuResponse) {
283 toggle_selection_at_path(items, path);
284 if close_on_activate {
285 self.close();
286 self.suppress_next_mouse_up = suppress_mouse_up;
287 }
288 crate::animation::request_draw();
289 (EventResult::Consumed, MenuResponse::Action(action))
290 }
291
292 fn handle_key(&mut self, items: &mut [MenuEntry], key: Key) -> (EventResult, MenuResponse) {
293 match key {
294 Key::Escape => {
295 self.close();
296 crate::animation::request_draw();
297 (EventResult::Consumed, MenuResponse::Closed)
298 }
299 Key::ArrowDown => {
300 self.step_hover(items, 1);
301 (EventResult::Consumed, MenuResponse::None)
302 }
303 Key::ArrowUp => {
304 self.step_hover(items, -1);
305 (EventResult::Consumed, MenuResponse::None)
306 }
307 Key::ArrowRight => {
308 if let Some(path) = self.hover_path.clone() {
309 if item_at_path(items, &path).is_some_and(|item| item.has_submenu()) {
310 self.open_path = path;
311 crate::animation::request_draw();
312 }
313 }
314 (EventResult::Consumed, MenuResponse::None)
315 }
316 Key::ArrowLeft => {
317 self.open_path.pop();
318 self.hover_path = self.open_path.last().map(|_| self.open_path.clone());
319 crate::animation::request_draw();
320 (EventResult::Consumed, MenuResponse::None)
321 }
322 Key::Enter | Key::Char(' ') => {
323 if let Some(path) = self.hover_path.clone() {
324 if let Some(item) = item_at_path(items, &path) {
325 let enabled = item.enabled;
326 let has_submenu = item.has_submenu();
327 let action = item.action.clone();
328 let close_on_activate = item.close_on_activate;
329 if enabled && has_submenu {
330 self.open_path = path;
331 } else if enabled {
332 if let Some(action) = action {
333 return self.activate_action(
334 items,
335 &path,
336 action,
337 close_on_activate,
338 false,
339 );
340 }
341 }
342 }
343 }
344 (EventResult::Consumed, MenuResponse::None)
345 }
346 _ => (EventResult::Ignored, MenuResponse::None),
347 }
348 }
349
350 fn step_hover(&mut self, items: &[MenuEntry], delta: isize) {
351 let level_items = items_at_path(items, &self.open_path).unwrap_or(items);
352 let enabled: Vec<usize> = level_items
353 .iter()
354 .enumerate()
355 .filter_map(|(idx, entry)| match entry {
356 MenuEntry::Item(item) if item.enabled => Some(idx),
357 _ => None,
358 })
359 .collect();
360 if enabled.is_empty() {
361 return;
362 }
363 let current = self
364 .hover_path
365 .as_ref()
366 .and_then(|path| path.last().copied())
367 .and_then(|idx| enabled.iter().position(|candidate| *candidate == idx));
368 let base = current
369 .map(|idx| idx as isize)
370 .unwrap_or(if delta > 0 { -1 } else { 0 });
371 let next = (base + delta).rem_euclid(enabled.len() as isize) as usize;
372 let mut path = self.open_path.clone();
373 path.push(enabled[next]);
374 self.hover_path = Some(path);
375 crate::animation::request_draw();
376 }
377}
378
379fn items_at_path<'a>(items: &'a [MenuEntry], path: &[usize]) -> Option<&'a [MenuEntry]> {
380 let mut current = items;
381 for &idx in path {
382 current = &item_at_path(current, &[idx])?.submenu;
383 }
384 Some(current)
385}
386
387fn toggle_selection_at_path(items: &mut [MenuEntry], path: &[usize]) {
388 let Some(selection) = item_at_path(items, path).map(|item| item.selection) else {
389 return;
390 };
391 match selection {
392 MenuSelection::Check { selected } => {
393 if let Some(item) = item_at_path_mut(items, path) {
394 item.selection = MenuSelection::Check {
395 selected: !selected,
396 };
397 }
398 }
399 MenuSelection::Radio { .. } => {
400 let Some((&idx, parent_path)) = path.split_last() else {
401 return;
402 };
403 let Some(parent) = entries_at_path_mut(items, parent_path) else {
404 return;
405 };
406 for entry in parent.iter_mut() {
407 if let MenuEntry::Item(item) = entry {
408 if matches!(item.selection, MenuSelection::Radio { .. }) {
409 item.selection = MenuSelection::Radio { selected: false };
410 }
411 }
412 }
413 if let Some(MenuEntry::Item(item)) = parent.get_mut(idx) {
414 item.selection = MenuSelection::Radio { selected: true };
415 }
416 }
417 MenuSelection::None => {}
418 }
419}
420
421fn item_at_path_mut<'a>(
422 items: &'a mut [MenuEntry],
423 path: &[usize],
424) -> Option<&'a mut super::model::MenuItem> {
425 let (&idx, rest) = path.split_first()?;
426 let entry = items.get_mut(idx)?;
427 match entry {
428 MenuEntry::Item(item) => {
429 if rest.is_empty() {
430 Some(item)
431 } else {
432 item_at_path_mut(&mut item.submenu, rest)
433 }
434 }
435 MenuEntry::Separator => None,
436 }
437}
438
439fn entries_at_path_mut<'a>(
440 items: &'a mut [MenuEntry],
441 path: &[usize],
442) -> Option<&'a mut [MenuEntry]> {
443 if path.is_empty() {
444 return Some(items);
445 }
446 let (&idx, rest) = path.split_first()?;
447 match items.get_mut(idx)? {
448 MenuEntry::Item(item) => entries_at_path_mut(&mut item.submenu, rest),
449 MenuEntry::Separator => None,
450 }
451}
452
453fn shortcut_path(items: &[MenuEntry], key: &Key, modifiers: Modifiers) -> Option<Vec<usize>> {
454 for (idx, entry) in items.iter().enumerate() {
455 let MenuEntry::Item(item) = entry else {
456 continue;
457 };
458 if item.enabled
459 && item
460 .accelerator
461 .is_some_and(|accelerator| accelerator.matches(key, modifiers))
462 && item.action.is_some()
463 {
464 return Some(vec![idx]);
465 }
466 if let Some(mut path) = shortcut_path(&item.submenu, key, modifiers) {
467 path.insert(0, idx);
468 return Some(path);
469 }
470 }
471 None
472}