agg_gui/widgets/menu/widget/
mod.rs1use 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, Widget};
14
15use super::geometry::{contains, item_at_path, BAR_H};
16use super::model::MenuEntry;
17use super::paint::{paint_menu_bar_button, paint_popup_stack, MenuStyle};
18use super::state::{MenuAnchorKind, MenuResponse, PopupMenuState};
19
20const TOUCH_SYNTH_WINDOW_MS: u128 = 50;
25
26fn is_touch_synthesized() -> bool {
27 crate::touch_state::last_touch_event_age()
28 .map(|d| d.as_millis() < TOUCH_SYNTH_WINDOW_MS)
29 .unwrap_or(false)
30}
31
32#[derive(Clone)]
33pub struct PopupMenu {
34 pub items: Vec<MenuEntry>,
35 pub state: PopupMenuState,
36 pub style: MenuStyle,
37}
38
39impl PopupMenu {
40 pub fn new(items: Vec<MenuEntry>) -> Self {
41 Self {
42 items,
43 state: PopupMenuState::default(),
44 style: MenuStyle::default(),
45 }
46 }
47
48 pub fn open_at(&mut self, pos: Point) {
49 self.state.open_at(pos, MenuAnchorKind::Context);
50 }
51
52 pub fn close(&mut self) {
53 self.state.close();
54 }
55
56 pub fn is_open(&self) -> bool {
57 self.state.open
58 }
59
60 pub fn take_suppress_mouse_up(&mut self) -> bool {
61 self.state.take_suppress_mouse_up()
62 }
63
64 pub fn handle_event(&mut self, event: &Event, viewport: Size) -> (EventResult, MenuResponse) {
65 self.state.handle_event(&mut self.items, event, viewport)
66 }
67
68 pub fn body_contains(&self, pos: Point, viewport: Size) -> bool {
74 self.state
75 .layouts(&self.items, viewport)
76 .iter()
77 .any(|layout| {
78 pos.x >= layout.rect.x
79 && pos.x <= layout.rect.x + layout.rect.width
80 && pos.y >= layout.rect.y
81 && pos.y <= layout.rect.y + layout.rect.height
82 })
83 }
84
85 pub fn handle_shortcut(&mut self, key: &Key, modifiers: Modifiers) -> MenuResponse {
86 self.state.handle_shortcut(&mut self.items, key, modifiers)
87 }
88
89 pub fn paint(&self, ctx: &mut dyn DrawCtx, font: Arc<Font>, font_size: f64, viewport: Size) {
90 let layouts = self.state.layouts(&self.items, viewport);
91 paint_popup_stack(
92 ctx,
93 font,
94 font_size,
95 &self.items,
96 &self.state,
97 &layouts,
98 &self.style,
99 );
100 }
101}
102
103pub struct MenuBar {
104 bounds: Rect,
105 children: Vec<Box<dyn Widget>>,
106 font: Arc<Font>,
107 font_size: f64,
108 menus: Vec<TopMenu>,
109 open_index: Option<usize>,
110 hover_index: Option<usize>,
111 popup: PopupMenu,
112 on_action: Box<dyn FnMut(&str)>,
113 suppress_hover_for: Option<usize>,
121 fit_width: bool,
127}
128
129pub struct TopMenu {
130 pub label: String,
131 pub items: Vec<MenuEntry>,
132 rect: Rect,
133}
134
135impl TopMenu {
136 pub fn new(label: impl Into<String>, items: Vec<MenuEntry>) -> Self {
137 Self {
138 label: label.into(),
139 items,
140 rect: Rect::default(),
141 }
142 }
143}
144
145impl MenuBar {
146 pub fn new(
147 font: Arc<Font>,
148 menus: Vec<TopMenu>,
149 on_action: impl FnMut(&str) + 'static,
150 ) -> Self {
151 Self {
152 bounds: Rect::default(),
153 children: Vec::new(),
154 font,
155 font_size: 14.0,
156 menus,
157 open_index: None,
158 hover_index: None,
159 popup: PopupMenu::new(Vec::new()),
160 on_action: Box::new(on_action),
161 suppress_hover_for: None,
162 fit_width: false,
163 }
164 }
165
166 pub fn with_fit_width(mut self, fit: bool) -> Self {
172 self.fit_width = fit;
173 self
174 }
175
176 pub fn with_font_size(mut self, font_size: f64) -> Self {
177 self.font_size = font_size;
178 self
179 }
180
181 fn active_font(&self) -> Arc<Font> {
186 font_settings::current_system_font().unwrap_or_else(|| Arc::clone(&self.font))
187 }
188
189 fn menu_at(&self, pos: Point) -> Option<usize> {
190 self.menus.iter().position(|menu| contains(menu.rect, pos))
191 }
192
193 fn open_menu(&mut self, idx: usize) {
194 let rect = self.menus[idx].rect;
195 self.popup.items = self.menus[idx].items.clone();
196 self.popup
197 .state
198 .open_at(Point::new(rect.x, rect.y), MenuAnchorKind::Bar);
199 self.open_index = Some(idx);
200 self.hover_index = Some(idx);
201 crate::animation::request_draw();
202 }
203
204 fn open_menu_for_drag_release(&mut self, idx: usize) {
205 self.open_menu(idx);
206 self.popup.state.arm_mouse_up_activation();
207 }
208
209 fn switch_open_menu(&mut self, delta: isize) -> EventResult {
210 let Some(current) = self.open_index else {
211 return EventResult::Ignored;
212 };
213 if self.menus.is_empty() {
214 return EventResult::Ignored;
215 }
216 let len = self.menus.len() as isize;
217 let next = (current as isize + delta).rem_euclid(len) as usize;
218 self.open_menu(next);
219 EventResult::Consumed
220 }
221
222 fn should_switch_top_menu(&self, key: &Key) -> bool {
223 match key {
224 Key::ArrowLeft => self.popup.state.open_path.is_empty(),
225 Key::ArrowRight => {
226 if !self.popup.state.open_path.is_empty() {
227 return false;
228 }
229 self.popup
230 .state
231 .hover_path
232 .as_deref()
233 .and_then(|path| item_at_path(&self.popup.items, path))
234 .map_or(true, |item| !item.has_submenu())
235 }
236 _ => false,
237 }
238 }
239
240 fn set_hover_index(&mut self, hover: Option<usize>) {
241 let hover = if is_touch_synthesized() { None } else { hover };
247 if self.hover_index != hover {
248 self.hover_index = hover;
249 crate::animation::request_draw();
256 }
257 if self.suppress_hover_for != hover {
261 self.suppress_hover_for = None;
262 }
263 }
264}
265
266impl Widget for MenuBar {
267 fn type_name(&self) -> &'static str {
268 "MenuBar"
269 }
270
271 fn bounds(&self) -> Rect {
272 self.bounds
273 }
274
275 fn set_bounds(&mut self, bounds: Rect) {
276 self.bounds = bounds;
277 }
278
279 fn children(&self) -> &[Box<dyn Widget>] {
280 &self.children
281 }
282
283 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
284 &mut self.children
285 }
286
287 fn layout(&mut self, available: Size) -> Size {
288 let mut x = 0.0;
289 for menu in &mut self.menus {
290 let width = (menu.label.chars().count() as f64 * 8.0 + 22.0).max(52.0);
291 menu.rect = Rect::new(x, 0.0, width, BAR_H);
292 x += width;
293 }
294 let report_w = if self.fit_width { x } else { available.width };
299 Size::new(report_w, BAR_H)
300 }
301
302 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
303 ctx.set_font(self.active_font());
304 ctx.set_font_size(self.font_size);
305 let v = ctx.visuals();
306 ctx.set_fill_color(v.top_bar_bg);
307 ctx.begin_path();
308 ctx.rect(0.0, 0.0, self.bounds.width, BAR_H);
309 ctx.fill();
310 for (idx, menu) in self.menus.iter().enumerate() {
311 let hovered = self.hover_index == Some(idx)
317 && self.suppress_hover_for != Some(idx);
318 paint_menu_bar_button(
319 ctx,
320 menu.rect,
321 &menu.label,
322 self.open_index == Some(idx),
323 hovered,
324 );
325 }
326 }
327
328 fn hit_test_global_overlay(&self, _local_pos: Point) -> bool {
329 self.popup.is_open()
330 }
331
332 fn has_active_modal(&self) -> bool {
333 self.popup.is_open()
334 }
335
336 fn on_event(&mut self, event: &Event) -> EventResult {
337 if let Event::MouseMove { pos } = event {
338 let hovered = self.menu_at(*pos);
339 self.set_hover_index(hovered);
340 let from_touch = is_touch_synthesized();
351 if self.popup.is_open() && !from_touch {
352 if let Some(idx) = hovered {
353 if self.open_index != Some(idx) {
354 let activate_on_release = self.popup.state.is_mouse_up_activation_armed();
355 self.open_menu(idx);
356 if activate_on_release {
357 self.popup.state.arm_mouse_up_activation();
358 }
359 }
360 return EventResult::Consumed;
361 }
362 }
363 }
364 if self.popup.is_open() {
365 if let Event::KeyDown { key, .. } = event {
366 if self.should_switch_top_menu(key) {
367 return match key {
368 Key::ArrowLeft => self.switch_open_menu(-1),
369 Key::ArrowRight => self.switch_open_menu(1),
370 _ => EventResult::Ignored,
371 };
372 }
373 }
374 if let Event::MouseDown {
382 pos,
383 button: MouseButton::Left,
384 ..
385 } = event
386 {
387 if let Some(idx) = self.menu_at(*pos) {
388 if self.open_index != Some(idx) {
389 self.open_menu(idx);
390 return EventResult::Consumed;
391 }
392 }
393 }
394 if let Event::MouseUp {
403 pos,
404 button: MouseButton::Left,
405 ..
406 } = event
407 {
408 if self.popup.state.is_mouse_up_activation_armed()
409 && self.menu_at(*pos).is_none()
410 && !self.popup.body_contains(*pos, current_viewport())
411 {
412 self.popup.close();
413 self.open_index = None;
414 crate::animation::request_draw();
415 return EventResult::Consumed;
416 }
417 }
418 let (result, response) = self.popup.handle_event(event, current_viewport());
419 if let MenuResponse::Action(action) = response {
420 if let Some(idx) = self.open_index {
421 self.menus[idx].items = self.popup.items.clone();
422 }
423 (self.on_action)(&action);
424 if !self.popup.is_open() {
425 self.open_index = None;
426 }
427 } else if matches!(response, MenuResponse::Closed) {
428 self.open_index = None;
429 self.suppress_hover_for = self.hover_index;
435 }
436 if result == EventResult::Consumed {
437 return result;
438 }
439 }
440 match event {
441 Event::MouseDown {
442 pos,
443 button: MouseButton::Left,
444 ..
445 } => {
446 if let Some(idx) = self.menu_at(*pos) {
447 self.open_menu_for_drag_release(idx);
448 EventResult::Consumed
449 } else {
450 EventResult::Ignored
451 }
452 }
453 Event::MouseMove { .. } => EventResult::Ignored,
454 _ => EventResult::Ignored,
455 }
456 }
457
458 fn on_unconsumed_key(&mut self, key: &Key, modifiers: Modifiers) -> EventResult {
459 let response = if self.popup.is_open() {
460 self.popup.handle_shortcut(key, modifiers)
461 } else {
462 self.menus
463 .iter_mut()
464 .find_map(|menu| {
465 let mut popup = PopupMenu::new(menu.items.clone());
466 match popup.handle_shortcut(key, modifiers) {
467 MenuResponse::Action(action) => {
468 menu.items = popup.items;
469 Some(action)
470 }
471 MenuResponse::None | MenuResponse::Closed => None,
472 }
473 })
474 .map(MenuResponse::Action)
475 .unwrap_or(MenuResponse::None)
476 };
477 if let MenuResponse::Action(action) = response {
478 if let Some(idx) = self.open_index {
479 self.menus[idx].items = self.popup.items.clone();
480 }
481 (self.on_action)(&action);
482 if !self.popup.is_open() {
483 self.open_index = None;
484 }
485 EventResult::Consumed
486 } else {
487 EventResult::Ignored
488 }
489 }
490
491 fn paint_global_overlay(&mut self, ctx: &mut dyn DrawCtx) {
492 self.popup.paint(ctx, self.active_font(), self.font_size, current_viewport());
493 }
494}
495
496#[cfg(test)]
497mod tests_1;
498#[cfg(test)]
499mod tests_2;