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