agg_gui/widgets/menu/
geometry.rs1use 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 = match anchor_kind {
86 MenuAnchorKind::BottomBar => row.rect.y,
87 _ => row.rect.y + row.rect.height,
88 };
89 }
90
91 layouts
92}
93
94pub fn hit_test(layouts: &[PopupLayout], pos: Point) -> Option<MenuHit> {
95 for layout in layouts.iter().rev() {
96 if !contains(layout.rect, pos) {
97 continue;
98 }
99 for row in &layout.rows {
100 if contains(row.rect, pos) {
101 if let Some(idx) = row.item_index {
102 let mut path = layout.path_prefix.clone();
103 path.push(idx);
104 return Some(MenuHit::Item(path));
105 }
106 return Some(MenuHit::Panel);
107 }
108 }
109 return Some(MenuHit::Panel);
110 }
111 None
112}
113
114pub fn item_at_path<'a>(items: &'a [MenuEntry], path: &[usize]) -> Option<&'a MenuItem> {
115 let mut current = items;
116 let mut item = None;
117 for &idx in path {
118 item = item_at(current, idx);
119 current = &item?.submenu;
120 }
121 item
122}
123
124pub fn item_at(items: &[MenuEntry], idx: usize) -> Option<&MenuItem> {
125 match items.get(idx)? {
126 MenuEntry::Item(item) => Some(item),
127 MenuEntry::Separator => None,
128 }
129}
130
131pub fn popup_height(items: &[MenuEntry]) -> f64 {
132 items
133 .iter()
134 .map(|entry| match entry {
135 MenuEntry::Item(_) => ROW_H,
136 MenuEntry::Separator => SEP_H,
137 })
138 .sum::<f64>()
139 .max(ROW_H)
140}
141
142pub fn contains(rect: Rect, pos: Point) -> bool {
143 pos.x >= rect.x
144 && pos.x <= rect.x + rect.width
145 && pos.y >= rect.y
146 && pos.y <= rect.y + rect.height
147}
148
149fn popup_rect(
150 items: &[MenuEntry],
151 anchor: Point,
152 anchor_kind: MenuAnchorKind,
153 viewport: Size,
154) -> Rect {
155 let h = popup_height(items);
156 let x = anchor
157 .x
158 .clamp(MARGIN, (viewport.width - MENU_W - MARGIN).max(MARGIN));
159 let (min_y, raw_y) = match anchor_kind {
160 MenuAnchorKind::Bar => (-viewport.height, anchor.y - h),
165 MenuAnchorKind::BottomBar => (MARGIN, anchor.y),
168 MenuAnchorKind::Context => (MARGIN, anchor.y - h),
169 };
170 let y = raw_y.clamp(min_y, (viewport.height - h - MARGIN).max(min_y));
171 Rect::new(x, y, MENU_W, h)
172}
173
174fn row_layouts(items: &[MenuEntry], rect: Rect) -> Vec<RowLayout> {
175 let mut y = rect.y + rect.height;
176 let mut rows = Vec::with_capacity(items.len());
177 for (idx, entry) in items.iter().enumerate() {
178 let h = match entry {
179 MenuEntry::Item(_) => ROW_H,
180 MenuEntry::Separator => SEP_H,
181 };
182 y -= h;
183 rows.push(RowLayout {
184 rect: Rect::new(rect.x, y, rect.width, h),
185 item_index: matches!(entry, MenuEntry::Item(_)).then_some(idx),
186 });
187 }
188 rows
189}