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 mut triggered = None;
204 let area_id = ui.scope_id().with("mac_context_menu_area");
205
206 Area::new(area_id)
207 .order(Order::Foreground)
208 .fixed_pos(menu_rect.min)
209 .show(ui.ctx(), |menu_ui| {
210 let painter = menu_ui.painter();
211 let corner_r = 6u8;
212
213 painter.rect(
214 menu_rect,
215 CornerRadius::same(corner_r),
216 theme.window_background(),
217 Stroke::new(1.0, theme.separator()),
218 StrokeKind::Inside,
219 );
220
221 let font_id = FontId::proportional(12.0);
222
223 for (idx, item) in self.items.iter().enumerate() {
224 let item_y = menu_rect.top() + padding + (idx as f32) * item_height;
225 let item_rect = Rect::from_min_size(
226 Pos2::new(menu_rect.left() + padding, item_y),
227 Vec2::new(menu_rect.width() - padding * 2.0, item_height),
228 );
229
230 if matches!(
231 item.kind,
232 crate::navigation::menu::MacMenuItemKind::Separator
233 ) {
234 painter.line_segment(
235 [
236 Pos2::new(item_rect.left(), item_rect.center().y),
237 Pos2::new(item_rect.right(), item_rect.center().y),
238 ],
239 Stroke::new(1.0, theme.separator()),
240 );
241 continue;
242 }
243
244 let is_highlighted = *self.selected_idx == Some(idx);
245 let resp = menu_ui.interact(item_rect, area_id.with(idx), Sense::click());
246
247 if resp.hovered() && item.enabled {
248 *self.selected_idx = Some(idx);
249 }
250
251 if is_highlighted && item.enabled {
252 painter.rect(
253 item_rect,
254 CornerRadius::same(4),
255 theme.control_accent(),
256 Stroke::NONE,
257 StrokeKind::Inside,
258 );
259 }
260
261 let fg = if !item.enabled {
262 theme.placeholder_text()
263 } else if is_highlighted {
264 Color32::WHITE
265 } else {
266 theme.label()
267 };
268
269 painter.text(
270 Pos2::new(item_rect.left() + 8.0, item_rect.center().y),
271 Align2::LEFT_CENTER,
272 &item.title,
273 font_id.clone(),
274 fg,
275 );
276
277 if let crate::navigation::menu::MacMenuItemKind::Action {
278 shortcut: Some(sc),
279 ..
280 } = &item.kind
281 {
282 painter.text(
283 Pos2::new(item_rect.right() - 8.0, item_rect.center().y),
284 Align2::RIGHT_CENTER,
285 sc.format_text(),
286 font_id.clone(),
287 fg,
288 );
289 }
290
291 if resp.clicked() && item.enabled {
292 if let crate::navigation::menu::MacMenuItemKind::Action { id, .. } =
293 &item.kind
294 {
295 triggered = Some(id.clone());
296 self.close();
297 }
298 }
299 }
300 });
301
302 triggered
303 }
304}