1use std::rc::Rc;
6
7use crabshell_reactive::Memo;
8
9use crate::cx::Cx;
10use crate::popup::PopupWindow;
11use crate::prop::{IntoProp, Prop};
12use crate::style::{Color, Style};
13use crate::widgets::{
14 Align, Button, Column, Element, ElementKind, Row, Spacer, Stack, Text, Widget,
15};
16
17#[derive(Clone, Debug, PartialEq)]
19pub struct MenuEntry {
20 pub id: i32,
22 pub label: String,
24 pub separator: bool,
26 pub enabled: bool,
28 pub toggle: Option<MenuToggle>,
30 pub checked: bool,
32 pub submenu: bool,
35 pub children: Vec<MenuEntry>,
37}
38
39#[derive(Clone, Copy, Debug, PartialEq, Eq)]
41pub enum MenuToggle {
42 Checkmark,
44 Radio,
46}
47
48const ITEM: Style = Style::new().padding(4, 12);
50const HOVER: Style = Style::new().background(Color::rgba(128, 128, 128, 64));
52const SEPARATOR: Style = Style::new()
54 .background(Color::rgba(128, 128, 128, 128))
55 .padding(1, 0);
56const DISABLED: Style = Style::new().opacity(0.5);
58const HIDDEN: Style = Style::new().opacity(0.0);
60
61pub struct Menu {
87 entries: Prop<Vec<MenuEntry>>,
88 style: Option<Prop<Style>>,
89 item_style: Option<Prop<Style>>,
90 hover_style: Option<Prop<Style>>,
91 separator_style: Option<Prop<Style>>,
92 on_activate: Option<EntryHandler>,
93 on_open_submenu: Option<EntryHandler>,
94}
95
96impl Menu {
97 pub fn new(entries: impl IntoProp<Vec<MenuEntry>>) -> Menu {
99 Menu {
100 entries: entries.into_prop(),
101 style: None,
102 item_style: None,
103 hover_style: None,
104 separator_style: None,
105 on_activate: None,
106 on_open_submenu: None,
107 }
108 }
109
110 pub fn style(mut self, style: impl IntoProp<Style>) -> Menu {
112 self.style = Some(style.into_prop());
113 self
114 }
115
116 pub fn item_style(mut self, style: impl IntoProp<Style>) -> Menu {
119 self.item_style = Some(style.into_prop());
120 self
121 }
122
123 pub fn hover_style(mut self, style: impl IntoProp<Style>) -> Menu {
126 self.hover_style = Some(style.into_prop());
127 self
128 }
129
130 pub fn separator_style(mut self, style: impl IntoProp<Style>) -> Menu {
133 self.separator_style = Some(style.into_prop());
134 self
135 }
136
137 pub fn on_activate(mut self, handler: impl Fn(&Cx, i32) + 'static) -> Menu {
139 self.on_activate = Some(Rc::new(handler));
140 self
141 }
142
143 pub fn on_open_submenu(mut self, handler: impl Fn(&Cx, i32) + 'static) -> Menu {
146 self.on_open_submenu = Some(Rc::new(handler));
147 self
148 }
149}
150
151struct Look {
154 menu: Shared<Style>,
155 item: Shared<Style>,
156 hover: Shared<Style>,
157 separator: Shared<Style>,
158 on_activate: Option<EntryHandler>,
159 on_open_submenu: Option<EntryHandler>,
160}
161
162type Shared<T> = Rc<dyn Fn() -> T>;
163
164type EntryHandler = Rc<dyn Fn(&Cx, i32)>;
166
167fn shared<T: Clone + 'static>(prop: Option<Prop<T>>, default: T) -> Shared<T> {
169 prop.map_or_else(|| Rc::new(move || default.clone()), Prop::shared)
170}
171
172impl Widget for Menu {
173 fn into_element(self: Box<Self>) -> Element {
174 let look = Rc::new(Look {
175 menu: shared(self.style, Style::new()),
176 item: shared(self.item_style, ITEM),
177 hover: shared(self.hover_style, HOVER),
178 separator: shared(self.separator_style, SEPARATOR),
179 on_activate: self.on_activate,
180 on_open_submenu: self.on_open_submenu,
181 });
182 menu(look, shared(Some(self.entries), Vec::new()))
183 }
184}
185
186struct Built(Element);
189
190impl Widget for Built {
191 fn into_element(self: Box<Self>) -> Element {
192 self.0
193 }
194}
195
196fn menu(look: Rc<Look>, entries: Shared<Vec<MenuEntry>>) -> Element {
198 let style = Rc::clone(&look.menu);
199 let column = Column::new().style(move || style()).children_keyed(
200 move || entries(),
201 |entry: &MenuEntry| (entry.id, entry.separator),
204 move |_cx, entry| entry_view(&look, entry),
205 );
206 let mut element = Box::new(column).into_element();
207 element.name = "Menu";
208 element
209}
210
211fn entry_view(look: &Rc<Look>, entry: Memo<MenuEntry>) -> Built {
214 if entry.with_untracked(|entry| entry.separator) {
215 let separator = Rc::clone(&look.separator);
216 let line = Stack::new().style(move || separator());
217 return Built(Box::new(Column::new().padding(3, 0).child(line)).into_element());
218 }
219 let mark = move || match entry.get().toggle {
220 Some(MenuToggle::Radio) => "●".to_owned(),
221 _ => "✓".to_owned(),
222 };
223 let shown = move || {
224 let entry = entry.get();
225 if entry.toggle.is_some() && entry.checked {
226 Style::new()
227 } else {
228 HIDDEN
229 }
230 };
231 let arrow = move || if entry.get().submenu { "▸" } else { "" }.to_owned();
232 let marks = Stack::new()
234 .child(Text::new("✓").style(HIDDEN))
235 .child(Text::new("●").style(HIDDEN))
236 .child(Text::new(mark).style(shown));
237 let row = Row::new()
238 .gap(8)
239 .align(Align::Center)
240 .child(marks)
241 .child(Text::new(move || entry.get().label))
242 .child(Spacer::new())
243 .child(Text::new(arrow));
244 let mut row = Box::new(row).into_element();
247 if let ElementKind::Flex(flex) = &mut row.kind {
248 flex.grow = true;
249 }
250 let (item, hover) = (Rc::clone(&look.item), Rc::clone(&look.hover));
251 let clicked = Rc::clone(look);
252 let button = Button::new(Built(row))
253 .style(move || {
254 let style = item();
255 if entry.get().enabled {
256 style
257 } else {
258 style.merge(DISABLED)
259 }
260 })
261 .hover_style(move || {
262 if entry.get().enabled {
263 hover()
264 } else {
265 Style::new()
266 }
267 })
268 .on_click(move |cx| click(&clicked, cx, entry));
269 Built(Box::new(button).into_element())
270}
271
272fn click(look: &Rc<Look>, cx: &Cx, entry: Memo<MenuEntry>) {
275 let (id, enabled, submenu) =
276 entry.with_untracked(|entry| (entry.id, entry.enabled, entry.submenu));
277 if !enabled {
278 return;
279 }
280 if submenu {
281 if let Some(open) = &look.on_open_submenu {
282 open(cx, id);
283 }
284 let look = Rc::clone(look);
285 let children: Shared<Vec<MenuEntry>> = Rc::new(move || {
286 let children = entry.try_with(|entry| entry.children.clone());
287 children.unwrap_or_default()
288 });
289 cx.open_popup(move |_| PopupWindow::new().child(Built(menu(look, children))));
290 return;
291 }
292 if let Some(activate) = &look.on_activate {
293 activate(cx, id);
294 }
295 cx.close_popups_if_any();
297}