1use crate::{
2 border::BorderBuilder,
3 brush::Brush,
4 core::{algebra::Vector2, color::Color, pool::Handle},
5 decorator::DecoratorBuilder,
6 define_constructor,
7 grid::{Column, GridBuilder, Row},
8 message::{ButtonState, MessageDirection, OsEvent, UiMessage},
9 popup::{Placement, Popup, PopupBuilder, PopupMessage},
10 stack_panel::StackPanelBuilder,
11 text::TextBuilder,
12 widget::{Widget, WidgetBuilder, WidgetMessage},
13 BuildContext, Control, HorizontalAlignment, NodeHandleMapping, Orientation, RestrictionEntry,
14 Thickness, UiNode, UserInterface, VerticalAlignment, BRUSH_BRIGHT_BLUE, BRUSH_PRIMARY,
15};
16use std::sync::mpsc::Sender;
17use std::{
18 any::{Any, TypeId},
19 ops::{Deref, DerefMut},
20 rc::Rc,
21};
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum MenuMessage {
25 Activate,
26 Deactivate,
27}
28
29impl MenuMessage {
30 define_constructor!(MenuMessage:Activate => fn activate(), layout: false);
31 define_constructor!(MenuMessage:Deactivate => fn deactivate(), layout: false);
32}
33
34#[derive(Debug, Clone, PartialEq)]
35pub enum MenuItemMessage {
36 Open,
37 Close,
38 Click,
39}
40
41impl MenuItemMessage {
42 define_constructor!(MenuItemMessage:Open => fn open(), layout: false);
43 define_constructor!(MenuItemMessage:Close => fn close(), layout: false);
44 define_constructor!(MenuItemMessage:Click => fn click(), layout: false);
45}
46
47#[derive(Clone)]
48pub struct Menu {
49 widget: Widget,
50 active: bool,
51}
52
53crate::define_widget_deref!(Menu);
54
55impl Control for Menu {
56 fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
57 if type_id == TypeId::of::<Self>() {
58 Some(self)
59 } else {
60 None
61 }
62 }
63
64 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
65 self.widget.handle_routed_message(ui, message);
66
67 if let Some(msg) = message.data::<MenuMessage>() {
68 match msg {
69 MenuMessage::Activate => {
70 if !self.active {
71 ui.push_picking_restriction(RestrictionEntry {
72 handle: self.handle(),
73 stop: false,
74 });
75 self.active = true;
76 }
77 }
78 MenuMessage::Deactivate => {
79 if self.active {
80 self.active = false;
81 ui.remove_picking_restriction(self.handle());
82
83 let mut stack = self.children().to_vec();
85 while let Some(handle) = stack.pop() {
86 let node = ui.node(handle);
87 if let Some(item) = node.cast::<MenuItem>() {
88 ui.send_message(MenuItemMessage::close(
89 handle,
90 MessageDirection::ToWidget,
91 ));
92 stack.push(item.popup);
95 }
96 stack.extend_from_slice(node.children());
98 }
99 }
100 }
101 }
102 }
103 }
104
105 fn handle_os_event(
106 &mut self,
107 _self_handle: Handle<UiNode>,
108 ui: &mut UserInterface,
109 event: &OsEvent,
110 ) {
111 if let OsEvent::MouseInput { state, .. } = event {
116 if *state == ButtonState::Pressed && self.active {
117 let pos = ui.cursor_position();
119 if !self.widget.screen_bounds().contains(pos) {
120 let mut any_picked = false;
123 let mut stack = self.children().to_vec();
124 'depth_search: while let Some(handle) = stack.pop() {
125 let node = ui.node(handle);
126 if let Some(item) = node.cast::<MenuItem>() {
127 if ui.node(item.popup).screen_bounds().contains(pos) {
128 any_picked = true;
132 break 'depth_search;
133 }
134 stack.push(item.popup);
137 }
138 stack.extend_from_slice(node.children());
140 }
141
142 if !any_picked {
143 ui.send_message(MenuMessage::deactivate(
144 self.handle(),
145 MessageDirection::ToWidget,
146 ));
147 }
148 }
149 }
150 }
151 }
152}
153
154#[derive(Copy, Clone, PartialOrd, PartialEq, Hash)]
155enum MenuItemPlacement {
156 Bottom,
157 Right,
158}
159
160#[derive(Clone)]
161pub struct MenuItem {
162 widget: Widget,
163 items: Vec<Handle<UiNode>>,
164 popup: Handle<UiNode>,
165 placement: MenuItemPlacement,
166}
167
168crate::define_widget_deref!(MenuItem);
169
170fn find_menu(from: Handle<UiNode>, ui: &UserInterface) -> Handle<UiNode> {
176 let mut handle = from;
177 while handle.is_some() {
178 if let Some((_, popup)) = ui.try_borrow_by_type_up::<Popup>(handle) {
179 handle = popup
181 .user_data_ref::<Handle<UiNode>>()
182 .cloned()
183 .unwrap_or_default();
184 } else {
185 return ui.find_by_criteria_up(handle, |n| n.cast::<Menu>().is_some());
187 }
188 }
189 Default::default()
190}
191
192fn close_menu_chain(from: Handle<UiNode>, ui: &UserInterface) {
193 let mut handle = from;
194 while handle.is_some() {
195 if let Some((popup_handle, popup)) = ui.try_borrow_by_type_up::<Popup>(handle) {
196 ui.send_message(PopupMessage::close(
197 popup_handle,
198 MessageDirection::ToWidget,
199 ));
200
201 handle = popup
203 .user_data_ref::<Handle<UiNode>>()
204 .cloned()
205 .unwrap_or_default();
206 }
207 }
208}
209
210impl Control for MenuItem {
211 fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
212 if type_id == TypeId::of::<Self>() {
213 Some(self)
214 } else {
215 None
216 }
217 }
218
219 fn on_remove(&self, sender: &Sender<UiMessage>) {
220 sender
223 .send(WidgetMessage::remove(
224 self.popup,
225 MessageDirection::ToWidget,
226 ))
227 .unwrap();
228 }
229
230 fn resolve(&mut self, node_map: &NodeHandleMapping) {
231 node_map.resolve_slice(&mut self.items);
232 node_map.resolve(&mut self.popup);
233 }
234
235 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
236 self.widget.handle_routed_message(ui, message);
237
238 if let Some(msg) = message.data::<WidgetMessage>() {
239 match msg {
240 WidgetMessage::MouseDown { .. } => {
241 let menu = find_menu(self.parent(), ui);
242 if menu.is_some() {
243 ui.send_message(MenuMessage::activate(menu, MessageDirection::ToWidget));
246
247 ui.send_message(MenuItemMessage::open(
248 self.handle(),
249 MessageDirection::ToWidget,
250 ));
251 }
252 }
253 WidgetMessage::MouseUp { .. } => {
254 if !message.handled() {
255 ui.send_message(MenuItemMessage::click(
256 self.handle(),
257 MessageDirection::ToWidget,
258 ));
259 if self.items.is_empty() {
260 let menu = find_menu(self.parent(), ui);
261 if menu.is_some() {
262 ui.send_message(MenuMessage::deactivate(
264 menu,
265 MessageDirection::ToWidget,
266 ));
267 } else {
268 close_menu_chain(self.parent(), ui);
270 }
271 }
272 message.set_handled(true);
273 }
274 }
275 WidgetMessage::MouseEnter => {
276 let menu = find_menu(self.parent(), ui);
279 let open = if menu.is_some() {
280 if let Some(menu) = ui.node(menu).cast::<Menu>() {
281 menu.active
282 } else {
283 false
284 }
285 } else {
286 true
287 };
288 if open {
289 ui.send_message(MenuItemMessage::open(
290 self.handle(),
291 MessageDirection::ToWidget,
292 ));
293 }
294 }
295 _ => {}
296 }
297 } else if let Some(msg) = message.data::<MenuItemMessage>() {
298 match msg {
299 MenuItemMessage::Open => {
300 if !self.items.is_empty() {
301 let placement = match self.placement {
302 MenuItemPlacement::Bottom => Placement::LeftBottom(self.handle),
303 MenuItemPlacement::Right => Placement::RightTop(self.handle),
304 };
305
306 ui.send_message(PopupMessage::placement(
308 self.popup,
309 MessageDirection::ToWidget,
310 placement,
311 ));
312 ui.send_message(PopupMessage::open(self.popup, MessageDirection::ToWidget));
313 }
314 }
315 MenuItemMessage::Close => {
316 ui.send_message(PopupMessage::close(self.popup, MessageDirection::ToWidget));
317 }
318 MenuItemMessage::Click => {}
319 }
320 }
321 }
322
323 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
324 if message.destination() != self.handle() {
327 if let Some(MenuItemMessage::Open) = message.data::<MenuItemMessage>() {
328 let mut found = false;
329 let mut handle = message.destination();
330 while handle.is_some() {
331 if handle == self.handle() {
332 found = true;
333 break;
334 } else {
335 let node = ui.node(handle);
336 if let Some(popup) = node.cast::<Popup>() {
337 handle = popup
340 .user_data_ref::<Handle<UiNode>>()
341 .cloned()
342 .unwrap_or_default();
343 } else {
344 handle = node.parent();
345 }
346 }
347 }
348
349 if !found {
350 ui.send_message(MenuItemMessage::close(
351 self.handle(),
352 MessageDirection::ToWidget,
353 ));
354 }
355 }
356 }
357 }
358}
359
360pub struct MenuBuilder {
361 widget_builder: WidgetBuilder,
362 items: Vec<Handle<UiNode>>,
363}
364
365impl MenuBuilder {
366 pub fn new(widget_builder: WidgetBuilder) -> Self {
367 Self {
368 widget_builder,
369 items: Default::default(),
370 }
371 }
372
373 pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
374 self.items = items;
375 self
376 }
377
378 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
379 for &item in self.items.iter() {
380 if let Some(item) = ctx[item].cast_mut::<MenuItem>() {
381 item.placement = MenuItemPlacement::Bottom;
382 }
383 }
384
385 let back = BorderBuilder::new(
386 WidgetBuilder::new()
387 .with_background(BRUSH_PRIMARY)
388 .with_child(
389 StackPanelBuilder::new(
390 WidgetBuilder::new().with_children(self.items.iter().cloned()),
391 )
392 .with_orientation(Orientation::Horizontal)
393 .build(ctx),
394 ),
395 )
396 .build(ctx);
397
398 let menu = Menu {
399 widget: self
400 .widget_builder
401 .with_handle_os_events(true)
402 .with_child(back)
403 .build(),
404 active: false,
405 };
406
407 ctx.add_node(UiNode::new(menu))
408 }
409}
410
411pub enum MenuItemContent<'a, 'b> {
412 Text {
419 text: &'a str,
420 shortcut: &'b str,
421 icon: Handle<UiNode>,
422 },
423 Node(Handle<UiNode>),
426}
427
428impl<'a, 'b> MenuItemContent<'a, 'b> {
429 pub fn text_with_shortcut(text: &'a str, shortcut: &'b str) -> Self {
430 MenuItemContent::Text {
431 text,
432 shortcut,
433 icon: Default::default(),
434 }
435 }
436
437 pub fn text(text: &'a str) -> Self {
438 MenuItemContent::Text {
439 text,
440 shortcut: "",
441 icon: Default::default(),
442 }
443 }
444}
445
446pub struct MenuItemBuilder<'a, 'b> {
447 widget_builder: WidgetBuilder,
448 items: Vec<Handle<UiNode>>,
449 content: Option<MenuItemContent<'a, 'b>>,
450 back: Option<Handle<UiNode>>,
451}
452
453impl<'a, 'b> MenuItemBuilder<'a, 'b> {
454 pub fn new(widget_builder: WidgetBuilder) -> Self {
455 Self {
456 widget_builder,
457 items: Default::default(),
458 content: None,
459 back: None,
460 }
461 }
462
463 pub fn with_content(mut self, content: MenuItemContent<'a, 'b>) -> Self {
464 self.content = Some(content);
465 self
466 }
467
468 pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
469 self.items = items;
470 self
471 }
472
473 pub fn with_back(mut self, handle: Handle<UiNode>) -> Self {
476 self.back = Some(handle);
477 self
478 }
479
480 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
481 let content = match self.content {
482 None => Handle::NONE,
483 Some(MenuItemContent::Text {
484 text,
485 shortcut,
486 icon,
487 }) => GridBuilder::new(
488 WidgetBuilder::new()
489 .with_child(icon)
490 .with_child(
491 TextBuilder::new(
492 WidgetBuilder::new()
493 .with_vertical_alignment(VerticalAlignment::Center)
494 .with_margin(Thickness::uniform(1.0))
495 .on_column(1),
496 )
497 .with_text(text)
498 .build(ctx),
499 )
500 .with_child(
501 TextBuilder::new(
502 WidgetBuilder::new()
503 .with_vertical_alignment(VerticalAlignment::Center)
504 .with_horizontal_alignment(HorizontalAlignment::Right)
505 .with_margin(Thickness::uniform(1.0))
506 .on_column(2),
507 )
508 .with_text(shortcut)
509 .build(ctx),
510 ),
511 )
512 .add_row(Row::auto())
513 .add_column(Column::auto())
514 .add_column(Column::stretch())
515 .add_column(Column::auto())
516 .build(ctx),
517 Some(MenuItemContent::Node(node)) => node,
518 };
519
520 let back = self.back.unwrap_or_else(|| {
521 DecoratorBuilder::new(
522 BorderBuilder::new(WidgetBuilder::new())
523 .with_stroke_thickness(Thickness::uniform(0.0)),
524 )
525 .with_hover_brush(BRUSH_BRIGHT_BLUE)
526 .with_normal_brush(BRUSH_PRIMARY)
527 .with_pressed_brush(Brush::Solid(Color::TRANSPARENT))
528 .with_pressable(false)
529 .build(ctx)
530 });
531
532 ctx.link(content, back);
533
534 let popup = PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0)))
535 .with_content(
536 StackPanelBuilder::new(
537 WidgetBuilder::new().with_children(self.items.iter().cloned()),
538 )
539 .build(ctx),
540 )
541 .stays_open(true)
543 .build(ctx);
544
545 let menu = MenuItem {
546 widget: self
547 .widget_builder
548 .with_preview_messages(true)
549 .with_child(back)
550 .build(),
551 popup,
552 items: self.items,
553 placement: MenuItemPlacement::Right,
554 };
555
556 let handle = ctx.add_node(UiNode::new(menu));
557
558 if let Some(popup) = ctx[popup].cast_mut::<Popup>() {
560 popup.user_data = Some(Rc::new(handle));
561 }
562
563 handle
564 }
565}