kas_widgets/menu/
submenu.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//! Sub-menu
7
8use super::{BoxedMenu, Menu, SubItems};
9use crate::{AccessLabel, Mark};
10use kas::event::FocusSource;
11use kas::layout::{self, RulesSetter, RulesSolver};
12use kas::messages::{Activate, Collapse, Expand};
13use kas::prelude::*;
14use kas::theme::{FrameStyle, MarkStyle, TextClass};
15use kas::window::Popup;
16
17#[impl_self]
18mod SubMenu {
19    /// A sub-menu
20    ///
21    /// # Messages
22    ///
23    /// [`kas::messages::Activate`] may be used to open the sub-menu.
24    ///
25    /// [`kas::messages::Expand`] and [`kas::messages::Collapse`] may be used to
26    /// open and close the menu.
27    #[widget]
28    #[layout(self.label)]
29    pub struct SubMenu<const TOP_LEVEL: bool, Data> {
30        core: widget_core!(),
31        #[widget(&())]
32        label: AccessLabel,
33        // mark is not used in layout but may be used by sub_items
34        #[widget(&())]
35        mark: Mark,
36        #[widget]
37        popup: Popup<MenuView<BoxedMenu<Data>>>,
38    }
39
40    impl Self {
41        /// Construct a sub-menu, opening to the right
42        pub fn right<S: Into<AccessString>>(label: S, list: Vec<BoxedMenu<Data>>) -> Self {
43            SubMenu::new(label, list, Direction::Right)
44        }
45
46        /// Construct a sub-menu, opening downwards
47        pub fn down<S: Into<AccessString>>(label: S, list: Vec<BoxedMenu<Data>>) -> Self {
48            SubMenu::new(label, list, Direction::Down)
49        }
50
51        /// Construct a sub-menu
52        #[inline]
53        pub fn new<S: Into<AccessString>>(
54            label: S,
55            list: Vec<BoxedMenu<Data>>,
56            direction: Direction,
57        ) -> Self {
58            SubMenu {
59                core: Default::default(),
60                label: AccessLabel::new(label).with_class(TextClass::MenuLabel),
61                mark: Mark::new(MarkStyle::Chevron(direction), "Open"),
62                popup: Popup::new(MenuView::new(list), direction),
63            }
64        }
65
66        fn open_menu(&mut self, cx: &mut EventCx, data: &Data, set_focus: bool) {
67            if self.popup.open(cx, data, self.id(), true) {
68                if set_focus {
69                    cx.next_nav_focus(self.id(), false, FocusSource::Key);
70                }
71            }
72        }
73
74        fn handle_dir_key(&mut self, cx: &mut EventCx, data: &Data, cmd: Command) -> IsUsed {
75            if self.menu_is_open() {
76                if let Some(dir) = cmd.as_direction() {
77                    if dir.is_vertical() {
78                        let rev = dir.is_reversed();
79                        cx.next_nav_focus(None, rev, FocusSource::Key);
80                        Used
81                    } else if dir == self.popup.direction().reversed() {
82                        self.popup.close(cx);
83                        Used
84                    } else {
85                        Unused
86                    }
87                } else if matches!(cmd, Command::Home | Command::End) {
88                    cx.clear_nav_focus();
89                    let rev = cmd == Command::End;
90                    cx.next_nav_focus(self.id(), rev, FocusSource::Key);
91                    Used
92                } else {
93                    Unused
94                }
95            } else if Some(self.popup.direction()) == cmd.as_direction() {
96                self.open_menu(cx, data, true);
97                Used
98            } else {
99                Unused
100            }
101        }
102
103        /// Get text contents
104        pub fn as_str(&self) -> &str {
105            self.label.as_str()
106        }
107    }
108
109    impl Layout for Self {
110        fn draw(&self, mut draw: DrawCx) {
111            draw.frame(self.rect(), FrameStyle::MenuEntry, Default::default());
112            self.label.draw(draw.re());
113            if self.mark.rect().size != Size::ZERO {
114                self.mark.draw(draw.re());
115            }
116        }
117    }
118
119    impl Tile for Self {
120        fn navigable(&self) -> bool {
121            !TOP_LEVEL
122        }
123
124        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
125            Role::Menu {
126                expanded: self.popup.is_open(),
127            }
128        }
129
130        fn nav_next(&self, _: bool, _: Option<usize>) -> Option<usize> {
131            // We have no child within our rect
132            None
133        }
134
135        fn probe(&self, _: Coord) -> Id {
136            self.id()
137        }
138    }
139
140    impl Events for Self {
141        type Data = Data;
142
143        fn handle_event(&mut self, cx: &mut EventCx, data: &Data, event: Event) -> IsUsed {
144            match event {
145                Event::Command(cmd, code) if cmd.is_activate() => {
146                    self.open_menu(cx, data, true);
147                    cx.depress_with_key(&self, code);
148                    Used
149                }
150                Event::Command(cmd, _) => self.handle_dir_key(cx, data, cmd),
151                _ => Unused,
152            }
153        }
154
155        fn handle_messages(&mut self, cx: &mut EventCx, data: &Data) {
156            if let Some(Activate(code)) = cx.try_pop() {
157                self.popup.open(cx, data, self.id(), true);
158                cx.depress_with_key(&self, code);
159            } else if let Some(Expand) = cx.try_pop() {
160                self.popup.open(cx, data, self.id(), true);
161            } else if let Some(Collapse) = cx.try_pop() {
162                self.popup.close(cx);
163            } else {
164                self.popup.close(cx);
165            }
166        }
167    }
168
169    impl Menu for Self {
170        fn sub_items(&mut self) -> Option<SubItems<'_>> {
171            Some(SubItems {
172                label: Some(&mut self.label),
173                submenu: Some(&mut self.mark),
174                ..Default::default()
175            })
176        }
177
178        fn menu_is_open(&self) -> bool {
179            self.popup.is_open()
180        }
181
182        fn set_menu_path(
183            &mut self,
184            cx: &mut EventCx,
185            data: &Data,
186            target: Option<&Id>,
187            set_focus: bool,
188        ) {
189            if !self.id_ref().is_valid() {
190                return;
191            }
192
193            match target {
194                Some(id) if self.is_ancestor_of(id) => {
195                    self.open_menu(cx, data, set_focus);
196                }
197                _ => self.popup.close(cx),
198            }
199
200            for i in 0..self.popup.inner.len() {
201                self.popup.inner[i].set_menu_path(cx, data, target, set_focus);
202            }
203        }
204    }
205}
206
207const MENU_VIEW_COLS: u32 = 5;
208const fn menu_view_row_info(row: u32) -> layout::GridCellInfo {
209    layout::GridCellInfo {
210        col: 0,
211        last_col: MENU_VIEW_COLS - 1,
212        row,
213        last_row: row,
214    }
215}
216
217#[impl_self]
218mod MenuView {
219    /// A menu view
220    #[widget]
221    struct MenuView<W: Menu> {
222        core: widget_core!(),
223        dim: layout::GridDimensions,
224        store: layout::DynGridStorage, //NOTE(opt): number of columns is fixed
225        list: Vec<W>,
226    }
227
228    impl Layout for Self {
229        fn size_rules(&mut self, sizer: SizeCx, axis: AxisInfo) -> SizeRules {
230            self.dim = layout::GridDimensions {
231                cols: MENU_VIEW_COLS,
232                col_spans: self
233                    .list
234                    .iter_mut()
235                    .filter_map(|w| w.sub_items().is_none().then_some(()))
236                    .count()
237                    .cast(),
238                rows: self.list.len().cast(),
239                row_spans: 0,
240            };
241
242            let store = &mut self.store;
243            let mut solver = layout::GridSolver::<Vec<_>, Vec<_>, _>::new(axis, self.dim, store);
244
245            let frame_rules = sizer.frame(FrameStyle::MenuEntry, axis);
246
247            // Assumption: frame inner margin is at least as large as content margins
248            let child_rules = SizeRules::EMPTY;
249            let (_, _, frame_size_flipped) = sizer
250                .frame(FrameStyle::MenuEntry, axis.flipped())
251                .surround(child_rules);
252
253            let child_rules = |sizer: SizeCx, w: &mut dyn Tile, mut axis: AxisInfo| {
254                axis.sub_other(frame_size_flipped);
255                let rules = w.size_rules(sizer, axis);
256                frame_rules.surround(rules).0
257            };
258
259            for (row, child) in self.list.iter_mut().enumerate() {
260                let row = u32::conv(row);
261                let info = menu_view_row_info(row);
262
263                // Note: we are required to call child.size_rules even if sub_items are used
264                // Note: axis is not modified by the solver in this case
265                let rules = child.size_rules(sizer.re(), axis);
266
267                // Note: if we use sub-items, we are required to call size_rules
268                // on these for both axes
269                if let Some(items) = child.sub_items() {
270                    if let Some(w) = items.toggle {
271                        let info = layout::GridCellInfo::new(0, row);
272                        solver.for_child(store, info, |axis| child_rules(sizer.re(), w, axis));
273                    }
274                    if let Some(w) = items.icon {
275                        let info = layout::GridCellInfo::new(1, row);
276                        solver.for_child(store, info, |axis| child_rules(sizer.re(), w, axis));
277                    }
278                    if let Some(w) = items.label {
279                        let info = layout::GridCellInfo::new(2, row);
280                        solver.for_child(store, info, |axis| child_rules(sizer.re(), w, axis));
281                    }
282                    if let Some(w) = items.label2 {
283                        let info = layout::GridCellInfo::new(3, row);
284                        solver.for_child(store, info, |axis| child_rules(sizer.re(), w, axis));
285                    }
286                    if let Some(w) = items.submenu {
287                        let info = layout::GridCellInfo::new(4, row);
288                        solver.for_child(store, info, |axis| child_rules(sizer.re(), w, axis));
289                    }
290                } else {
291                    solver.for_child(store, info, |_| rules);
292                }
293            }
294            solver.finish(store)
295        }
296
297        fn set_rect(&mut self, cx: &mut ConfigCx, rect: Rect, _: AlignHints) {
298            widget_set_rect!(rect);
299            let store = &mut self.store;
300            let hints = AlignHints::NONE;
301            let mut setter = layout::GridSetter::<Vec<_>, Vec<_>, _>::new(rect, self.dim, store);
302
303            // Assumption: frame inner margin is at least as large as content margins
304            let child_rules = SizeRules::EMPTY;
305            let (_, frame_x, frame_w) = cx
306                .size_cx()
307                .frame(FrameStyle::MenuEntry, Direction::Right)
308                .surround(child_rules);
309            let (_, frame_y, frame_h) = cx
310                .size_cx()
311                .frame(FrameStyle::MenuEntry, Direction::Down)
312                .surround(child_rules);
313            let frame_offset = Offset(frame_x, frame_y);
314            let frame_size = Size(frame_w, frame_h);
315            let subtract_frame = |mut rect: Rect| {
316                rect.pos += frame_offset;
317                rect.size -= frame_size;
318                rect
319            };
320
321            for (row, child) in self.list.iter_mut().enumerate() {
322                let row = u32::conv(row);
323                let child_rect = setter.child_rect(store, menu_view_row_info(row));
324                // Note: we are required to call child.set_rect even if sub_items are used
325                child.set_rect(cx, child_rect, hints);
326
327                if let Some(items) = child.sub_items() {
328                    if let Some(w) = items.toggle {
329                        let info = layout::GridCellInfo::new(0, row);
330                        w.set_rect(cx, subtract_frame(setter.child_rect(store, info)), hints);
331                    }
332                    if let Some(w) = items.icon {
333                        let info = layout::GridCellInfo::new(1, row);
334                        w.set_rect(cx, subtract_frame(setter.child_rect(store, info)), hints);
335                    }
336                    if let Some(w) = items.label {
337                        let info = layout::GridCellInfo::new(2, row);
338                        w.set_rect(cx, subtract_frame(setter.child_rect(store, info)), hints);
339                    }
340                    if let Some(w) = items.label2 {
341                        let info = layout::GridCellInfo::new(3, row);
342                        w.set_rect(cx, subtract_frame(setter.child_rect(store, info)), hints);
343                    }
344                    if let Some(w) = items.submenu {
345                        let info = layout::GridCellInfo::new(4, row);
346                        w.set_rect(cx, subtract_frame(setter.child_rect(store, info)), hints);
347                    }
348                }
349            }
350        }
351
352        fn draw(&self, mut draw: DrawCx) {
353            for child in self.list.iter() {
354                child.draw(draw.re());
355            }
356        }
357    }
358
359    impl Tile for Self {
360        fn role(&self, _: &mut dyn RoleCx) -> Role<'_> {
361            Role::None
362        }
363
364        #[inline]
365        fn child_indices(&self) -> ChildIndices {
366            (0..self.list.len()).into()
367        }
368        fn get_child(&self, index: usize) -> Option<&dyn Tile> {
369            self.list.get(index).map(|w| w.as_tile())
370        }
371
372        fn probe(&self, coord: Coord) -> Id {
373            for child in self.list.iter() {
374                if let Some(id) = child.try_probe(coord) {
375                    return id;
376                }
377            }
378            self.id()
379        }
380    }
381
382    impl Widget for Self {
383        type Data = W::Data;
384
385        fn child_node<'n>(&'n mut self, data: &'n W::Data, index: usize) -> Option<Node<'n>> {
386            self.list.get_mut(index).map(|w| w.as_node(data))
387        }
388    }
389
390    impl Self {
391        /// Construct from a list of menu items
392        pub fn new(list: Vec<W>) -> Self {
393            MenuView {
394                core: Default::default(),
395                dim: Default::default(),
396                store: Default::default(),
397                list,
398            }
399        }
400
401        /// Number of menu items
402        pub fn len(&self) -> usize {
403            self.list.len()
404        }
405    }
406
407    impl std::ops::Index<usize> for Self {
408        type Output = W;
409
410        fn index(&self, index: usize) -> &Self::Output {
411            &self.list[index]
412        }
413    }
414
415    impl std::ops::IndexMut<usize> for Self {
416        fn index_mut(&mut self, index: usize) -> &mut Self::Output {
417            &mut self.list[index]
418        }
419    }
420}