Skip to main content

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::Label),
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
136    impl Events for Self {
137        const REDRAW_ON_MOUSE_OVER: bool = true;
138
139        type Data = Data;
140
141        fn probe(&self, _: Coord) -> Id {
142            self.id()
143        }
144
145        fn handle_event(&mut self, cx: &mut EventCx, data: &Data, event: Event) -> IsUsed {
146            match event {
147                Event::Command(cmd, code) if cmd.is_activate() => {
148                    self.open_menu(cx, data, true);
149                    cx.depress_with_key(&self, code);
150                    Used
151                }
152                Event::Command(cmd, _) => self.handle_dir_key(cx, data, cmd),
153                _ => Unused,
154            }
155        }
156
157        fn handle_messages(&mut self, cx: &mut EventCx, data: &Data) {
158            if let Some(Activate(code)) = cx.try_pop() {
159                self.popup.open(cx, data, self.id(), true);
160                cx.depress_with_key(&self, code);
161            } else if let Some(Expand) = cx.try_pop() {
162                self.popup.open(cx, data, self.id(), true);
163            } else if let Some(Collapse) = cx.try_pop() {
164                self.popup.close(cx);
165            } else {
166                self.popup.close(cx);
167            }
168        }
169    }
170
171    impl Menu for Self {
172        fn sub_items(&mut self) -> Option<SubItems<'_>> {
173            Some(SubItems {
174                label: Some(&mut self.label),
175                submenu: Some(&mut self.mark),
176                ..Default::default()
177            })
178        }
179
180        fn menu_is_open(&self) -> bool {
181            self.popup.is_open()
182        }
183
184        fn set_menu_path(
185            &mut self,
186            cx: &mut EventCx,
187            data: &Data,
188            target: Option<&Id>,
189            set_focus: bool,
190        ) {
191            if !self.id_ref().is_valid() {
192                return;
193            }
194
195            match target {
196                Some(id) if self.is_ancestor_of(id) => {
197                    self.open_menu(cx, data, set_focus);
198                }
199                _ => self.popup.close(cx),
200            }
201
202            for i in 0..self.popup.inner.len() {
203                self.popup.inner[i].set_menu_path(cx, data, target, set_focus);
204            }
205        }
206    }
207}
208
209const MENU_VIEW_COLS: u32 = 5;
210const fn menu_view_row_info(row: u32) -> layout::GridCellInfo {
211    layout::GridCellInfo {
212        col: 0,
213        last_col: MENU_VIEW_COLS - 1,
214        row,
215        last_row: row,
216    }
217}
218
219#[impl_self]
220mod MenuView {
221    /// A menu view
222    #[widget]
223    struct MenuView<W: Menu> {
224        core: widget_core!(),
225        dim: layout::GridDimensions,
226        store: layout::DynGridStorage, //NOTE(opt): number of columns is fixed
227        list: Vec<W>,
228    }
229
230    impl Layout for Self {
231        fn size_rules(&mut self, cx: &mut SizeCx, axis: AxisInfo) -> SizeRules {
232            self.dim = layout::GridDimensions {
233                cols: MENU_VIEW_COLS,
234                col_spans: self
235                    .list
236                    .iter_mut()
237                    .filter_map(|w| w.sub_items().is_none().then_some(()))
238                    .count()
239                    .cast(),
240                rows: self.list.len().cast(),
241                row_spans: 0,
242            };
243
244            let store = &mut self.store;
245            let mut solver = layout::GridSolver::<Vec<_>, Vec<_>, _>::new(axis, self.dim, store);
246
247            let frame_rules = cx.frame(FrameStyle::MenuEntry, axis);
248
249            // Assumption: frame inner margin is at least as large as content margins
250            let child_rules = SizeRules::EMPTY;
251            let (_, _, frame_size_flipped) = cx
252                .frame(FrameStyle::MenuEntry, axis.flipped())
253                .surround(child_rules);
254
255            let child_rules = |cx: &mut SizeCx, w: &mut dyn Tile, mut axis: AxisInfo| {
256                axis.map_other(|x| x - frame_size_flipped);
257                let rules = w.size_rules(cx, axis);
258                frame_rules.surround(rules).0
259            };
260
261            for (row, child) in self.list.iter_mut().enumerate() {
262                let row = u32::conv(row);
263                let info = menu_view_row_info(row);
264
265                // Note: we are required to call child.size_rules even if sub_items are used
266                // Note: axis is not modified by the solver in this case
267                let rules = child.size_rules(cx, axis);
268
269                // Note: if we use sub-items, we are required to call size_rules
270                // on these for both axes
271                if let Some(items) = child.sub_items() {
272                    if let Some(w) = items.toggle {
273                        let info = layout::GridCellInfo::new(0, row);
274                        solver.for_child(store, info, |axis| child_rules(cx, w, axis));
275                    }
276                    if let Some(w) = items.icon {
277                        let info = layout::GridCellInfo::new(1, row);
278                        solver.for_child(store, info, |axis| child_rules(cx, w, axis));
279                    }
280                    if let Some(w) = items.label {
281                        let info = layout::GridCellInfo::new(2, row);
282                        solver.for_child(store, info, |axis| child_rules(cx, w, axis));
283                    }
284                    if let Some(w) = items.label2 {
285                        let info = layout::GridCellInfo::new(3, row);
286                        solver.for_child(store, info, |axis| child_rules(cx, w, axis));
287                    }
288                    if let Some(w) = items.submenu {
289                        let info = layout::GridCellInfo::new(4, row);
290                        solver.for_child(store, info, |axis| child_rules(cx, w, axis));
291                    }
292                } else {
293                    solver.for_child(store, info, |_| rules);
294                }
295            }
296            solver.finish(store)
297        }
298
299        fn set_rect(&mut self, cx: &mut SizeCx, rect: Rect, _: AlignHints) {
300            self.core.set_rect(rect);
301            let store = &mut self.store;
302            let hints = AlignHints::NONE;
303            let mut setter = layout::GridSetter::<Vec<_>, Vec<_>, _>::new(rect, self.dim, store);
304
305            // Assumption: frame inner margin is at least as large as content margins
306            let child_rules = SizeRules::EMPTY;
307            let (_, frame_x, frame_w) = cx
308                .frame(FrameStyle::MenuEntry, Direction::Right)
309                .surround(child_rules);
310            let (_, frame_y, frame_h) = 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            ChildIndices::range(0..self.list.len())
367        }
368        fn get_child(&self, index: usize) -> Option<&dyn Tile> {
369            self.list.get(index).map(|w| w.as_tile())
370        }
371    }
372
373    impl Events for Self {
374        fn probe(&self, coord: Coord) -> Id {
375            for child in self.list.iter() {
376                if let Some(id) = child.try_probe(coord) {
377                    return id;
378                }
379            }
380            self.id()
381        }
382    }
383
384    impl Widget for Self {
385        type Data = W::Data;
386
387        fn child_node<'n>(&'n mut self, data: &'n W::Data, index: usize) -> Option<Node<'n>> {
388            self.list.get_mut(index).map(|w| w.as_node(data))
389        }
390    }
391
392    impl Self {
393        /// Construct from a list of menu items
394        pub fn new(list: Vec<W>) -> Self {
395            MenuView {
396                core: Default::default(),
397                dim: Default::default(),
398                store: Default::default(),
399                list,
400            }
401        }
402
403        /// Number of menu items
404        pub fn len(&self) -> usize {
405            self.list.len()
406        }
407    }
408
409    impl std::ops::Index<usize> for Self {
410        type Output = W;
411
412        fn index(&self, index: usize) -> &Self::Output {
413            &self.list[index]
414        }
415    }
416
417    impl std::ops::IndexMut<usize> for Self {
418        fn index_mut(&mut self, index: usize) -> &mut Self::Output {
419            &mut self.list[index]
420        }
421    }
422}