Skip to main content

agg_gui/widgets/menu/
state.rs

1//! Open menu state and event-side behavior.
2//!
3//! The state keeps only interaction data. Item trees stay in the model so
4//! callers can rebuild or reuse menus without carrying transient hover state.
5
6use 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}