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