kas_widgets/menu/
menubar.rs

1// Licensed under the Apache License, Version 2.0 (the "License");
2// you may not use this file except in compliance with the License.
3// You may obtain a copy of the License in the LICENSE-APACHE file or at:
4//     https://www.apache.org/licenses/LICENSE-2.0
5
6//! Menubar
7
8use super::{Menu, SubMenu, SubMenuBuilder};
9use kas::event::{FocusSource, TimerHandle};
10use kas::layout::{self, RowPositionSolver, RowSetter, RowSolver, RulesSetter, RulesSolver};
11use kas::prelude::*;
12use kas::theme::FrameStyle;
13
14const TIMER_SHOW: TimerHandle = TimerHandle::new(0, false);
15
16#[impl_self]
17mod MenuBar {
18    /// A menu-bar
19    ///
20    /// This widget houses a sequence of menu buttons, allowing input actions across
21    /// menus.
22    #[widget]
23    pub struct MenuBar<Data, D: Directional = kas::dir::Right> {
24        core: widget_core!(),
25        direction: D,
26        widgets: Vec<SubMenu<true, Data>>,
27        layout_store: layout::DynRowStorage,
28        delayed_open: Option<Id>,
29    }
30
31    impl Self
32    where
33        D: Default,
34    {
35        /// Construct a menubar
36        pub fn new(menus: Vec<SubMenu<true, Data>>) -> Self {
37            MenuBar::new_dir(menus, Default::default())
38        }
39
40        /// Construct a menu builder
41        pub fn builder() -> MenuBuilder<Data, D> {
42            MenuBuilder {
43                menus: vec![],
44                direction: D::default(),
45            }
46        }
47    }
48    impl<Data> MenuBar<Data, kas::dir::Right> {
49        /// Construct a menubar
50        pub fn right(menus: Vec<SubMenu<true, Data>>) -> Self {
51            MenuBar::new(menus)
52        }
53    }
54
55    impl Self {
56        /// Construct a menubar with explicit direction
57        pub fn new_dir(menus: Vec<SubMenu<true, Data>>, direction: D) -> Self {
58            MenuBar {
59                core: Default::default(),
60                direction,
61                widgets: menus,
62                layout_store: Default::default(),
63                delayed_open: None,
64            }
65        }
66    }
67
68    impl Layout for Self {
69        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
70            // Unusual behaviour: children's SizeRules are padded with a frame,
71            // but the frame does not adjust the children's rects.
72
73            let len = self.widgets.len();
74            let dim = (self.direction, len + 1);
75            let mut solver = RowSolver::new(axis, dim, &mut self.layout_store);
76            let frame_rules = sizer.frame(FrameStyle::MenuEntry, axis);
77            for (n, child) in self.widgets.iter_mut().enumerate() {
78                solver.for_child(&mut self.layout_store, n, |axis| {
79                    let rules = child.size_rules(sizer.re(), axis);
80                    frame_rules.surround(rules).0
81                });
82            }
83            solver.for_child(&mut self.layout_store, len, |axis| {
84                let mut rules = SizeRules::EMPTY;
85                if axis.is_horizontal() {
86                    rules.set_stretch(Stretch::Maximize);
87                }
88                rules
89            });
90            solver.finish(&mut self.layout_store)
91        }
92
93        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, _: AlignHints) {
94            widget_set_rect!(rect);
95            let dim = (self.direction, self.widgets.len() + 1);
96            let mut setter = RowSetter::<D, Vec<i32>, _>::new(rect, dim, &mut self.layout_store);
97            let hints = AlignHints::CENTER;
98
99            for (n, child) in self.widgets.iter_mut().enumerate() {
100                child.set_rect(cx, setter.child_rect(&mut self.layout_store, n), hints);
101            }
102        }
103
104        fn draw(&self, mut draw: DrawCx) {
105            let solver = RowPositionSolver::new(self.direction);
106            let rect = self.rect();
107            solver.for_children(&self.widgets, rect, |w| w.draw(draw.re()));
108        }
109    }
110
111    impl Tile for Self {
112        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
113            Role::MenuBar
114        }
115
116        #[inline]
117        fn child_indices(&self) -> ChildIndices {
118            (0..self.widgets.len()).into()
119        }
120        fn get_child(&self, index: usize) -> Option<&dyn Tile> {
121            self.widgets.get(index).map(|w| w.as_tile())
122        }
123
124        fn probe(&self, coord: Coord) -> Id {
125            let solver = RowPositionSolver::new(self.direction);
126            solver
127                .find_child(&self.widgets, coord)
128                .and_then(|child| child.try_probe(coord))
129                .unwrap_or_else(|| self.id())
130        }
131    }
132
133    impl Events for Self {
134        fn handle_event(&mut self, cx: &mut EventCx, data: &Data, event: Event) -> IsUsed {
135            match event {
136                Event::Timer(TIMER_SHOW) => {
137                    if let Some(id) = self.delayed_open.clone() {
138                        self.set_menu_path(cx, data, Some(&id), false);
139                    }
140                    Used
141                }
142                Event::PressStart(press) => {
143                    if press
144                        .id
145                        .as_ref()
146                        .map(|id| self.is_ancestor_of(id))
147                        .unwrap_or(false)
148                    {
149                        if press.is_primary() {
150                            let any_menu_open = self.widgets.iter().any(|w| w.menu_is_open());
151                            let press_in_the_bar = self.rect().contains(press.coord());
152
153                            if !press_in_the_bar || !any_menu_open {
154                                press.grab_move(self.id()).complete(cx);
155                            }
156                            cx.set_grab_depress(*press, press.id.clone());
157                            if press_in_the_bar {
158                                if self
159                                    .widgets
160                                    .iter()
161                                    .any(|w| w.eq_id(&press.id) && !w.menu_is_open())
162                                {
163                                    self.set_menu_path(cx, data, press.id.as_ref(), false);
164                                } else {
165                                    self.set_menu_path(cx, data, None, false);
166                                }
167                            }
168                        }
169                        Used
170                    } else {
171                        // Click happened out of the menubar or submenus,
172                        // while one or more submenus are opened.
173                        self.delayed_open = None;
174                        self.set_menu_path(cx, data, None, false);
175                        Unused
176                    }
177                }
178                Event::CursorMove { press } | Event::PressMove { press, .. } => {
179                    cx.set_grab_depress(*press, press.id.clone());
180
181                    let id = match press.id {
182                        Some(x) => x,
183                        None => return Used,
184                    };
185
186                    if self.is_strict_ancestor_of(&id) {
187                        // We instantly open a sub-menu on motion over the bar,
188                        // but delay when over a sub-menu (most intuitive?)
189                        if self.rect().contains(press.coord) {
190                            cx.clear_nav_focus();
191                            self.delayed_open = None;
192                            self.set_menu_path(cx, data, Some(&id), false);
193                        } else if id != self.delayed_open {
194                            cx.request_nav_focus(id.clone(), FocusSource::Pointer);
195                            let delay = cx.config().event().menu_delay();
196                            cx.request_timer(self.id(), TIMER_SHOW, delay);
197                            self.delayed_open = Some(id);
198                        }
199                    } else {
200                        self.delayed_open = None;
201                    }
202                    Used
203                }
204                Event::PressEnd { press, success, .. } if success => {
205                    let id = match press.id {
206                        Some(x) => x,
207                        None => return Used,
208                    };
209
210                    if !self.rect().contains(press.coord) {
211                        // not on the menubar
212                        self.delayed_open = None;
213                        cx.send(id, Command::Activate);
214                    }
215                    Used
216                }
217                Event::Command(cmd, _) => {
218                    // Arrow keys can switch to the next / previous menu
219                    // as well as to the first / last item of an open menu.
220                    use Command::{Left, Up};
221                    let is_vert = self.direction.is_vertical();
222                    let reverse = self.direction.is_reversed() ^ matches!(cmd, Left | Up);
223                    match cmd.as_direction().map(|d| d.is_vertical()) {
224                        Some(v) if v == is_vert => {
225                            for i in 0..self.widgets.len() {
226                                if self.widgets[i].menu_is_open() {
227                                    let mut j = isize::conv(i);
228                                    j = if reverse { j - 1 } else { j + 1 };
229                                    j = j.rem_euclid(self.widgets.len().cast());
230                                    self.widgets[i].set_menu_path(cx, data, None, true);
231                                    let w = &mut self.widgets[usize::conv(j)];
232                                    w.set_menu_path(cx, data, Some(&w.id()), true);
233                                    break;
234                                }
235                            }
236                            Used
237                        }
238                        Some(_) => {
239                            cx.next_nav_focus(self.id(), reverse, FocusSource::Key);
240                            Used
241                        }
242                        None => Unused,
243                    }
244                }
245                _ => Unused,
246            }
247        }
248    }
249
250    impl Widget for Self {
251        type Data = Data;
252
253        fn child_node<'n>(&'n mut self, data: &'n Data, index: usize) -> Option<Node<'n>> {
254            self.widgets.get_mut(index).map(|w| w.as_node(data))
255        }
256    }
257
258    impl Self {
259        fn set_menu_path(
260            &mut self,
261            cx: &mut EventCx,
262            data: &Data,
263            target: Option<&Id>,
264            set_focus: bool,
265        ) {
266            log::trace!(
267                "set_menu_path: self={}, target={target:?}, set_focus={set_focus}",
268                self.identify()
269            );
270            self.delayed_open = None;
271            for i in 0..self.widgets.len() {
272                self.widgets[i].set_menu_path(cx, data, target, set_focus);
273            }
274        }
275    }
276}
277
278/// Builder for [`MenuBar`]
279///
280/// Access through [`MenuBar::builder`].
281pub struct MenuBuilder<Data, D: Directional> {
282    menus: Vec<SubMenu<true, Data>>,
283    direction: D,
284}
285
286impl<Data, D: Directional> MenuBuilder<Data, D> {
287    /// Add a new menu
288    ///
289    /// The menu's direction is determined via [`Directional::Flipped`].
290    pub fn menu<F>(mut self, label: impl Into<AccessString>, f: F) -> Self
291    where
292        F: FnOnce(SubMenuBuilder<Data>),
293    {
294        let mut menu = Vec::new();
295        f(SubMenuBuilder { menu: &mut menu });
296        let dir = self.direction.as_direction().flipped();
297        self.menus.push(SubMenu::new(label, menu, dir));
298        self
299    }
300
301    /// Finish, yielding a [`MenuBar`]
302    pub fn build(self) -> MenuBar<Data, D> {
303        MenuBar::new_dir(self.menus, self.direction)
304    }
305}