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