kas_widgets/menu/
mod.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//! Menu widgets
7//!
8//! The following serve as menu roots:
9//!
10//! -   [`crate::ComboBox`]
11//! -   [`MenuBar`]
12//!
13//! Any implementation of the [`Menu`] trait may be used as a menu item:
14//!
15//! -   [`SubMenu`]
16//! -   [`MenuEntry`]
17//! -   [`MenuToggle`]
18//! -   [`Separator`]
19
20use crate::Separator;
21use crate::adapt::MapAny;
22use kas::prelude::*;
23use std::fmt::Debug;
24
25mod menu_entry;
26mod menubar;
27mod submenu;
28
29pub use menu_entry::{MenuEntry, MenuToggle};
30pub use menubar::{MenuBar, MenuBuilder};
31pub use submenu::SubMenu;
32
33/// Return value of [`Menu::sub_items`]
34#[derive(Default)]
35pub struct SubItems<'a> {
36    /// Primary label
37    pub label: Option<&'a mut dyn Tile>,
38    /// Secondary label, often used to show shortcut key
39    pub label2: Option<&'a mut dyn Tile>,
40    /// Sub-menu indicator
41    pub submenu: Option<&'a mut dyn Tile>,
42    /// Icon
43    pub icon: Option<&'a mut dyn Tile>,
44    /// Toggle mark
45    pub toggle: Option<&'a mut dyn Tile>,
46}
47
48/// Trait governing menus, sub-menus and menu-entries
49///
50/// Implementations will automatically receive nav focus on mouse-over, thus
51/// should ensure that [`Tile::probe`] returns the identifier of the widget
52/// which should be focussed, and that this widget has [`Tile::navigable`]
53/// return true.
54#[autoimpl(for<T: trait + ?Sized> Box<T>)]
55pub trait Menu: Widget {
56    /// Access row items for aligned layout
57    ///
58    /// If this returns sub-items, then these items are aligned in the menu view. This involves
59    /// (1) calling `Self::size_rules` and `Self::set_rect` like usual, and (2) running an external
60    /// layout solver on these items (which also calls `size_rules` and `set_rect` on each item).
61    /// This is redundant, but ensures the expectations on [`Layout::size_rules`] and
62    /// [`Layout::set_rect`] are met.
63    ///
64    /// Note further: if this returns `Some(_)`, then spacing for menu item frames is added
65    /// "magically" by the caller. The implementor should draw a frame as follows:
66    /// ```
67    /// # use kas::geom::Rect;
68    /// # use kas::theme::{DrawCx, FrameStyle};
69    /// # struct S;
70    /// # impl S {
71    /// # fn rect(&self) -> Rect { Rect::ZERO }
72    /// fn draw(&self, mut draw: DrawCx) {
73    ///     draw.frame(self.rect(), FrameStyle::MenuEntry, Default::default());
74    ///     // draw children here
75    /// }
76    /// # }
77    /// ```
78    // TODO: adding frame spacing like this is quite hacky. Find a better approach?
79    fn sub_items(&mut self) -> Option<SubItems<'_>> {
80        None
81    }
82
83    /// Report whether a submenu (if any) is open
84    ///
85    /// By default, this is `false`.
86    fn menu_is_open(&self) -> bool {
87        false
88    }
89
90    /// Open or close a sub-menu, including parents
91    ///
92    /// Given `Some(id) = target`, the sub-menu with this `id` should open its
93    /// menu; if it has child-menus, these should close; and if any ancestors
94    /// are menus, these should open.
95    ///
96    /// `target == None` implies that all menus should close.
97    ///
98    /// When opening menus and `set_focus` is true, the first navigable child
99    /// of the newly opened menu will be given focus. This is used for keyboard
100    /// navigation only.
101    fn set_menu_path(
102        &mut self,
103        cx: &mut EventCx,
104        data: &Self::Data,
105        target: Option<&Id>,
106        set_focus: bool,
107    ) {
108        let _ = (cx, data, target, set_focus);
109    }
110}
111
112impl<A, W: Menu<Data = ()>> Menu for MapAny<A, W> {
113    fn sub_items(&mut self) -> Option<SubItems<'_>> {
114        self.inner.sub_items()
115    }
116
117    fn menu_is_open(&self) -> bool {
118        self.inner.menu_is_open()
119    }
120
121    fn set_menu_path(&mut self, cx: &mut EventCx, _: &A, target: Option<&Id>, set_focus: bool) {
122        self.inner.set_menu_path(cx, &(), target, set_focus);
123    }
124}
125
126/// A boxed menu
127pub type BoxedMenu<Data> = Box<dyn Menu<Data = Data>>;
128
129/// Builder for a [`SubMenu`]
130///
131/// Access through [`MenuBar::builder`].
132pub struct SubMenuBuilder<'a, Data> {
133    menu: &'a mut Vec<BoxedMenu<Data>>,
134}
135
136impl<'a, Data> SubMenuBuilder<'a, Data> {
137    /// Append an item
138    #[inline]
139    pub fn push_item(&mut self, item: BoxedMenu<Data>) {
140        self.menu.push(item);
141    }
142
143    /// Append an item, chain style
144    #[inline]
145    pub fn item(mut self, item: BoxedMenu<Data>) -> Self {
146        self.push_item(item);
147        self
148    }
149}
150
151impl<'a, Data: 'static> SubMenuBuilder<'a, Data> {
152    /// Append a [`MenuEntry`]
153    pub fn push_entry<S: Into<AccessString>, M>(&mut self, label: S, msg: M)
154    where
155        M: Clone + Debug + 'static,
156    {
157        self.menu
158            .push(Box::new(MapAny::new(MenuEntry::new_msg(label, msg))));
159    }
160
161    /// Append a [`MenuEntry`], chain style
162    #[inline]
163    pub fn entry<S: Into<AccessString>, M>(mut self, label: S, msg: M) -> Self
164    where
165        M: Clone + Debug + 'static,
166    {
167        self.push_entry(label, msg);
168        self
169    }
170
171    /// Append a [`MenuToggle`]
172    pub fn push_toggle<M: Debug + 'static>(
173        &mut self,
174        label: impl Into<AccessString>,
175        state_fn: impl Fn(&ConfigCx, &Data) -> bool + 'static,
176        msg_fn: impl Fn(bool) -> M + 'static,
177    ) {
178        self.menu
179            .push(Box::new(MenuToggle::new_msg(label, state_fn, msg_fn)));
180    }
181
182    /// Append a [`MenuToggle`], chain style
183    pub fn toggle<M: Debug + 'static>(
184        mut self,
185        label: impl Into<AccessString>,
186        state_fn: impl Fn(&ConfigCx, &Data) -> bool + 'static,
187        msg_fn: impl Fn(bool) -> M + 'static,
188    ) -> Self {
189        self.push_toggle(label, state_fn, msg_fn);
190        self
191    }
192
193    /// Append a [`Separator`]
194    pub fn push_separator(&mut self) {
195        self.menu.push(Box::new(MapAny::new(Separator::new())));
196    }
197
198    /// Append a [`Separator`], chain style
199    #[inline]
200    pub fn separator(mut self) -> Self {
201        self.push_separator();
202        self
203    }
204
205    /// Append a [`SubMenu`]
206    ///
207    /// This submenu prefers opens to the right.
208    pub fn push_submenu<F>(&mut self, label: impl Into<AccessString>, f: F)
209    where
210        F: FnOnce(SubMenuBuilder<Data>),
211    {
212        self.push_submenu_dir(label, f, Direction::Right);
213    }
214
215    /// Append a [`SubMenu`], chain style
216    ///
217    /// This submenu prefers opens to the right.
218    pub fn submenu<F>(mut self, label: impl Into<AccessString>, f: F) -> Self
219    where
220        F: FnOnce(SubMenuBuilder<Data>),
221    {
222        self.push_submenu(label, f);
223        self
224    }
225
226    /// Append a [`SubMenu`]
227    ///
228    /// This submenu prefers to open in the specified direction.
229    pub fn push_submenu_dir<F>(&mut self, label: impl Into<AccessString>, f: F, dir: Direction)
230    where
231        F: FnOnce(SubMenuBuilder<Data>),
232    {
233        let mut menu = Vec::new();
234        f(SubMenuBuilder { menu: &mut menu });
235        self.menu
236            .push(Box::new(SubMenu::<false, _>::new(label, menu, dir)));
237    }
238
239    /// Append a [`SubMenu`], chain style
240    ///
241    /// This submenu prefers to open in the specified direction.
242    #[inline]
243    pub fn submenu_dir<F>(mut self, label: impl Into<AccessString>, f: F, dir: Direction) -> Self
244    where
245        F: FnOnce(SubMenuBuilder<Data>),
246    {
247        self.push_submenu_dir(label, f, dir);
248        self
249    }
250}