1use std::sync::Arc;
7
8use crate::draw_ctx::DrawCtx;
9use crate::event::{Event, EventResult, Key, Modifiers, MouseButton};
10use crate::font_settings;
11use crate::geometry::{Point, Rect, Size};
12use crate::text::Font;
13use crate::widget::{current_viewport, BackbufferCache, Widget};
14
15use super::geometry::{contains, item_at_path, BAR_H};
16use super::model::{MenuEntry, MenuSelection};
17use super::paint::{
18 bar_button_text_color, paint_check_mark, paint_item_row_bg, paint_menu_bar_button_bg,
19 paint_panel, paint_separator, paint_submenu_chevron, MenuStyle,
20};
21use super::state::{MenuAnchorKind, MenuResponse, PopupMenuState};
22
23use labels::{BarLabels, PopupLabels};
24
25mod labels;
26
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum MenuOrientation {
30 Horizontal,
31 HorizontalBottom,
32 Vertical,
33}
34
35pub const VERTICAL_ROW_H: f64 = 36.0;
36
37const TOUCH_SYNTH_WINDOW_MS: u128 = 50;
42
43fn is_touch_synthesized() -> bool {
44 crate::touch_state::last_touch_event_age()
45 .map(|d| d.as_millis() < TOUCH_SYNTH_WINDOW_MS)
46 .unwrap_or(false)
47}
48
49#[derive(Clone)]
50pub struct PopupMenu {
51 pub items: Vec<MenuEntry>,
52 pub state: PopupMenuState,
53 pub style: MenuStyle,
54 labels: PopupLabels,
56}
57
58impl PopupMenu {
59 pub fn new(items: Vec<MenuEntry>) -> Self {
60 Self {
61 items,
62 state: PopupMenuState::default(),
63 style: MenuStyle::default(),
64 labels: PopupLabels::new(),
65 }
66 }
67
68 pub fn open_at(&mut self, pos: Point) {
69 self.state.open_at(pos, MenuAnchorKind::Context);
70 }
71
72 pub fn close(&mut self) {
73 self.state.close();
74 }
75
76 pub fn is_open(&self) -> bool {
77 self.state.open
78 }
79
80 pub fn take_suppress_mouse_up(&mut self) -> bool {
81 self.state.take_suppress_mouse_up()
82 }
83
84 pub fn handle_event(&mut self, event: &Event, viewport: Size) -> (EventResult, MenuResponse) {
85 self.state.handle_event(&mut self.items, event, viewport)
86 }
87
88 pub fn body_contains(&self, pos: Point, viewport: Size) -> bool {
94 self.state
95 .layouts(&self.items, viewport)
96 .iter()
97 .any(|layout| {
98 pos.x >= layout.rect.x
99 && pos.x <= layout.rect.x + layout.rect.width
100 && pos.y >= layout.rect.y
101 && pos.y <= layout.rect.y + layout.rect.height
102 })
103 }
104
105 pub fn handle_shortcut(&mut self, key: &Key, modifiers: Modifiers) -> MenuResponse {
106 self.state.handle_shortcut(&mut self.items, key, modifiers)
107 }
108
109 pub fn paint(
110 &mut self,
111 ctx: &mut dyn DrawCtx,
112 font: Arc<Font>,
113 font_size: f64,
114 viewport: Size,
115 ) {
116 let layouts = self.state.layouts(&self.items, viewport);
117 self.labels.sync_to(&font, font_size, &self.items, &layouts);
121
122 ctx.set_font(Arc::clone(&font));
128 ctx.set_font_size(font_size);
129
130 for (level_idx, layout) in layouts.iter().enumerate() {
131 paint_panel(ctx, layout.rect, &self.style);
132 paint_popup_level(
133 ctx,
134 level_idx,
135 layout,
136 &self.items,
137 &self.state,
138 &self.style,
139 &mut self.labels,
140 );
141 }
142 }
143}
144
145pub struct MenuBar {
146 bounds: Rect,
147 children: Vec<Box<dyn Widget>>,
148 font: Arc<Font>,
149 font_size: f64,
150 menus: Vec<TopMenu>,
151 open_index: Option<usize>,
152 hover_index: Option<usize>,
153 popup: PopupMenu,
154 on_action: Box<dyn FnMut(&str)>,
155 suppress_hover_for: Option<usize>,
157 fit_width: bool,
163 orientation: MenuOrientation,
164 cache: BackbufferCache,
166 bar_labels: BarLabels,
168}
169
170pub struct TopMenu {
171 pub label: String,
172 pub items: Vec<MenuEntry>,
173 rect: Rect,
174}
175
176impl TopMenu {
177 pub fn new(label: impl Into<String>, items: Vec<MenuEntry>) -> Self {
178 Self {
179 label: label.into(),
180 items,
181 rect: Rect::default(),
182 }
183 }
184}
185
186impl MenuBar {
187 pub fn new(
188 font: Arc<Font>,
189 menus: Vec<TopMenu>,
190 on_action: impl FnMut(&str) + 'static,
191 ) -> Self {
192 Self {
193 bounds: Rect::default(),
194 children: Vec::new(),
195 font,
196 font_size: 14.0,
197 menus,
198 open_index: None,
199 hover_index: None,
200 popup: PopupMenu::new(Vec::new()),
201 on_action: Box::new(on_action),
202 suppress_hover_for: None,
203 fit_width: false,
204 orientation: MenuOrientation::Horizontal,
205 cache: BackbufferCache::new(),
206 bar_labels: BarLabels::new(),
207 }
208 }
209
210 fn sync_bar_labels(&mut self) {
212 let labels: Vec<&str> = self.menus.iter().map(|m| m.label.as_str()).collect();
213 self.bar_labels
214 .sync_to(&self.active_font(), self.font_size, &labels);
215 }
216
217 pub fn with_orientation(mut self, orientation: MenuOrientation) -> Self {
222 self.orientation = orientation;
223 self
224 }
225
226 pub fn with_fit_width(mut self, fit: bool) -> Self {
232 self.fit_width = fit;
233 self
234 }
235
236 pub fn with_font_size(mut self, font_size: f64) -> Self {
237 self.font_size = font_size;
238 self
239 }
240
241 pub fn with_menu_style(mut self, style: MenuStyle) -> Self {
247 self.popup.style = style;
248 self.cache.invalidate();
249 self
250 }
251
252 pub fn set_menus(&mut self, menus: Vec<TopMenu>) {
258 self.menus = menus;
259 self.cache.invalidate();
260 }
261
262 pub fn menus(&self) -> &[TopMenu] {
266 &self.menus
267 }
268
269 fn active_font(&self) -> Arc<Font> {
274 font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font))
275 }
276
277 fn menu_at(&self, pos: Point) -> Option<usize> {
278 self.menus.iter().position(|menu| contains(menu.rect, pos))
279 }
280
281 fn open_menu(&mut self, idx: usize) {
282 let rect = self.menus[idx].rect;
283 self.popup.items = self.menus[idx].items.clone();
284 let (anchor, kind) = match self.orientation {
293 MenuOrientation::Horizontal => (Point::new(rect.x, rect.y), MenuAnchorKind::Bar),
294 MenuOrientation::HorizontalBottom => (
297 Point::new(rect.x, rect.y + rect.height),
298 MenuAnchorKind::BottomBar,
299 ),
300 MenuOrientation::Vertical => (
301 Point::new(rect.x + rect.width, rect.y + rect.height),
302 MenuAnchorKind::Context,
303 ),
304 };
305 self.popup.state.open_at(anchor, kind);
306 self.open_index = Some(idx);
307 self.hover_index = Some(idx);
308 self.cache.invalidate();
309 crate::animation::request_draw();
310 }
311
312 fn open_menu_for_drag_release(&mut self, idx: usize) {
313 self.open_menu(idx);
314 self.popup.state.arm_mouse_up_activation();
315 }
316
317 fn switch_open_menu(&mut self, delta: isize) -> EventResult {
318 let Some(current) = self.open_index else {
319 return EventResult::Ignored;
320 };
321 if self.menus.is_empty() {
322 return EventResult::Ignored;
323 }
324 let len = self.menus.len() as isize;
325 let next = (current as isize + delta).rem_euclid(len) as usize;
326 self.open_menu(next);
327 EventResult::Consumed
328 }
329
330 fn should_switch_top_menu(&self, key: &Key) -> bool {
331 match key {
332 Key::ArrowLeft => self.popup.state.open_path.is_empty(),
333 Key::ArrowRight => {
334 if !self.popup.state.open_path.is_empty() {
335 return false;
336 }
337 self.popup
338 .state
339 .hover_path
340 .as_deref()
341 .and_then(|path| item_at_path(&self.popup.items, path))
342 .map_or(true, |item| !item.has_submenu())
343 }
344 _ => false,
345 }
346 }
347
348 fn set_hover_index(&mut self, hover: Option<usize>) {
349 let hover = if is_touch_synthesized() { None } else { hover };
355 if self.hover_index != hover {
356 self.hover_index = hover;
357 crate::animation::request_draw();
364 self.cache.invalidate();
367 }
368 if self.suppress_hover_for != hover {
372 self.suppress_hover_for = None;
373 self.cache.invalidate();
374 }
375 }
376}
377
378impl Widget for MenuBar {
379 fn type_name(&self) -> &'static str {
380 "MenuBar"
381 }
382
383 fn bounds(&self) -> Rect {
384 self.bounds
385 }
386
387 fn set_bounds(&mut self, bounds: Rect) {
388 if (bounds.width - self.bounds.width).abs() > 0.5
389 || (bounds.height - self.bounds.height).abs() > 0.5
390 {
391 self.cache.invalidate();
392 }
393 self.bounds = bounds;
394 }
395
396 fn children(&self) -> &[Box<dyn Widget>] {
397 &self.children
398 }
399
400 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
401 &mut self.children
402 }
403
404 fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
405 Some(&mut self.cache)
406 }
407
408 fn backbuffer_mode(&self) -> crate::widget::BackbufferMode {
409 if crate::font_settings::lcd_enabled() {
417 crate::widget::BackbufferMode::LcdCoverage
418 } else {
419 crate::widget::BackbufferMode::Rgba
420 }
421 }
422
423 fn layout(&mut self, available: Size) -> Size {
424 self.sync_bar_labels();
427 match self.orientation {
428 MenuOrientation::Horizontal | MenuOrientation::HorizontalBottom => {
429 let mut x = 0.0;
430 for menu in &mut self.menus {
431 let width = (menu.label.chars().count() as f64 * 8.0 + 22.0).max(52.0);
432 menu.rect = Rect::new(x, 0.0, width, BAR_H);
433 x += width;
434 }
435 let report_w = if self.fit_width { x } else { available.width };
437 Size::new(report_w, BAR_H)
438 }
439 MenuOrientation::Vertical => {
440 let mut y = available.height;
442 for menu in &mut self.menus {
443 y -= VERTICAL_ROW_H;
444 menu.rect = Rect::new(0.0, y, available.width, VERTICAL_ROW_H);
445 }
446 let used_h = self.menus.len() as f64 * VERTICAL_ROW_H;
447 Size::new(available.width, used_h)
448 }
449 }
450 }
451
452 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
453 self.sync_bar_labels();
455 ctx.set_font(self.active_font());
456 ctx.set_font_size(self.font_size);
457 let v = ctx.visuals();
458 ctx.set_fill_color(v.top_bar_bg);
459 ctx.begin_path();
460 let bg_h = match self.orientation {
461 MenuOrientation::Horizontal | MenuOrientation::HorizontalBottom => BAR_H,
462 MenuOrientation::Vertical => self.bounds.height,
463 };
464 ctx.rect(0.0, 0.0, self.bounds.width, bg_h);
465 ctx.fill();
466 for (idx, menu) in self.menus.iter().enumerate() {
468 let hovered = self.hover_index == Some(idx) && self.suppress_hover_for != Some(idx);
474 let open = self.open_index == Some(idx);
475 paint_menu_bar_button_bg(ctx, menu.rect, open, hovered);
476 }
477 let menu_rects: Vec<(Rect, bool)> = self
482 .menus
483 .iter()
484 .enumerate()
485 .map(|(idx, menu)| (menu.rect, self.open_index == Some(idx)))
486 .collect();
487 for (idx, (rect, open)) in menu_rects.into_iter().enumerate() {
488 let color = bar_button_text_color(ctx, open);
489 self.bar_labels.paint_in(ctx, idx, rect, color);
490 }
491 }
492
493 fn hit_test_global_overlay(&self, _local_pos: Point) -> bool {
494 self.popup.is_open()
495 }
496
497 fn has_active_modal(&self) -> bool {
498 self.popup.is_open()
499 }
500
501 fn on_event(&mut self, event: &Event) -> EventResult {
502 if let Event::MouseMove { pos } = event {
503 let hovered = self.menu_at(*pos);
504 self.set_hover_index(hovered);
505 let from_touch = is_touch_synthesized();
516 if self.popup.is_open() && !from_touch {
517 if let Some(idx) = hovered {
518 if self.open_index != Some(idx) {
519 let activate_on_release = self.popup.state.is_mouse_up_activation_armed();
520 self.open_menu(idx);
521 if activate_on_release {
522 self.popup.state.arm_mouse_up_activation();
523 }
524 }
525 return EventResult::Consumed;
526 }
527 }
528 }
529 if self.popup.is_open() {
530 if let Event::KeyDown { key, .. } = event {
531 if self.should_switch_top_menu(key) {
532 return match key {
533 Key::ArrowLeft => self.switch_open_menu(-1),
534 Key::ArrowRight => self.switch_open_menu(1),
535 _ => EventResult::Ignored,
536 };
537 }
538 }
539 if let Event::MouseDown {
547 pos,
548 button: MouseButton::Left,
549 ..
550 } = event
551 {
552 if let Some(idx) = self.menu_at(*pos) {
553 if self.open_index != Some(idx) {
554 self.open_menu(idx);
555 return EventResult::Consumed;
556 }
557 }
558 }
559 if let Event::MouseUp {
568 pos,
569 button: MouseButton::Left,
570 ..
571 } = event
572 {
573 if self.popup.state.is_mouse_up_activation_armed()
574 && self.menu_at(*pos).is_none()
575 && !self.popup.body_contains(*pos, current_viewport())
576 {
577 self.popup.close();
578 self.open_index = None;
579 self.cache.invalidate();
580 crate::animation::request_draw();
581 return EventResult::Consumed;
582 }
583 }
584 let (result, response) = self.popup.handle_event(event, current_viewport());
585 if let MenuResponse::Action(action) = response {
586 if let Some(idx) = self.open_index {
587 self.menus[idx].items = self.popup.items.clone();
588 }
589 (self.on_action)(&action);
590 if !self.popup.is_open() {
591 self.open_index = None;
592 self.cache.invalidate();
593 }
594 } else if matches!(response, MenuResponse::Closed) {
595 self.open_index = None;
596 self.suppress_hover_for = self.hover_index;
602 self.cache.invalidate();
603 }
604 if result == EventResult::Consumed {
605 return result;
606 }
607 }
608 match event {
609 Event::MouseDown {
610 pos,
611 button: MouseButton::Left,
612 ..
613 } => {
614 if let Some(idx) = self.menu_at(*pos) {
615 self.open_menu_for_drag_release(idx);
616 EventResult::Consumed
617 } else {
618 EventResult::Ignored
619 }
620 }
621 Event::MouseMove { .. } => EventResult::Ignored,
622 _ => EventResult::Ignored,
623 }
624 }
625
626 fn on_unconsumed_key(&mut self, key: &Key, modifiers: Modifiers) -> EventResult {
627 let response = if self.popup.is_open() {
628 self.popup.handle_shortcut(key, modifiers)
629 } else {
630 self.menus
631 .iter_mut()
632 .find_map(|menu| {
633 let mut popup = PopupMenu::new(menu.items.clone());
634 match popup.handle_shortcut(key, modifiers) {
635 MenuResponse::Action(action) => {
636 menu.items = popup.items;
637 Some(action)
638 }
639 MenuResponse::None | MenuResponse::Closed => None,
640 }
641 })
642 .map(MenuResponse::Action)
643 .unwrap_or(MenuResponse::None)
644 };
645 if let MenuResponse::Action(action) = response {
646 if let Some(idx) = self.open_index {
647 self.menus[idx].items = self.popup.items.clone();
648 }
649 (self.on_action)(&action);
650 if !self.popup.is_open() {
651 self.open_index = None;
652 self.cache.invalidate();
653 }
654 EventResult::Consumed
655 } else {
656 EventResult::Ignored
657 }
658 }
659
660 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
661 let font = self.active_font();
662 self.popup
663 .paint(ctx, font, self.font_size, current_viewport());
664 }
665}
666
667fn paint_popup_level(
673 ctx: &mut dyn DrawCtx,
674 level_idx: usize,
675 layout: &super::geometry::PopupLayout,
676 items: &[MenuEntry],
677 state: &PopupMenuState,
678 style: &MenuStyle,
679 labels: &mut PopupLabels,
680) {
681 let level_items = items_for_layout(items, &layout.path_prefix);
682 for (row_idx, row_layout) in layout.rows.iter().enumerate() {
683 let Some(item_idx) = row_layout.item_index else {
684 paint_separator(ctx, row_layout.rect);
686 continue;
687 };
688 let Some(MenuEntry::Item(item)) = level_items.get(item_idx) else {
689 continue;
690 };
691 let mut path = layout.path_prefix.clone();
692 path.push(item_idx);
693 let hovered = state.hover_path.as_ref() == Some(&path);
694 let open = state.open_path.starts_with(&path);
695
696 paint_item_row_bg(ctx, row_layout.rect, hovered, open, item.enabled);
698
699 let inline_color =
703 super::paint::popup_row_text_color(ctx, item.enabled, open && item.enabled);
704 ctx.set_fill_color(inline_color);
705 if let Some(color) = item.swatch {
706 let size = 12.0;
712 let sx = row_layout.rect.x + style.icon_x - size * 0.5 + 4.0;
713 let sy = row_layout.rect.y + (row_layout.rect.height - size) * 0.5;
714 let fill = if item.enabled {
715 color
716 } else {
717 color.with_alpha(0.45)
720 };
721 ctx.set_fill_color(fill);
722 ctx.begin_path();
723 ctx.rounded_rect(sx, sy, size, size, 3.0);
724 ctx.fill();
725 if matches!(item.selection, MenuSelection::Radio { selected: true }) {
726 ctx.set_stroke_color(inline_color);
727 ctx.set_line_width(1.5);
728 ctx.begin_path();
729 ctx.rounded_rect(sx - 2.0, sy - 2.0, size + 4.0, size + 4.0, 4.0);
730 ctx.stroke();
731 }
732 } else if let Some(icon) = item.icon {
733 let icon = icon.to_string();
734 ctx.fill_text(
735 &icon,
736 row_layout.rect.x + style.icon_x,
737 row_layout.rect.y + 7.0,
738 );
739 }
740 let selected = matches!(
748 item.selection,
749 MenuSelection::Check { selected: true } | MenuSelection::Radio { selected: true }
750 );
751 if selected {
752 let has_left_marker = item.swatch.is_some() || item.icon.is_some();
753 let cx = if has_left_marker {
754 let right_offset = if item.has_submenu() { 30.0 } else { 12.0 };
755 row_layout.rect.x + row_layout.rect.width - right_offset
756 } else {
757 row_layout.rect.x + style.icon_x
758 };
759 let cy = row_layout.rect.y + row_layout.rect.height * 0.5;
760 paint_check_mark(ctx, cx, cy, inline_color);
761 }
762 if item.has_submenu() {
763 paint_submenu_chevron(ctx, row_layout.rect, inline_color);
764 }
765
766 labels.paint_row_with_state(
769 ctx,
770 level_idx,
771 row_idx,
772 row_layout.rect,
773 style.label_x,
774 style.shortcut_right,
775 item.enabled,
776 open && item.enabled,
777 );
778 }
779}
780
781fn items_for_layout<'a>(items: &'a [MenuEntry], path: &[usize]) -> &'a [MenuEntry] {
782 let mut current = items;
783 for &idx in path {
784 let Some(MenuEntry::Item(item)) = current.get(idx) else {
785 return current;
786 };
787 current = &item.submenu;
788 }
789 current
790}
791
792#[cfg(test)]
793mod tests_1;
794#[cfg(test)]
795mod tests_2;