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::MouseUp {
163 pos,
164 button: MouseButton::Left,
165 ..
166 } if self.activate_on_mouse_up => {
167 self.activate_on_mouse_up = false;
168 self.handle_release_activation(items, *pos, viewport)
169 }
170 Event::MouseUp {
171 button: MouseButton::Left,
172 ..
173 } if self.take_suppress_mouse_up() => (EventResult::Consumed, MenuResponse::None),
174 Event::KeyDown { key, modifiers } => {
175 let response = self.handle_shortcut(items, key, *modifiers);
176 if response != MenuResponse::None {
177 (EventResult::Consumed, response)
178 } else {
179 self.handle_key(items, key.clone())
180 }
181 }
182 _ => (EventResult::Ignored, MenuResponse::None),
183 }
184 }
185
186 pub fn update_hover(&mut self, items: &[MenuEntry], pos: Point, viewport: Size) -> bool {
187 if is_touch_synthesized() {
196 return self.set_hover_path(None);
197 }
198 let layouts = self.layouts(items, viewport);
199 let next_hover = match hit_test(&layouts, pos) {
200 Some(MenuHit::Item(path)) => {
201 if let Some(item) = item_at_path(items, &path) {
202 if !item.enabled {
203 if !self.open_path.starts_with(&path) {
204 self.open_path.truncate(path.len().saturating_sub(1));
205 }
206 return self.set_hover_path(None);
207 }
208 if item.enabled && item.has_submenu() {
209 self.open_path = path.clone();
210 } else if !self.open_path.starts_with(&path) {
211 self.open_path.truncate(path.len().saturating_sub(1));
212 }
213 }
214 Some(path)
215 }
216 _ => None,
217 };
218 if self.hover_path != next_hover {
221 self.hover_path = next_hover;
222 true
223 } else {
224 false
225 }
226 }
227
228 fn set_hover_path(&mut self, hover_path: Option<Vec<usize>>) -> bool {
229 if self.hover_path != hover_path {
230 self.hover_path = hover_path;
231 true
232 } else {
233 false
234 }
235 }
236
237 fn handle_left_down(
238 &mut self,
239 items: &mut [MenuEntry],
240 pos: Point,
241 viewport: Size,
242 ) -> (EventResult, MenuResponse) {
243 let layouts = self.layouts(items, viewport);
244 match hit_test(&layouts, pos) {
245 Some(MenuHit::Item(path)) => {
246 let Some(item) = item_at_path(items, &path) else {
247 return (EventResult::Consumed, MenuResponse::None);
248 };
249 let enabled = item.enabled;
250 let has_submenu = item.has_submenu();
251 let action = item.action.clone();
252 let close_on_activate = item.close_on_activate;
253 if !enabled {
254 self.hover_path = None;
255 return (EventResult::Consumed, MenuResponse::None);
256 }
257 self.hover_path = Some(path.clone());
258 if has_submenu {
259 self.open_path = path;
260 crate::animation::request_draw();
261 (EventResult::Consumed, MenuResponse::None)
262 } else if let Some(action) = action {
263 self.activate_action(items, &path, action, close_on_activate, true)
264 } else {
265 (EventResult::Consumed, MenuResponse::None)
266 }
267 }
268 Some(MenuHit::Panel) => (EventResult::Consumed, MenuResponse::None),
269 None => {
270 self.close();
271 self.suppress_next_mouse_up = true;
272 crate::animation::request_draw();
273 (EventResult::Consumed, MenuResponse::Closed)
274 }
275 }
276 }
277
278 fn handle_release_activation(
279 &mut self,
280 items: &mut [MenuEntry],
281 pos: Point,
282 viewport: Size,
283 ) -> (EventResult, MenuResponse) {
284 let layouts = self.layouts(items, viewport);
285 match hit_test(&layouts, pos) {
286 Some(MenuHit::Item(path)) => {
287 self.hover_path = Some(path.clone());
288 let Some(item) = item_at_path(items, &path) else {
289 return (EventResult::Consumed, MenuResponse::None);
290 };
291 let enabled = item.enabled;
292 let has_submenu = item.has_submenu();
293 let action = item.action.clone();
294 let close_on_activate = item.close_on_activate;
295 if !enabled || has_submenu {
296 return (EventResult::Consumed, MenuResponse::None);
297 }
298 if let Some(action) = action {
299 self.activate_action(items, &path, action, close_on_activate, false)
300 } else {
301 (EventResult::Consumed, MenuResponse::None)
302 }
303 }
304 Some(MenuHit::Panel) | None => (EventResult::Consumed, MenuResponse::None),
305 }
306 }
307
308 fn activate_action(
309 &mut self,
310 items: &mut [MenuEntry],
311 path: &[usize],
312 action: String,
313 close_on_activate: bool,
314 suppress_mouse_up: bool,
315 ) -> (EventResult, MenuResponse) {
316 toggle_selection_at_path(items, path);
317 if close_on_activate {
318 self.close();
319 self.suppress_next_mouse_up = suppress_mouse_up;
320 }
321 crate::animation::request_draw();
322 (EventResult::Consumed, MenuResponse::Action(action))
323 }
324
325 fn handle_key(&mut self, items: &mut [MenuEntry], key: Key) -> (EventResult, MenuResponse) {
326 match key {
327 Key::Escape => {
328 self.close();
329 crate::animation::request_draw();
330 (EventResult::Consumed, MenuResponse::Closed)
331 }
332 Key::ArrowDown => {
333 self.step_hover(items, 1);
334 (EventResult::Consumed, MenuResponse::None)
335 }
336 Key::ArrowUp => {
337 self.step_hover(items, -1);
338 (EventResult::Consumed, MenuResponse::None)
339 }
340 Key::ArrowRight => {
341 if let Some(path) = self.hover_path.clone() {
342 if item_at_path(items, &path).is_some_and(|item| item.has_submenu()) {
343 self.open_path = path;
344 crate::animation::request_draw();
345 }
346 }
347 (EventResult::Consumed, MenuResponse::None)
348 }
349 Key::ArrowLeft => {
350 self.open_path.pop();
351 self.hover_path = self.open_path.last().map(|_| self.open_path.clone());
352 crate::animation::request_draw();
353 (EventResult::Consumed, MenuResponse::None)
354 }
355 Key::Enter | Key::Char(' ') => {
356 if let Some(path) = self.hover_path.clone() {
357 if let Some(item) = item_at_path(items, &path) {
358 let enabled = item.enabled;
359 let has_submenu = item.has_submenu();
360 let action = item.action.clone();
361 let close_on_activate = item.close_on_activate;
362 if enabled && has_submenu {
363 self.open_path = path;
364 } else if enabled {
365 if let Some(action) = action {
366 return self.activate_action(
367 items,
368 &path,
369 action,
370 close_on_activate,
371 false,
372 );
373 }
374 }
375 }
376 }
377 (EventResult::Consumed, MenuResponse::None)
378 }
379 _ => (EventResult::Ignored, MenuResponse::None),
380 }
381 }
382
383 fn step_hover(&mut self, items: &[MenuEntry], delta: isize) {
384 let level_items = items_at_path(items, &self.open_path).unwrap_or(items);
385 let enabled: Vec<usize> = level_items
386 .iter()
387 .enumerate()
388 .filter_map(|(idx, entry)| match entry {
389 MenuEntry::Item(item) if item.enabled => Some(idx),
390 _ => None,
391 })
392 .collect();
393 if enabled.is_empty() {
394 return;
395 }
396 let current = self
397 .hover_path
398 .as_ref()
399 .and_then(|path| path.last().copied())
400 .and_then(|idx| enabled.iter().position(|candidate| *candidate == idx));
401 let base = current
402 .map(|idx| idx as isize)
403 .unwrap_or(if delta > 0 { -1 } else { 0 });
404 let next = (base + delta).rem_euclid(enabled.len() as isize) as usize;
405 let mut path = self.open_path.clone();
406 path.push(enabled[next]);
407 self.hover_path = Some(path);
408 crate::animation::request_draw();
409 }
410}
411
412fn items_at_path<'a>(items: &'a [MenuEntry], path: &[usize]) -> Option<&'a [MenuEntry]> {
413 let mut current = items;
414 for &idx in path {
415 current = &item_at_path(current, &[idx])?.submenu;
416 }
417 Some(current)
418}
419
420fn toggle_selection_at_path(items: &mut [MenuEntry], path: &[usize]) {
421 let Some(selection) = item_at_path(items, path).map(|item| item.selection) else {
422 return;
423 };
424 match selection {
425 MenuSelection::Check { selected } => {
426 if let Some(item) = item_at_path_mut(items, path) {
427 item.selection = MenuSelection::Check {
428 selected: !selected,
429 };
430 }
431 }
432 MenuSelection::Radio { .. } => {
433 let Some((&idx, parent_path)) = path.split_last() else {
434 return;
435 };
436 let Some(parent) = entries_at_path_mut(items, parent_path) else {
437 return;
438 };
439 for entry in parent.iter_mut() {
440 if let MenuEntry::Item(item) = entry {
441 if matches!(item.selection, MenuSelection::Radio { .. }) {
442 item.selection = MenuSelection::Radio { selected: false };
443 }
444 }
445 }
446 if let Some(MenuEntry::Item(item)) = parent.get_mut(idx) {
447 item.selection = MenuSelection::Radio { selected: true };
448 }
449 }
450 MenuSelection::None => {}
451 }
452}
453
454fn item_at_path_mut<'a>(
455 items: &'a mut [MenuEntry],
456 path: &[usize],
457) -> Option<&'a mut super::model::MenuItem> {
458 let (&idx, rest) = path.split_first()?;
459 let entry = items.get_mut(idx)?;
460 match entry {
461 MenuEntry::Item(item) => {
462 if rest.is_empty() {
463 Some(item)
464 } else {
465 item_at_path_mut(&mut item.submenu, rest)
466 }
467 }
468 MenuEntry::Separator => None,
469 }
470}
471
472fn entries_at_path_mut<'a>(
473 items: &'a mut [MenuEntry],
474 path: &[usize],
475) -> Option<&'a mut [MenuEntry]> {
476 if path.is_empty() {
477 return Some(items);
478 }
479 let (&idx, rest) = path.split_first()?;
480 match items.get_mut(idx)? {
481 MenuEntry::Item(item) => entries_at_path_mut(&mut item.submenu, rest),
482 MenuEntry::Separator => None,
483 }
484}
485
486fn shortcut_path(items: &[MenuEntry], key: &Key, modifiers: Modifiers) -> Option<Vec<usize>> {
487 for (idx, entry) in items.iter().enumerate() {
488 let MenuEntry::Item(item) = entry else {
489 continue;
490 };
491 if item.enabled
492 && item
493 .accelerator
494 .is_some_and(|accelerator| accelerator.matches(key, modifiers))
495 && item.action.is_some()
496 {
497 return Some(vec![idx]);
498 }
499 if let Some(mut path) = shortcut_path(&item.submenu, key, modifiers) {
500 path.insert(0, idx);
501 return Some(path);
502 }
503 }
504 None
505}