Skip to main content

agg_gui/widgets/menu/
geometry.rs

1//! Y-up menu geometry and hit testing.
2//!
3//! Popup menus are global overlays, so geometry is expressed in the owning
4//! widget's local coordinate space but clamped against the current viewport.
5
6use crate::geometry::{Point, Rect, Size};
7
8use super::model::{MenuEntry, MenuItem};
9use super::state::MenuAnchorKind;
10
11pub const ROW_H: f64 = 24.0;
12pub const SEP_H: f64 = 7.0;
13pub const MENU_W: f64 = 224.0;
14pub const BAR_H: f64 = 26.0;
15const MARGIN: f64 = 4.0;
16
17#[derive(Clone, Debug)]
18pub struct PopupLayout {
19    pub rect: Rect,
20    pub rows: Vec<RowLayout>,
21    pub path_prefix: Vec<usize>,
22}
23
24#[derive(Clone, Debug)]
25pub struct RowLayout {
26    pub rect: Rect,
27    pub item_index: Option<usize>,
28}
29
30#[derive(Clone, Debug)]
31pub enum MenuHit {
32    Item(Vec<usize>),
33    Panel,
34}
35
36pub fn stack_layout(
37    root_items: &[MenuEntry],
38    anchor: Point,
39    anchor_kind: MenuAnchorKind,
40    open_path: &[usize],
41    viewport: Size,
42) -> Vec<PopupLayout> {
43    let mut layouts = Vec::new();
44    let mut items = root_items;
45    let mut x = anchor.x;
46    let mut y_top = anchor.y;
47    let mut prefix = Vec::new();
48
49    loop {
50        let rect = popup_rect(items, Point::new(x, y_top), anchor_kind, viewport);
51        let rows = row_layouts(items, rect);
52        layouts.push(PopupLayout {
53            rect,
54            rows,
55            path_prefix: prefix.clone(),
56        });
57
58        let Some(next_idx) = open_path.get(prefix.len()).copied() else {
59            break;
60        };
61        let Some(item) = item_at(items, next_idx) else {
62            break;
63        };
64        if item.submenu.is_empty() {
65            break;
66        }
67        let Some(row) = layouts.last().and_then(|layout| {
68            layout
69                .rows
70                .iter()
71                .find(|row| row.item_index == Some(next_idx))
72                .cloned()
73        }) else {
74            break;
75        };
76        prefix.push(next_idx);
77        items = &item.submenu;
78        x = (rect.x + rect.width - 2.0).min(viewport.width - MENU_W - MARGIN);
79        y_top = row.rect.y + row.rect.height;
80    }
81
82    layouts
83}
84
85pub fn hit_test(layouts: &[PopupLayout], pos: Point) -> Option<MenuHit> {
86    for layout in layouts.iter().rev() {
87        if !contains(layout.rect, pos) {
88            continue;
89        }
90        for row in &layout.rows {
91            if contains(row.rect, pos) {
92                if let Some(idx) = row.item_index {
93                    let mut path = layout.path_prefix.clone();
94                    path.push(idx);
95                    return Some(MenuHit::Item(path));
96                }
97                return Some(MenuHit::Panel);
98            }
99        }
100        return Some(MenuHit::Panel);
101    }
102    None
103}
104
105pub fn item_at_path<'a>(items: &'a [MenuEntry], path: &[usize]) -> Option<&'a MenuItem> {
106    let mut current = items;
107    let mut item = None;
108    for &idx in path {
109        item = item_at(current, idx);
110        current = &item?.submenu;
111    }
112    item
113}
114
115pub fn item_at(items: &[MenuEntry], idx: usize) -> Option<&MenuItem> {
116    match items.get(idx)? {
117        MenuEntry::Item(item) => Some(item),
118        MenuEntry::Separator => None,
119    }
120}
121
122pub fn popup_height(items: &[MenuEntry]) -> f64 {
123    items
124        .iter()
125        .map(|entry| match entry {
126            MenuEntry::Item(_) => ROW_H,
127            MenuEntry::Separator => SEP_H,
128        })
129        .sum::<f64>()
130        .max(ROW_H)
131}
132
133pub fn contains(rect: Rect, pos: Point) -> bool {
134    pos.x >= rect.x
135        && pos.x <= rect.x + rect.width
136        && pos.y >= rect.y
137        && pos.y <= rect.y + rect.height
138}
139
140fn popup_rect(
141    items: &[MenuEntry],
142    anchor: Point,
143    anchor_kind: MenuAnchorKind,
144    viewport: Size,
145) -> Rect {
146    let h = popup_height(items);
147    let x = anchor
148        .x
149        .clamp(MARGIN, (viewport.width - MENU_W - MARGIN).max(MARGIN));
150    let min_y = if anchor_kind == MenuAnchorKind::Bar {
151        -viewport.height
152    } else {
153        MARGIN
154    };
155    let y = (anchor.y - h).clamp(min_y, (viewport.height - h - MARGIN).max(min_y));
156    Rect::new(x, y, MENU_W, h)
157}
158
159fn row_layouts(items: &[MenuEntry], rect: Rect) -> Vec<RowLayout> {
160    let mut y = rect.y + rect.height;
161    let mut rows = Vec::with_capacity(items.len());
162    for (idx, entry) in items.iter().enumerate() {
163        let h = match entry {
164            MenuEntry::Item(_) => ROW_H,
165            MenuEntry::Separator => SEP_H,
166        };
167        y -= h;
168        rows.push(RowLayout {
169            rect: Rect::new(rect.x, y, rect.width, h),
170            item_index: matches!(entry, MenuEntry::Item(_)).then_some(idx),
171        });
172    }
173    rows
174}