Skip to main content

zenthra_widgets/controls/
menu.rs

1// crates/zenthra-widgets/src/controls/menu.rs
2
3use crate::ui::{DrawCommand, RectDraw, TextDraw, Ui};
4use zenthra_core::{Color, Id, Rect, Response};
5use zenthra_render::RectInstance;
6
7pub struct MenuBarBuilder<'u, 'a> {
8    ui: &'u mut Ui<'a>,
9    bg: Option<Color>,
10}
11
12impl<'u, 'a> MenuBarBuilder<'u, 'a> {
13    pub fn new(ui: &'u mut Ui<'a>) -> Self {
14        Self { ui, bg: None }
15    }
16
17    pub fn bg(mut self, color: Color) -> Self {
18        self.bg = Some(color);
19        self
20    }
21
22    pub fn show<F>(self, f: F)
23    where F: FnOnce(&mut Ui) {
24        let active_menu_key = Id::from_u64(999999900);
25        let active_submenu_key = Id::from_u64(999999901);
26        let hover_flag_key = Id::from_u64(999999902);
27
28        // Reset hover flag for this frame
29        self.ui.interaction_state.insert(hover_flag_key, 0.0);
30
31        // Render the horizontal menu bar container
32        let menu_bar_bg = self.bg.unwrap_or_else(|| {
33            let is_light_theme = self.ui.interaction_state.get(&zenthra_core::Id::from_u64(999999999)).copied().unwrap_or(0.0) > 0.5;
34            if is_light_theme {
35                Color::rgb(0.90, 0.90, 0.92)
36            } else {
37                Color::rgb(2.0 / 255.0, 2.0 / 255.0, 2.0 / 255.0) // 2 – menu bar
38            }
39        });
40
41        self.ui.container()
42            .full_width()
43            .height(30.0)
44            .bg(menu_bar_bg)
45            .row()
46            .padding_left(8.0)
47            .padding_right(8.0)
48            .show(|ui| {
49                f(ui);
50            });
51
52        // Click outside (light dismiss) handling
53        let clicked = self.ui.clicked;
54        let active_menu_id = self.ui.interaction_state.get(&active_menu_key).copied().map(|v| v as u64).unwrap_or(0);
55        let hover_flag = self.ui.interaction_state.get(&hover_flag_key).copied().unwrap_or(0.0) > 0.5;
56
57        if active_menu_id != 0 && clicked && !hover_flag {
58            self.ui.interaction_state.insert(active_menu_key, 0.0);
59            self.ui.interaction_state.insert(active_submenu_key, 0.0);
60            self.ui.needs_redraw = true;
61        }
62    }
63}
64
65pub struct MenuBuilder<'u, 'a> {
66    ui: &'u mut Ui<'a>,
67    label: String,
68    id: Id,
69}
70
71impl<'u, 'a> MenuBuilder<'u, 'a> {
72    pub fn new(ui: &'u mut Ui<'a>, label: &str) -> Self {
73        let id = ui.id();
74        Self {
75            ui,
76            label: label.to_string(),
77            id,
78        }
79    }
80
81    pub fn show<F>(self, f: F)
82    where F: FnOnce(&mut Ui) {
83        let active_menu_key = Id::from_u64(999999900);
84        let active_submenu_key = Id::from_u64(999999901);
85        let hover_flag_key = Id::from_u64(999999902);
86
87        let active_menu_id = self.ui.interaction_state.get(&active_menu_key).copied().map(|v| v as u64).unwrap_or(0);
88        let is_currently_active = active_menu_id == self.id.raw();
89
90        let (x, y) = (self.ui.cursor_x, self.ui.cursor_y);
91        let mut text_w = self.label.len() as f32 * 7.0;
92        if let Some(fs) = self.ui.font_system.as_ref() {
93            let mut adapter = zenthra_text::prelude::CosmicFontProvider::new_with_system(fs.clone());
94            let options = zenthra_text::prelude::TextOptions::new().font_size(13.0);
95            adapter.set_layout_size(1000.0, 100.0);
96            let buffer = adapter.shape(&self.label, &options);
97            text_w = buffer.size().0;
98        }
99        let padding_x = 10.0;
100        let w = text_w + padding_x * 2.0;
101        let h = 26.0;
102
103        let (actual_ox, actual_oy, actual_w, actual_h) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
104            (
105                rect.origin.x + self.ui.offset_x,
106                rect.origin.y + self.ui.offset_y,
107                rect.size.width.max(w),
108                rect.size.height.max(h)
109            )
110        } else {
111            (x + self.ui.offset_x, y + self.ui.offset_y, w, h)
112        };
113
114        let is_hovered = self.ui.is_hovered(self.id, actual_ox, actual_oy, actual_w, actual_h);
115
116        if is_hovered {
117            self.ui.interaction_state.insert(hover_flag_key, 1.0);
118        }
119
120        // Auto-expand on hover if menu bar is active
121        if is_hovered && active_menu_id != 0 && !is_currently_active {
122            self.ui.interaction_state.insert(active_menu_key, self.id.raw() as f32);
123            self.ui.interaction_state.remove(&active_submenu_key);
124            self.ui.needs_redraw = true;
125        }
126
127        // Toggle on click
128        if self.ui.clicked && is_hovered {
129            if is_currently_active {
130                self.ui.interaction_state.insert(active_menu_key, 0.0);
131            } else {
132                self.ui.interaction_state.insert(active_menu_key, self.id.raw() as f32);
133            }
134            self.ui.interaction_state.remove(&active_submenu_key);
135            self.ui.needs_redraw = true;
136        }
137
138        let is_light_theme = self.ui.interaction_state.get(&Id::from_u64(999999999)).copied().unwrap_or(0.0) > 0.5;
139
140        let bg_color = if is_light_theme {
141            if is_currently_active {
142                Color::rgb(0.80, 0.80, 0.85)
143            } else if is_hovered {
144                Color::rgb(0.88, 0.88, 0.90)
145            } else {
146                Color::TRANSPARENT
147            }
148        } else {
149            if is_currently_active || is_hovered {
150                Color::rgb(3.0 / 255.0, 3.0 / 255.0, 3.0 / 255.0) // 3 – hover/active
151            } else {
152                Color::TRANSPARENT
153            }
154        };
155
156        let text_color = if is_light_theme {
157            Color::rgb(0.1, 0.1, 0.15)
158        } else {
159            if is_currently_active || is_hovered {
160                Color::rgb(255.0 / 255.0, 214.0 / 255.0, 0.0 / 255.0) // #FFD600 vivid yellow (accent)
161            } else {
162                Color::rgb(224.0 / 255.0, 224.0 / 255.0, 224.0 / 255.0) // #e0e0e0 (text_primary)
163            }
164        };
165
166        let start_draw = self.ui.draws.len();
167        self.ui.draws.push(DrawCommand::Rect(RectDraw {
168            instance: RectInstance {
169                pos: [x, y + 2.0],
170                size: [w, h],
171                color: bg_color.to_array(),
172                radius: [3.0; 4],
173                ..Default::default()
174            }
175        }));
176
177        self.ui.draws.push(DrawCommand::Text(TextDraw {
178            text: self.label.clone(),
179            pos: [x + (w - text_w) / 2.0, y + 6.0],
180            options: zenthra_text::prelude::TextOptions::new().font_size(13.0).color(text_color),
181            clip: [x, y, w, h + 4.0],
182        }));
183
184        self.ui.record_layout(self.id, Rect::new(x, y, w, h + 4.0));
185        self.ui.advance(w, h + 4.0, start_draw);
186
187        if is_currently_active {
188            let popup_id = Id::from_u64((self.id.raw() << 8) | 10);
189
190            // Check if popup is hovered
191            if let Some(rect) = self.ui.screen_layout_cache.get(&popup_id) {
192                if self.ui.mouse_in_rect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height) {
193                    self.ui.interaction_state.insert(hover_flag_key, 1.0);
194                }
195            }
196
197            // Isolate layout state variables
198            let prev_child_ranges = std::mem::take(&mut self.ui.child_draw_ranges);
199            let prev_id_ranges = std::mem::take(&mut self.ui.id_ranges);
200            let prev_id_log = std::mem::take(&mut self.ui.id_log);
201            let prev_child_sizes = std::mem::take(&mut self.ui.child_sizes);
202            let prev_child_origins = std::mem::take(&mut self.ui.child_origins);
203
204            let popup_bg = if is_light_theme {
205                Color::WHITE
206            } else {
207                Color::rgb(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0) // 1 – popup bg
208            };
209
210            let popup_border = if is_light_theme {
211                Color::rgb(0.8, 0.8, 0.85)
212            } else {
213                Color::rgb(3.0 / 255.0, 3.0 / 255.0, 3.0 / 255.0) // 3 – popup border
214            };
215
216            let popup_x = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
217                rect.origin.x
218            } else {
219                x
220            };
221            let popup_y = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
222                rect.origin.y
223            } else {
224                y
225            };
226
227            self.ui.container()
228                .id(popup_id)
229                .absolute(popup_x, popup_y + h + 2.0)
230                .overlay()
231                .width(270.0)
232                .bg(popup_bg)
233                .border(popup_border, 1.0)
234                .radius_all(4.0)
235                .padding(4.0, 4.0, 4.0, 4.0)
236                .column()
237                .show(|ui| {
238                    f(ui);
239                });
240
241            // Discard child variables and restore parent's
242            let _ = std::mem::take(&mut self.ui.child_draw_ranges);
243            let _ = std::mem::take(&mut self.ui.id_ranges);
244            let _ = std::mem::take(&mut self.ui.id_log);
245            let _ = std::mem::take(&mut self.ui.child_sizes);
246            let _ = std::mem::take(&mut self.ui.child_origins);
247
248            self.ui.child_draw_ranges = prev_child_ranges;
249            self.ui.id_ranges = prev_id_ranges;
250            self.ui.id_log = prev_id_log;
251            self.ui.child_sizes = prev_child_sizes;
252            self.ui.child_origins = prev_child_origins;
253        }
254    }
255}
256
257pub struct SubMenuBuilder<'u, 'a> {
258    ui: &'u mut Ui<'a>,
259    label: String,
260    id: Id,
261}
262
263impl<'u, 'a> SubMenuBuilder<'u, 'a> {
264    pub fn new(ui: &'u mut Ui<'a>, label: &str) -> Self {
265        let id = ui.id();
266        Self {
267            ui,
268            label: label.to_string(),
269            id,
270        }
271    }
272
273    pub fn show<F>(self, f: F)
274    where F: FnOnce(&mut Ui) {
275        let active_submenu_key = Id::from_u64(999999901);
276        let hover_flag_key = Id::from_u64(999999902);
277
278        let active_submenu_id = self.ui.interaction_state.get(&active_submenu_key).copied().map(|v| v as u64).unwrap_or(0);
279        let is_currently_active = active_submenu_id == self.id.raw();
280
281        let (x, y) = (self.ui.cursor_x, self.ui.cursor_y);
282        let w = 262.0;
283        let h = 26.0;
284
285        let (actual_ox, actual_oy, actual_w, actual_h) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
286            (
287                rect.origin.x + self.ui.offset_x,
288                rect.origin.y + self.ui.offset_y,
289                rect.size.width.max(w),
290                rect.size.height.max(h)
291            )
292        } else {
293            (x + self.ui.offset_x, y + self.ui.offset_y, w, h)
294        };
295
296        let is_hovered = self.ui.is_hovered(self.id, actual_ox, actual_oy, actual_w, actual_h);
297
298        if is_hovered {
299            self.ui.interaction_state.insert(hover_flag_key, 1.0);
300        }
301
302        // Open sub-menu on hover
303        if is_hovered && !is_currently_active {
304            self.ui.interaction_state.insert(active_submenu_key, self.id.raw() as f32);
305            self.ui.needs_redraw = true;
306        }
307
308        let is_light_theme = self.ui.interaction_state.get(&Id::from_u64(999999999)).copied().unwrap_or(0.0) > 0.5;
309
310        let bg_color = if is_light_theme {
311            if is_currently_active || is_hovered {
312                Color::rgb(0.90, 0.90, 0.95)
313            } else {
314                Color::TRANSPARENT
315            }
316        } else {
317            if is_currently_active || is_hovered {
318                Color::rgb(3.0 / 255.0, 3.0 / 255.0, 3.0 / 255.0) // 3 – hover/active
319            } else {
320                Color::TRANSPARENT
321            }
322        };
323
324        let text_color = if is_light_theme {
325            Color::rgb(0.1, 0.1, 0.15)
326        } else {
327            if is_currently_active || is_hovered {
328                Color::rgb(255.0 / 255.0, 214.0 / 255.0, 0.0 / 255.0) // #FFD600 vivid yellow (accent)
329            } else {
330                Color::rgb(224.0 / 255.0, 224.0 / 255.0, 224.0 / 255.0) // #e0e0e0 (text_primary)
331            }
332        };
333
334        let chevron_color = if is_light_theme {
335            Color::rgb(0.5, 0.5, 0.5)
336        } else {
337            if is_currently_active || is_hovered {
338                Color::rgb(255.0 / 255.0, 214.0 / 255.0, 0.0 / 255.0) // #FFD600 vivid yellow (accent)
339            } else {
340                Color::rgb(102.0 / 255.0, 102.0 / 255.0, 102.0 / 255.0) // #666666 (text_muted)
341            }
342        };
343
344        let start_draw = self.ui.draws.len();
345
346        self.ui.draws.push(DrawCommand::Rect(RectDraw {
347            instance: RectInstance {
348                pos: [x, y],
349                size: [w, h],
350                color: bg_color.to_array(),
351                radius: [3.0; 4],
352                ..Default::default()
353            }
354        }));
355
356        self.ui.draws.push(DrawCommand::Text(TextDraw {
357            text: self.label.clone(),
358            pos: [x + 8.0, y + 6.0],
359            options: zenthra_text::prelude::TextOptions::new().font_size(13.0).color(text_color),
360            clip: [x, y, w, h],
361        }));
362
363        // Right arrow indicator
364        self.ui.draws.push(DrawCommand::Text(TextDraw {
365            text: crate::icons::NF_FA_CHEVRON_RIGHT.to_string(),
366            pos: [x + w - 15.0, y + 6.0],
367            options: zenthra_text::prelude::TextOptions::new().font_size(12.0).color(chevron_color),
368            clip: [x, y, w, h],
369        }));
370
371        self.ui.record_layout(self.id, Rect::new(x, y, w, h));
372        self.ui.advance(w, h, start_draw);
373
374        if is_currently_active {
375            let sub_popup_id = Id::from_u64((self.id.raw() << 8) | 11);
376
377            // Check if sub-popup is hovered
378            if let Some(rect) = self.ui.screen_layout_cache.get(&sub_popup_id) {
379                if self.ui.mouse_in_rect(rect.origin.x, rect.origin.y, rect.size.width, rect.size.height) {
380                    self.ui.interaction_state.insert(hover_flag_key, 1.0);
381                }
382            }
383
384            // Isolate layout state variables
385            let prev_child_ranges = std::mem::take(&mut self.ui.child_draw_ranges);
386            let prev_id_ranges = std::mem::take(&mut self.ui.id_ranges);
387            let prev_id_log = std::mem::take(&mut self.ui.id_log);
388            let prev_child_sizes = std::mem::take(&mut self.ui.child_sizes);
389            let prev_child_origins = std::mem::take(&mut self.ui.child_origins);
390
391            let popup_bg = if is_light_theme {
392                Color::WHITE
393            } else {
394                Color::rgb(1.0 / 255.0, 1.0 / 255.0, 1.0 / 255.0) // 1 – popup bg
395            };
396
397            let popup_border = if is_light_theme {
398                Color::rgb(0.8, 0.8, 0.85)
399            } else {
400                Color::rgb(3.0 / 255.0, 3.0 / 255.0, 3.0 / 255.0) // 3 – popup border
401            };
402
403            self.ui.container()
404                .id(sub_popup_id)
405                .absolute(x + w + 4.0, y)
406                .overlay()
407                .width(270.0)
408                .bg(popup_bg)
409                .border(popup_border, 1.0)
410                .radius_all(4.0)
411                .padding(4.0, 4.0, 4.0, 4.0)
412                .column()
413                .show(|ui| {
414                    f(ui);
415                });
416
417            // Discard child variables and restore parent's
418            let _ = std::mem::take(&mut self.ui.child_draw_ranges);
419            let _ = std::mem::take(&mut self.ui.id_ranges);
420            let _ = std::mem::take(&mut self.ui.id_log);
421            let _ = std::mem::take(&mut self.ui.child_sizes);
422            let _ = std::mem::take(&mut self.ui.child_origins);
423
424            self.ui.child_draw_ranges = prev_child_ranges;
425            self.ui.id_ranges = prev_id_ranges;
426            self.ui.id_log = prev_id_log;
427            self.ui.child_sizes = prev_child_sizes;
428            self.ui.child_origins = prev_child_origins;
429        }
430    }
431}
432
433pub struct MenuItemBuilder<'u, 'a> {
434    ui: &'u mut Ui<'a>,
435    label: String,
436    shortcut: Option<String>,
437    id: Id,
438}
439
440impl<'u, 'a> MenuItemBuilder<'u, 'a> {
441    pub fn new(ui: &'u mut Ui<'a>, label: &str) -> Self {
442        let id = ui.id();
443        Self {
444            ui,
445            label: label.to_string(),
446            shortcut: None,
447            id,
448        }
449    }
450
451    pub fn shortcut(mut self, shortcut: &str) -> Self {
452        self.shortcut = Some(shortcut.to_string());
453        self
454    }
455
456    pub fn show(self) -> Response {
457        let hover_flag_key = Id::from_u64(999999902);
458
459        let (x, y) = (self.ui.cursor_x, self.ui.cursor_y);
460        let w = 262.0;
461        let h = 26.0;
462
463        let (actual_ox, actual_oy, actual_w, actual_h) = if let Some((rect, _)) = self.ui.get_recorded_layout(self.id) {
464            (
465                rect.origin.x + self.ui.offset_x,
466                rect.origin.y + self.ui.offset_y,
467                rect.size.width.max(w),
468                rect.size.height.max(h)
469            )
470        } else {
471            (x + self.ui.offset_x, y + self.ui.offset_y, w, h)
472        };
473
474        let is_hovered = self.ui.is_hovered(self.id, actual_ox, actual_oy, actual_w, actual_h);
475
476        if is_hovered {
477            self.ui.interaction_state.insert(hover_flag_key, 1.0);
478        }
479
480        let is_light_theme = self.ui.interaction_state.get(&Id::from_u64(999999999)).copied().unwrap_or(0.0) > 0.5;
481
482        let bg_color = if is_light_theme {
483            if is_hovered {
484                Color::rgb(0.9, 0.9, 0.95)
485            } else {
486                Color::TRANSPARENT
487            }
488        } else {
489            if is_hovered {
490                Color::rgb(3.0 / 255.0, 3.0 / 255.0, 3.0 / 255.0) // 3 – hover
491            } else {
492                Color::TRANSPARENT
493            }
494        };
495
496        let text_color = if is_light_theme {
497            Color::rgb(0.1, 0.1, 0.15)
498        } else {
499            if is_hovered {
500                Color::rgb(255.0 / 255.0, 214.0 / 255.0, 0.0 / 255.0) // #FFD600 vivid yellow (accent)
501            } else {
502                Color::rgb(224.0 / 255.0, 224.0 / 255.0, 224.0 / 255.0) // #e0e0e0 (text_primary)
503            }
504        };
505
506        let shortcut_color = if is_light_theme {
507            Color::rgb(0.5, 0.5, 0.5)
508        } else {
509            if is_hovered {
510                Color::rgb(255.0 / 255.0, 214.0 / 255.0, 0.0 / 255.0) // #FFD600 vivid yellow (accent)
511            } else {
512                Color::rgb(102.0 / 255.0, 102.0 / 255.0, 102.0 / 255.0) // #666666 (text_muted)
513            }
514        };
515
516        let start_draw = self.ui.draws.len();
517
518        self.ui.draws.push(DrawCommand::Rect(RectDraw {
519            instance: RectInstance {
520                pos: [x, y],
521                size: [w, h],
522                color: bg_color.to_array(),
523                radius: [3.0; 4],
524                ..Default::default()
525            }
526        }));
527
528        self.ui.draws.push(DrawCommand::Text(TextDraw {
529            text: self.label.clone(),
530            pos: [x + 8.0, y + 6.0],
531            options: zenthra_text::prelude::TextOptions::new().font_size(13.0).color(text_color),
532            clip: [x, y, w, h],
533        }));
534
535        if let Some(sh) = self.shortcut {
536            self.ui.draws.push(DrawCommand::Text(TextDraw {
537                text: sh,
538                pos: [x + w - 80.0, y + 6.0],
539                options: zenthra_text::prelude::TextOptions::new().font_size(12.0).color(shortcut_color),
540                clip: [x, y, w, h],
541            }));
542        }
543
544        self.ui.record_layout(self.id, Rect::new(x, y, w, h));
545        self.ui.advance(w, h, start_draw);
546
547        let clicked = self.ui.clicked && is_hovered;
548        if clicked {
549            // Close all menus upon item selection
550            let active_menu_key = Id::from_u64(999999900);
551            let active_submenu_key = Id::from_u64(999999901);
552            self.ui.interaction_state.insert(active_menu_key, 0.0);
553            self.ui.interaction_state.insert(active_submenu_key, 0.0);
554            self.ui.needs_redraw = true;
555        }
556
557        Response {
558            clicked,
559            hovered: is_hovered,
560            pressed: is_hovered && self.ui.mouse_down,
561        }
562    }
563}