1use crate::navigation::menu::NSMenuItem;
7use crate::theme::MacTheme;
8use egui::{
9 Align2, Area, Color32, CornerRadius, FontId, Key, Order, Pos2, Rect, Sense, Stroke, StrokeKind,
10 Ui, Vec2,
11};
12
13pub fn clamp_menu_rect(click_pos: Pos2, menu_size: Vec2, viewport_rect: Rect) -> Rect {
15 let effective_size = Vec2::new(
16 menu_size.x.min(viewport_rect.width()),
17 menu_size.y.min(viewport_rect.height()),
18 );
19 let mut x = click_pos.x;
20 let mut y = click_pos.y;
21
22 if x + effective_size.x > viewport_rect.max.x {
24 x = viewport_rect.max.x - effective_size.x;
25 }
26 if y + effective_size.y > viewport_rect.max.y {
28 y = viewport_rect.max.y - effective_size.y;
29 }
30 x = x.max(viewport_rect.min.x);
32 y = y.max(viewport_rect.min.y);
33
34 Rect::from_min_size(Pos2::new(x, y), effective_size)
35}
36
37pub struct MacContextMenu<'a> {
39 pub items: &'a [NSMenuItem],
41 pub is_open: &'a mut bool,
43 pub spawn_pos: &'a mut Pos2,
45 pub selected_idx: &'a mut Option<usize>,
47 pub theme: Option<&'a MacTheme>,
49}
50
51impl<'a> MacContextMenu<'a> {
52 pub fn new(
54 items: &'a [NSMenuItem],
55 is_open: &'a mut bool,
56 spawn_pos: &'a mut Pos2,
57 selected_idx: &'a mut Option<usize>,
58 ) -> Self {
59 Self {
60 items,
61 is_open,
62 spawn_pos,
63 selected_idx,
64 theme: None,
65 }
66 }
67
68 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
70 self.theme = Some(theme);
71 self
72 }
73
74 pub fn attach_to(&mut self, response: &egui::Response) {
77 if response.secondary_clicked() {
78 *self.is_open = true;
79 *self.spawn_pos = response
80 .interact_pointer_pos()
81 .unwrap_or_else(|| response.rect.left_top());
82 *self.selected_idx = None;
83 }
84 }
85
86 pub fn close(&mut self) {
88 *self.is_open = false;
89 *self.selected_idx = None;
90 }
91
92 pub fn handle_input(&mut self, input: &egui::InputState) -> Option<String> {
94 if !*self.is_open {
95 return None;
96 }
97
98 if input.key_pressed(Key::Escape) {
99 self.close();
100 return None;
101 }
102
103 let items_len = self.items.len();
104 if items_len == 0 {
105 return None;
106 }
107
108 if input.key_pressed(Key::ArrowDown) {
109 let start = self.selected_idx.map_or(0, |idx| (idx + 1) % items_len);
110 for step in 0..items_len {
111 let candidate = (start + step) % items_len;
112 if let Some(item) = self.items.get(candidate) {
113 if item.enabled
114 && !matches!(
115 item.kind,
116 crate::navigation::menu::MacMenuItemKind::Separator
117 )
118 {
119 *self.selected_idx = Some(candidate);
120 break;
121 }
122 }
123 }
124 return None;
125 }
126
127 if input.key_pressed(Key::ArrowUp) {
128 let start = self
129 .selected_idx
130 .map_or(items_len.saturating_sub(1), |idx| {
131 (idx + items_len - 1) % items_len
132 });
133 for step in 0..items_len {
134 let candidate = (start + items_len - (step % items_len)) % items_len;
135 if let Some(item) = self.items.get(candidate) {
136 if item.enabled
137 && !matches!(
138 item.kind,
139 crate::navigation::menu::MacMenuItemKind::Separator
140 )
141 {
142 *self.selected_idx = Some(candidate);
143 break;
144 }
145 }
146 }
147 return None;
148 }
149
150 if input.key_pressed(Key::Enter) || input.key_pressed(Key::Space) {
151 if let Some(idx) = *self.selected_idx {
152 if let Some(item) = self.items.get(idx) {
153 if item.enabled {
154 if let crate::navigation::menu::MacMenuItemKind::Action { id, .. } =
155 &item.kind
156 {
157 let action = id.clone();
158 self.close();
159 return Some(action);
160 }
161 }
162 }
163 }
164 }
165
166 None
167 }
168
169 pub fn show(mut self, ui: &mut Ui) -> Option<String> {
171 if !*self.is_open {
172 return None;
173 }
174
175 let fallback_theme;
176 let theme = match self.theme {
177 Some(t) => t,
178 None => {
179 fallback_theme = MacTheme::default();
180 &fallback_theme
181 }
182 };
183
184 if let Some(action) = ui.input(|i| self.handle_input(i)) {
185 return Some(action);
186 }
187
188 let item_height = 22.0f32;
189 let padding = 4.0f32;
190 let min_width = 140.0f32;
191 let total_h = (self.items.len() as f32) * item_height + padding * 2.0;
192
193 let viewport_rect = ui
194 .ctx()
195 .input(|i| i.raw.screen_rect)
196 .unwrap_or_else(|| ui.clip_rect());
197 let menu_rect = clamp_menu_rect(
198 *self.spawn_pos,
199 Vec2::new(min_width, total_h),
200 viewport_rect,
201 );
202
203 let clicked_outside = ui.input(|i| {
205 for ev in &i.events {
206 if let egui::Event::PointerButton { pos, pressed: true, .. } = ev {
207 if !menu_rect.contains(*pos) {
208 return true;
209 }
210 }
211 }
212 false
213 });
214 if clicked_outside {
215 self.close();
216 return None;
217 }
218
219 let mut triggered = None;
220 let area_id = ui.scope_id().with("mac_context_menu_area");
221
222 Area::new(area_id)
223 .order(Order::Foreground)
224 .fixed_pos(menu_rect.min)
225 .show(ui.ctx(), |menu_ui| {
226 let painter = menu_ui.painter();
227 let corner_r = 6u8;
228
229 painter.rect(
230 menu_rect,
231 CornerRadius::same(corner_r),
232 theme.window_background(),
233 Stroke::new(1.0, theme.separator()),
234 StrokeKind::Inside,
235 );
236
237 let font_id = FontId::proportional(12.0);
238
239 for (idx, item) in self.items.iter().enumerate() {
240 let item_y = menu_rect.top() + padding + (idx as f32) * item_height;
241 let item_rect = Rect::from_min_size(
242 Pos2::new(menu_rect.left() + padding, item_y),
243 Vec2::new(menu_rect.width() - padding * 2.0, item_height),
244 );
245
246 if matches!(
247 item.kind,
248 crate::navigation::menu::MacMenuItemKind::Separator
249 ) {
250 painter.line_segment(
251 [
252 Pos2::new(item_rect.left(), item_rect.center().y),
253 Pos2::new(item_rect.right(), item_rect.center().y),
254 ],
255 Stroke::new(1.0, theme.separator()),
256 );
257 continue;
258 }
259
260 let is_highlighted = *self.selected_idx == Some(idx);
261 let resp = menu_ui.interact(item_rect, area_id.with(idx), Sense::click());
262
263 if resp.hovered() && item.enabled {
264 *self.selected_idx = Some(idx);
265 }
266
267 if is_highlighted && item.enabled {
268 painter.rect(
269 item_rect,
270 CornerRadius::same(4),
271 theme.control_accent(),
272 Stroke::NONE,
273 StrokeKind::Inside,
274 );
275 }
276
277 let fg = if !item.enabled {
278 theme.placeholder_text()
279 } else if is_highlighted {
280 Color32::WHITE
281 } else {
282 theme.label()
283 };
284
285 painter.text(
286 Pos2::new(item_rect.left() + 8.0, item_rect.center().y),
287 Align2::LEFT_CENTER,
288 &item.title,
289 font_id.clone(),
290 fg,
291 );
292
293 if let crate::navigation::menu::MacMenuItemKind::Action {
294 shortcut: Some(sc),
295 ..
296 } = &item.kind
297 {
298 painter.text(
299 Pos2::new(item_rect.right() - 8.0, item_rect.center().y),
300 Align2::RIGHT_CENTER,
301 sc.format_text(),
302 font_id.clone(),
303 fg,
304 );
305 }
306
307 if resp.clicked() && item.enabled {
308 if let crate::navigation::menu::MacMenuItemKind::Action { id, .. } =
309 &item.kind
310 {
311 triggered = Some(id.clone());
312 self.close();
313 }
314 }
315 }
316 });
317
318 triggered
319 }
320}