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