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, effective_metrics, item_at_path, DEFAULT_FONT_SIZE};
16use super::model::MenuEntry;
17use super::paint::{bar_button_text_color, paint_menu_bar_button_bg, paint_panel, MenuStyle};
18use super::state::{MenuAnchorKind, MenuResponse, PopupMenuState};
19
20use labels::{BarLabels, PopupLabels};
21
22mod labels;
23mod popup_paint;
24
25#[derive(Clone, Copy, Debug, PartialEq, Eq)]
27pub enum MenuOrientation {
28 Horizontal,
29 HorizontalBottom,
30 Vertical,
31}
32
33const TOUCH_SYNTH_WINDOW_MS: u128 = 50;
38
39fn is_touch_synthesized() -> bool {
40 crate::touch_state::last_touch_event_age()
41 .map(|d| d.as_millis() < TOUCH_SYNTH_WINDOW_MS)
42 .unwrap_or(false)
43}
44
45#[derive(Clone)]
46pub struct PopupMenu {
47 pub items: Vec<MenuEntry>,
48 pub state: PopupMenuState,
49 pub style: MenuStyle,
50 labels: PopupLabels,
52}
53
54impl PopupMenu {
55 pub fn new(items: Vec<MenuEntry>) -> Self {
56 Self {
57 items,
58 state: PopupMenuState::default(),
59 style: MenuStyle::default(),
60 labels: PopupLabels::new(),
61 }
62 }
63
64 pub fn open_at(&mut self, pos: Point) {
65 self.state.open_at(pos, MenuAnchorKind::Context);
66 }
67
68 pub fn close(&mut self) {
69 self.state.close();
70 }
71
72 pub fn is_open(&self) -> bool {
73 self.state.open
74 }
75
76 pub fn take_suppress_mouse_up(&mut self) -> bool {
77 self.state.take_suppress_mouse_up()
78 }
79
80 pub fn handle_event(&mut self, event: &Event, viewport: Size) -> (EventResult, MenuResponse) {
81 self.state.handle_event(&mut self.items, event, viewport)
82 }
83
84 pub fn body_contains(&self, pos: Point, viewport: Size) -> bool {
90 self.state
91 .layouts(&self.items, viewport)
92 .iter()
93 .any(|layout| {
94 pos.x >= layout.rect.x
95 && pos.x <= layout.rect.x + layout.rect.width
96 && pos.y >= layout.rect.y
97 && pos.y <= layout.rect.y + layout.rect.height
98 })
99 }
100
101 pub fn handle_shortcut(&mut self, key: &Key, modifiers: Modifiers) -> MenuResponse {
102 self.state.handle_shortcut(&mut self.items, key, modifiers)
103 }
104
105 pub fn paint(
106 &mut self,
107 ctx: &mut dyn DrawCtx,
108 font: Arc<Font>,
109 font_size: f64,
110 viewport: Size,
111 ) {
112 let layouts = self.state.layouts(&self.items, viewport);
113 self.labels.sync_to(&font, font_size, &self.items, &layouts);
117
118 ctx.set_font(Arc::clone(&font));
124 ctx.set_font_size(font_size);
125
126 for (level_idx, layout) in layouts.iter().enumerate() {
127 paint_panel(ctx, layout.rect, &self.style);
128 popup_paint::paint_popup_level(
129 ctx,
130 level_idx,
131 layout,
132 &self.items,
133 &self.state,
134 &self.style,
135 &mut self.labels,
136 );
137 }
138 }
139}
140
141pub struct MenuBar {
142 bounds: Rect,
143 children: Vec<Box<dyn Widget>>,
144 font: Arc<Font>,
145 font_size: Option<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: None,
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 let font_size = self.effective_font_size();
214 self.bar_labels
215 .sync_to(&self.active_font(), font_size, &labels);
216 }
217
218 pub fn with_orientation(mut self, orientation: MenuOrientation) -> Self {
223 self.orientation = orientation;
224 self
225 }
226
227 pub fn with_fit_width(mut self, fit: bool) -> Self {
233 self.fit_width = fit;
234 self
235 }
236
237 pub fn with_font_size(mut self, font_size: f64) -> Self {
238 self.font_size = Some(font_size);
239 self
240 }
241
242 fn effective_font_size(&self) -> f64 {
247 self.font_size
248 .unwrap_or_else(|| effective_metrics().default_font_size)
249 }
250
251 pub fn with_menu_style(mut self, style: MenuStyle) -> Self {
257 self.popup.style = style;
258 self.cache.invalidate();
259 self
260 }
261
262 pub fn set_menus(&mut self, menus: Vec<TopMenu>) {
268 self.menus = menus;
269 self.cache.invalidate();
270 }
271
272 pub fn menus(&self) -> &[TopMenu] {
276 &self.menus
277 }
278
279 fn active_font(&self) -> Arc<Font> {
284 font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font))
285 }
286
287 fn menu_at(&self, pos: Point) -> Option<usize> {
288 self.menus.iter().position(|menu| contains(menu.rect, pos))
289 }
290
291 fn open_menu(&mut self, idx: usize) {
292 let rect = self.menus[idx].rect;
293 self.popup.items = self.menus[idx].items.clone();
294 let (anchor, kind) = match self.orientation {
303 MenuOrientation::Horizontal => (Point::new(rect.x, rect.y), MenuAnchorKind::Bar),
304 MenuOrientation::HorizontalBottom => (
307 Point::new(rect.x, rect.y + rect.height),
308 MenuAnchorKind::BottomBar,
309 ),
310 MenuOrientation::Vertical => (
311 Point::new(rect.x + rect.width, rect.y + rect.height),
312 MenuAnchorKind::Context,
313 ),
314 };
315 self.popup.state.open_at(anchor, kind);
316 self.open_index = Some(idx);
317 self.hover_index = Some(idx);
318 self.cache.invalidate();
319 crate::animation::request_draw();
320 }
321
322 fn open_menu_for_drag_release(&mut self, idx: usize) {
323 self.open_menu(idx);
324 self.popup.state.arm_mouse_up_activation();
325 }
326
327 fn switch_open_menu(&mut self, delta: isize) -> EventResult {
328 let Some(current) = self.open_index else {
329 return EventResult::Ignored;
330 };
331 if self.menus.is_empty() {
332 return EventResult::Ignored;
333 }
334 let len = self.menus.len() as isize;
335 let next = (current as isize + delta).rem_euclid(len) as usize;
336 self.open_menu(next);
337 EventResult::Consumed
338 }
339
340 fn should_switch_top_menu(&self, key: &Key) -> bool {
341 match key {
342 Key::ArrowLeft => self.popup.state.open_path.is_empty(),
343 Key::ArrowRight => {
344 if !self.popup.state.open_path.is_empty() {
345 return false;
346 }
347 self.popup
348 .state
349 .hover_path
350 .as_deref()
351 .and_then(|path| item_at_path(&self.popup.items, path))
352 .map_or(true, |item| !item.has_submenu())
353 }
354 _ => false,
355 }
356 }
357
358 fn set_hover_index(&mut self, hover: Option<usize>) {
359 let hover = if is_touch_synthesized() { None } else { hover };
365 if self.hover_index != hover {
366 self.hover_index = hover;
367 crate::animation::request_draw();
374 self.cache.invalidate();
377 }
378 if self.suppress_hover_for != hover {
382 self.suppress_hover_for = None;
383 self.cache.invalidate();
384 }
385 }
386}
387
388impl Widget for MenuBar {
389 fn type_name(&self) -> &'static str {
390 "MenuBar"
391 }
392
393 fn bounds(&self) -> Rect {
394 self.bounds
395 }
396
397 fn set_bounds(&mut self, bounds: Rect) {
398 if (bounds.width - self.bounds.width).abs() > 0.5
399 || (bounds.height - self.bounds.height).abs() > 0.5
400 {
401 self.cache.invalidate();
402 }
403 self.bounds = bounds;
404 }
405
406 fn children(&self) -> &[Box<dyn Widget>] {
407 &self.children
408 }
409
410 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
411 &mut self.children
412 }
413
414 fn backbuffer_cache_mut(&mut self) -> Option<&mut BackbufferCache> {
415 Some(&mut self.cache)
416 }
417
418 fn backbuffer_mode(&self) -> crate::widget::BackbufferMode {
419 if crate::font_settings::lcd_enabled() {
427 crate::widget::BackbufferMode::LcdCoverage
428 } else {
429 crate::widget::BackbufferMode::Rgba
430 }
431 }
432
433 fn layout(&mut self, available: Size) -> Size {
434 self.sync_bar_labels();
437 let m = effective_metrics();
438 match self.orientation {
439 MenuOrientation::Horizontal | MenuOrientation::HorizontalBottom => {
440 let wscale = self.effective_font_size() / DEFAULT_FONT_SIZE;
444 let mut x = 0.0;
445 for menu in &mut self.menus {
446 let width = (menu.label.chars().count() as f64 * 8.0 + 22.0).max(52.0) * wscale;
447 menu.rect = Rect::new(x, 0.0, width, m.bar_h);
448 x += width;
449 }
450 let report_w = if self.fit_width { x } else { available.width };
452 Size::new(report_w, m.bar_h)
453 }
454 MenuOrientation::Vertical => {
455 let mut y = available.height;
457 for menu in &mut self.menus {
458 y -= m.vertical_row_h;
459 menu.rect = Rect::new(0.0, y, available.width, m.vertical_row_h);
460 }
461 let used_h = self.menus.len() as f64 * m.vertical_row_h;
462 Size::new(available.width, used_h)
463 }
464 }
465 }
466
467 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
468 self.sync_bar_labels();
470 ctx.set_font(self.active_font());
471 ctx.set_font_size(self.effective_font_size());
472 let v = ctx.visuals();
473 ctx.set_fill_color(v.top_bar_bg);
474 ctx.begin_path();
475 let bg_h = match self.orientation {
476 MenuOrientation::Horizontal | MenuOrientation::HorizontalBottom => {
477 effective_metrics().bar_h
478 }
479 MenuOrientation::Vertical => self.bounds.height,
480 };
481 ctx.rect(0.0, 0.0, self.bounds.width, bg_h);
482 ctx.fill();
483 for (idx, menu) in self.menus.iter().enumerate() {
485 let hovered = self.hover_index == Some(idx) && self.suppress_hover_for != Some(idx);
491 let open = self.open_index == Some(idx);
492 paint_menu_bar_button_bg(ctx, menu.rect, open, hovered);
493 }
494 let menu_rects: Vec<(Rect, bool)> = self
499 .menus
500 .iter()
501 .enumerate()
502 .map(|(idx, menu)| (menu.rect, self.open_index == Some(idx)))
503 .collect();
504 for (idx, (rect, open)) in menu_rects.into_iter().enumerate() {
505 let color = bar_button_text_color(ctx, open);
506 self.bar_labels.paint_in(ctx, idx, rect, color);
507 }
508 }
509
510 fn hit_test_global_overlay(&self, _local_pos: Point) -> bool {
511 self.popup.is_open()
512 }
513
514 fn has_active_modal(&self) -> bool {
515 self.popup.is_open()
516 }
517
518 fn on_event(&mut self, event: &Event) -> EventResult {
519 if let Event::MouseMove { pos } = event {
520 let hovered = self.menu_at(*pos);
521 self.set_hover_index(hovered);
522 let from_touch = is_touch_synthesized();
533 if self.popup.is_open() && !from_touch {
534 if let Some(idx) = hovered {
535 if self.open_index != Some(idx) {
536 let activate_on_release = self.popup.state.is_mouse_up_activation_armed();
537 self.open_menu(idx);
538 if activate_on_release {
539 self.popup.state.arm_mouse_up_activation();
540 }
541 }
542 return EventResult::Consumed;
543 }
544 }
545 }
546 if self.popup.is_open() {
547 if let Event::KeyDown { key, .. } = event {
548 if self.should_switch_top_menu(key) {
549 return match key {
550 Key::ArrowLeft => self.switch_open_menu(-1),
551 Key::ArrowRight => self.switch_open_menu(1),
552 _ => EventResult::Ignored,
553 };
554 }
555 }
556 if let Event::MouseDown {
564 pos,
565 button: MouseButton::Left,
566 ..
567 } = event
568 {
569 if let Some(idx) = self.menu_at(*pos) {
570 if self.open_index != Some(idx) {
571 self.open_menu(idx);
572 return EventResult::Consumed;
573 }
574 }
575 }
576 if let Event::MouseUp {
585 pos,
586 button: MouseButton::Left,
587 ..
588 } = event
589 {
590 if self.popup.state.is_mouse_up_activation_armed()
591 && self.menu_at(*pos).is_none()
592 && !self.popup.body_contains(*pos, current_viewport())
593 {
594 self.popup.close();
595 self.open_index = None;
596 self.cache.invalidate();
597 crate::animation::request_draw();
598 return EventResult::Consumed;
599 }
600 }
601 let (result, response) = self.popup.handle_event(event, current_viewport());
602 if let MenuResponse::Action(action) = response {
603 if let Some(idx) = self.open_index {
604 self.menus[idx].items = self.popup.items.clone();
605 }
606 (self.on_action)(&action);
607 if !self.popup.is_open() {
608 self.open_index = None;
609 self.cache.invalidate();
610 }
611 } else if matches!(response, MenuResponse::Closed) {
612 self.open_index = None;
613 self.suppress_hover_for = self.hover_index;
619 self.cache.invalidate();
620 }
621 if result.is_consumed() {
622 return result;
623 }
624 }
625 match event {
626 Event::MouseDown {
627 pos,
628 button: MouseButton::Left,
629 ..
630 } => {
631 if let Some(idx) = self.menu_at(*pos) {
632 self.open_menu_for_drag_release(idx);
633 EventResult::Consumed
634 } else {
635 EventResult::Ignored
636 }
637 }
638 Event::MouseMove { .. } => EventResult::Ignored,
639 _ => EventResult::Ignored,
640 }
641 }
642
643 fn on_unconsumed_key(&mut self, key: &Key, modifiers: Modifiers) -> EventResult {
644 let response = if self.popup.is_open() {
645 self.popup.handle_shortcut(key, modifiers)
646 } else {
647 self.menus
648 .iter_mut()
649 .find_map(|menu| {
650 let mut popup = PopupMenu::new(menu.items.clone());
651 match popup.handle_shortcut(key, modifiers) {
652 MenuResponse::Action(action) => {
653 menu.items = popup.items;
654 Some(action)
655 }
656 MenuResponse::None | MenuResponse::Closed => None,
657 }
658 })
659 .map(MenuResponse::Action)
660 .unwrap_or(MenuResponse::None)
661 };
662 if let MenuResponse::Action(action) = response {
663 if let Some(idx) = self.open_index {
664 self.menus[idx].items = self.popup.items.clone();
665 }
666 (self.on_action)(&action);
667 if !self.popup.is_open() {
668 self.open_index = None;
669 self.cache.invalidate();
670 }
671 EventResult::Consumed
672 } else {
673 EventResult::Ignored
674 }
675 }
676
677 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
678 let font = self.active_font();
679 let font_size = self.effective_font_size();
680 self.popup.paint(ctx, font, font_size, current_viewport());
681 }
682}
683
684#[cfg(test)]
685mod tests_1;
686#[cfg(test)]
687mod tests_2;