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 = 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}