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}