1use std::sync::{Arc, Mutex};
2use zenthra_text::prelude::*;
3use crate::container::{ContainerBuilder, Direction};
4use crate::lazy_container::LazyContainerBuilder;
5use crate::text::{CursorIcon, TextBuilder};
6use zenthra_core::{Color, Id, SemanticNode, Rect};
7use zenthra_render::RectInstance;
8use zenthra_platform::event::PlatformEvent;
9
10pub struct TextDraw {
11 pub text: String,
12 pub pos: [f32; 2],
13 pub options: TextOptions,
14 pub clip: [f32; 4],
15}
16
17pub struct OverlayRectDraw {
18 pub x: f32,
19 pub y: f32,
20 pub width: f32,
21 pub height: f32,
22 pub color: Color,
23 pub clip: [f32; 4],
24}
25
26pub struct RectDraw {
27 pub instance: RectInstance,
28}
29
30pub struct ImageDraw {
31 pub source: zenthra_core::ImageSource,
32 pub instance: zenthra_render::ImageInstance,
33 pub fit: zenthra_core::ObjectFit,
34 pub internal_scale: [f32; 2],
35 pub internal_offset: [f32; 2],
36}
37
38pub struct BackdropBlurDraw {
43 pub x: f32,
45 pub y: f32,
46 pub width: f32,
47 pub height: f32,
48 pub radius: [f32; 4],
50 pub blur_radius: f32,
52 pub clip_rect: [f32; 4],
54
55 pub brightness: f32,
57 pub saturation: f32,
58 pub contrast: f32,
59 pub opacity: f32,
60 pub blur_type: zenthra_core::style::blur::Type,
61}
62
63pub struct CustomPostProcessDraw {
64 pub x: f32,
65 pub y: f32,
66 pub width: f32,
67 pub height: f32,
68 pub radius: [f32; 4],
69 pub blur_radius: f32,
70 pub shader_id: &'static str,
71 pub clip_rect: [f32; 4],
72}
73
74pub enum DrawCommand {
75 Rect(RectDraw),
76 Text(TextDraw),
77 OverlayRect(OverlayRectDraw),
78 Image(ImageDraw),
79 BackdropBlur(BackdropBlurDraw),
81 CustomPostProcess(CustomPostProcessDraw),
83}
84
85#[derive(Debug, Clone, Copy)]
86pub struct ScrollDrag {
87 pub id: Id,
88 pub start_mouse: f32,
89 pub start_scroll: f32,
90}
91
92pub struct Ui<'a> {
93 pub width: f32,
94 pub height: f32,
95 pub scale_factor: f32,
96 pub mouse_x: f32,
97 pub mouse_y: f32,
98 pub mouse_down: bool,
99 pub cursor_icon: CursorIcon,
100 pub draws: Vec<DrawCommand>,
101 pub overlays: Vec<DrawCommand>,
102 pub cursor_x: f32,
103 pub cursor_y: f32,
104 pub base_x: f32,
105 pub base_y: f32,
106 pub direction: Direction,
107 pub line_height: f32,
108 pub child_sizes: Vec<(f32, f32)>,
109 pub child_draw_ranges: Vec<(usize, usize)>,
110 pub child_origins: Vec<(f32, f32)>,
111 pub id_log: Vec<Id>,
112 pub id_ranges: Vec<(usize, usize)>,
113 pub last_w: f32,
114 pub last_h: f32,
115 pub max_x: f32,
116 pub max_y: f32,
117 pub offset_x: f32,
118 pub offset_y: f32,
119 pub font_system: Option<Arc<Mutex<FontSystem>>>,
120 pub input_events: Vec<PlatformEvent>,
121 pub focused_id: Option<Id>,
122 pub id_counter: u64,
123 pub scroll_state: &'a mut std::collections::HashMap<Id, (f32, f32)>,
124 pub cursor_state: &'a mut std::collections::HashMap<Id, usize>,
125 pub interaction_state: &'a mut std::collections::HashMap<Id, f32>,
126 pub active_drag: Option<ScrollDrag>,
127 pub clicked: bool,
128 pub right_clicked: bool,
129 pub elapsed_time: f32,
130 pub semantic_nodes: Vec<SemanticNode>,
131 pub semantic_stack: Vec<Id>,
132 pub render_mode_stack: Vec<zenthra_core::RenderMode>,
133 pub needs_redraw: bool,
134 pub layout_cache: &'a std::collections::HashMap<Id, (Rect, u64)>,
135 pub next_layout_cache: &'a mut std::collections::HashMap<Id, (Rect, u64)>,
136 pub screen_layout_cache: &'a std::collections::HashMap<Id, Rect>,
137 pub next_screen_layout_cache: &'a mut std::collections::HashMap<Id, Rect>,
138 pub image_sizes: &'a std::collections::HashMap<zenthra_core::ImageSource, (u32, u32)>,
139 pub available_width: f32,
140 pub skip_clip_stack: Vec<bool>,
141 pub current_viewport: Rect,
142 pub window_overlays: Vec<(Id, Vec<DrawCommand>)>,
143 pub current_window_id: Option<Id>,
144 pub widget_window_map: &'a std::collections::HashMap<Id, Id>,
145 pub next_widget_window_map: &'a mut std::collections::HashMap<Id, Id>,
146 pub requested_redraw_at: Option<std::time::Instant>,
147 pub event_listeners: std::collections::HashMap<Id, Vec<EventHandler<'a>>>,
148 pub window_actions: Vec<zenthra_platform::app::WindowAction>,
149 pub active_overlay_stack: Vec<Id>,
150}
151
152impl<'a> Ui<'a> {
153 pub fn new(
154 width: u32,
155 height: u32,
156 scale_factor: f64,
157 font_system: Option<Arc<Mutex<FontSystem>>>,
158 events: Vec<PlatformEvent>,
159 initial_focused_id: Option<Id>,
160 mouse_pos: (f32, f32),
161 mouse_down: bool,
162 scroll_state: &'a mut std::collections::HashMap<Id, (f32, f32)>,
163 cursor_state: &'a mut std::collections::HashMap<Id, usize>,
164 interaction_state: &'a mut std::collections::HashMap<Id, f32>,
165 active_drag: Option<ScrollDrag>,
166 clicked: bool,
167 right_clicked: bool,
168 elapsed_time: f32,
169 layout_cache: &'a std::collections::HashMap<Id, (Rect, u64)>,
170 next_layout_cache: &'a mut std::collections::HashMap<Id, (Rect, u64)>,
171 screen_layout_cache: &'a std::collections::HashMap<Id, Rect>,
172 next_screen_layout_cache: &'a mut std::collections::HashMap<Id, Rect>,
173 widget_window_map: &'a std::collections::HashMap<Id, Id>,
174 next_widget_window_map: &'a mut std::collections::HashMap<Id, Id>,
175 image_sizes: &'a std::collections::HashMap<zenthra_core::ImageSource, (u32, u32)>,
176 ) -> Self {
177 let mouse_x = mouse_pos.0;
178 let mouse_y = mouse_pos.1;
179
180 Self {
181 width: width as f32,
182 height: height as f32,
183 scale_factor: scale_factor as f32,
184 mouse_x,
185 mouse_y,
186 mouse_down,
187 input_events: events,
188 focused_id: initial_focused_id,
189 id_counter: 0,
190 cursor_icon: CursorIcon::Default,
191 draws: Vec::new(),
192 overlays: Vec::new(),
193 cursor_x: 0.0,
194 cursor_y: 0.0,
195 base_x: 0.0,
196 base_y: 0.0,
197 direction: Direction::Column,
198 line_height: 0.0,
199 child_sizes: Vec::new(),
200 child_draw_ranges: Vec::new(),
201 child_origins: Vec::new(),
202 id_log: Vec::new(),
203 id_ranges: Vec::new(),
204 last_w: 0.0,
205 last_h: 0.0,
206 max_x: width as f32,
207 max_y: height as f32,
208 offset_x: 0.0,
209 offset_y: 0.0,
210 font_system,
211 scroll_state,
212 cursor_state,
213 interaction_state,
214 active_drag,
215 clicked,
216 right_clicked,
217 elapsed_time,
218 semantic_nodes: Vec::new(),
219 semantic_stack: Vec::new(),
220 render_mode_stack: vec![zenthra_core::RenderMode::Static],
221 needs_redraw: false,
222 layout_cache,
223 next_layout_cache,
224 screen_layout_cache,
225 next_screen_layout_cache,
226 image_sizes,
227 available_width: width as f32,
228 skip_clip_stack: Vec::new(),
229 current_viewport: Rect::new(0.0, 0.0, width as f32, height as f32),
230 window_overlays: Vec::new(),
231 current_window_id: None,
232 widget_window_map,
233 next_widget_window_map,
234 requested_redraw_at: None,
235 event_listeners: std::collections::HashMap::new(),
236 window_actions: Vec::new(),
237 active_overlay_stack: Vec::new(),
238 }
239 }
240
241 pub fn request_redraw_at(&mut self, instant: std::time::Instant) {
242 if let Some(current) = self.requested_redraw_at {
243 if instant < current {
244 self.requested_redraw_at = Some(instant);
245 }
246 } else {
247 self.requested_redraw_at = Some(instant);
248 }
249 }
250
251 pub fn add_listener<F>(&mut self, id: Id, phase: EventPhase, callback: F)
252 where
253 F: FnMut(&mut EventContext, &WidgetEvent) + 'a,
254 {
255 self.event_listeners.entry(id).or_default().push(EventHandler {
256 phase,
257 callback: Box::new(callback),
258 });
259 }
260
261 pub fn dispatch_event(&mut self, target_id: Id, event: WidgetEvent) {
262 let ancestors = self.semantic_stack.clone();
263
264 let mut ctx = EventContext {
265 target: target_id,
266 current_target: target_id,
267 propagation_stopped: false,
268 };
269
270 for ancestor_id in &ancestors {
272 ctx.current_target = *ancestor_id;
273 if let Some(handlers) = self.event_listeners.get_mut(ancestor_id) {
274 for handler in handlers {
275 if handler.phase == EventPhase::Capture {
276 (handler.callback)(&mut ctx, &event);
277 if ctx.propagation_stopped {
278 return;
279 }
280 }
281 }
282 }
283 }
284
285 ctx.current_target = target_id;
287 if let Some(handlers) = self.event_listeners.get_mut(&target_id) {
288 for handler in handlers {
289 if handler.phase == EventPhase::Target || handler.phase == EventPhase::Bubble {
290 (handler.callback)(&mut ctx, &event);
291 if ctx.propagation_stopped {
292 return;
293 }
294 }
295 }
296 }
297
298 for ancestor_id in ancestors.iter().rev() {
300 ctx.current_target = *ancestor_id;
301 if let Some(handlers) = self.event_listeners.get_mut(ancestor_id) {
302 for handler in handlers {
303 if handler.phase == EventPhase::Bubble {
304 (handler.callback)(&mut ctx, &event);
305 if ctx.propagation_stopped {
306 return;
307 }
308 }
309 }
310 }
311 }
312 }
313
314 pub fn request_redraw_after(&mut self, duration: std::time::Duration) {
315 let target = std::time::Instant::now() + duration;
316 self.request_redraw_at(target);
317 }
318
319 pub fn id(&mut self) -> Id {
320 self.id_counter += 1;
321 Id::from_u64(self.id_counter)
322 }
323
324 pub fn record_layout(&mut self, id: Id, rect: Rect) {
325 let id_count = self.id_counter.saturating_sub(id.raw());
326
327 self.next_layout_cache.insert(id, (rect, id_count));
328 let screen_rect = Rect::new(rect.origin.x + self.offset_x, rect.origin.y + self.offset_y, rect.size.width, rect.size.height);
329 self.next_screen_layout_cache.insert(id, screen_rect);
330 if let Some(win_id) = self.current_window_id {
331 self.next_widget_window_map.insert(id, win_id);
332 }
333 self.id_log.push(id);
334 }
335
336 pub fn is_occluded(&self, id: Id, x: f32, y: f32) -> bool {
337 let our_win_id = self.widget_window_map.get(&id).copied().unwrap_or(id);
338 let our_z = self.interaction_state
339 .get(&Id::from_u64((our_win_id.raw() << 8) | 4))
340 .copied()
341 .unwrap_or(0.0);
342
343 for (&other_id, _) in self.screen_layout_cache {
345 let other_win_id = self.widget_window_map.get(&other_id).copied().unwrap_or(other_id);
346 let modal_key = Id::from_u64((other_win_id.raw() << 8) | 5);
347 let is_modal = self.interaction_state
348 .get(&modal_key)
349 .map(|&v| v > 0.5)
350 .unwrap_or(false);
351
352 if is_modal && other_win_id != our_win_id {
353 return true;
354 }
355 }
356
357 for (&other_id, other_rect) in self.screen_layout_cache {
359 let overlay_key = Id::from_u64((other_id.raw() << 8) | 99);
360 let is_overlay = self.interaction_state.get(&overlay_key).map(|&v| v > 0.5).unwrap_or(false);
361 if is_overlay {
362 if x >= other_rect.origin.x && x <= other_rect.origin.x + other_rect.size.width &&
364 y >= other_rect.origin.y && y <= other_rect.origin.y + other_rect.size.height {
365 if !self.active_overlay_stack.contains(&other_id) {
367 return true; }
369 }
370 }
371 }
372
373 for (&other_id, other_rect) in self.screen_layout_cache {
375 let other_win_id = self.widget_window_map.get(&other_id).copied().unwrap_or(other_id);
376 let other_z_key = Id::from_u64((other_win_id.raw() << 8) | 4);
377 if let Some(&other_z) = self.interaction_state.get(&other_z_key) {
378 if other_win_id != our_win_id && other_z > our_z {
379 if x >= other_rect.origin.x && x <= other_rect.origin.x + other_rect.size.width &&
380 y >= other_rect.origin.y && y <= other_rect.origin.y + other_rect.size.height {
381 return true;
382 }
383 }
384 }
385 }
386 false
387 }
388
389 pub fn is_hovered(&self, id: Id, fallback_x: f32, fallback_y: f32, fallback_w: f32, fallback_h: f32) -> bool {
390 let is_in = if let Some((rect, _)) = self.get_recorded_layout(id) {
391 self.mouse_in_rect(rect.origin.x + self.offset_x, rect.origin.y + self.offset_y, rect.size.width, rect.size.height)
392 } else {
393 self.mouse_in_rect(fallback_x, fallback_y, fallback_w, fallback_h)
394 };
395 let mouse_in_viewport = self.current_viewport.contains(zenthra_core::Point::new(self.mouse_x, self.mouse_y));
396 is_in && mouse_in_viewport && !self.is_occluded(id, self.mouse_x, self.mouse_y)
397 }
398
399 pub fn overlay<F>(&mut self, f: F)
400 where
401 F: FnOnce(&mut Ui),
402 {
403 let already_swapped = self.skip_clip_stack.last().cloned().unwrap_or(false);
404
405 if !already_swapped {
406 std::mem::swap(&mut self.draws, &mut self.overlays);
408 }
409
410 self.skip_clip_stack.push(true);
411 let prev_viewport = self.current_viewport;
412 self.current_viewport = Rect::new(-100000.0, -100000.0, 2000000.0, 2000000.0);
413
414 f(self);
415
416 self.current_viewport = prev_viewport;
417 self.skip_clip_stack.pop();
418
419 if !already_swapped {
420 std::mem::swap(&mut self.draws, &mut self.overlays);
422 }
423 }
424
425 pub fn get_recorded_layout(&self, id: Id) -> Option<(Rect, u64)> {
426 self.layout_cache.get(&id).cloned()
427 }
428
429 pub fn current_render_mode(&self) -> zenthra_core::RenderMode {
430 *self.render_mode_stack.last().unwrap_or(&zenthra_core::RenderMode::Static)
431 }
432
433 pub fn request_redraw(&mut self) {
434 self.needs_redraw = true;
435 }
436
437 pub fn get_max_bounds(&self) -> (f32, f32) {
438 (self.max_x, self.max_y)
439 }
440
441 pub fn row(&mut self) -> ContainerBuilder<'_, 'a> {
442 ContainerBuilder::new(self).row()
443 }
444
445 pub fn column(&mut self) -> ContainerBuilder<'_, 'a> {
446 ContainerBuilder::new(self).column()
447 }
448
449 pub fn container(&mut self) -> ContainerBuilder<'_, 'a> {
450 ContainerBuilder::new(self)
451 }
452
453 pub fn lazy_container(&mut self) -> LazyContainerBuilder<'_, 'a> {
454 LazyContainerBuilder::new(self)
455 }
456
457 pub fn button(&mut self, label: &str) -> crate::button::ButtonBuilder<'_, 'a> {
458 crate::button::ButtonBuilder::new(self, label)
459 }
460
461 pub fn image(&mut self, source: zenthra_core::ImageSource) -> crate::image::ImageBuilder<'_, 'a> {
462 crate::image::ImageBuilder::new(self, source)
463 }
464
465 pub fn spacing(&mut self, size: f32) {
466 let (w, h) = match self.direction {
467 Direction::Column => (0.0, size),
468 Direction::Row => (size, 0.0),
469 Direction::Stack => (0.0, 0.0),
470 };
471 let draw_start = self.draws.len();
472 self.advance(w, h, draw_start);
473 }
474
475 pub fn add_space(&mut self, h: f32) {
478 match self.direction {
479 Direction::Column => self.cursor_y += h,
480 Direction::Row => self.cursor_x += h,
481 Direction::Stack => {}
482 }
483 }
484
485 pub fn set_min_size(&mut self, w: f32, h: f32) {
489 let draw_start = self.draws.len();
490 self.advance(w, h, draw_start);
491 }
492
493
494 pub fn input<'b>(&mut self, buffer: &'b mut String, id: impl std::hash::Hash) -> crate::input::InputBuilder<'_, 'a, 'b> {
495 crate::input::InputBuilder::new(self, buffer).id(id)
496 }
497
498 pub fn slider<'b>(&mut self, value: &'b mut f32, id: impl std::hash::Hash) -> crate::slider::SliderBuilder<'_, 'a, 'b> {
499 crate::slider::SliderBuilder::new(self, value).id(id)
500 }
501
502 pub fn progress_bar(&mut self, value: f32) -> crate::progress_bar::ProgressBarBuilder<'_, 'a> {
503 crate::progress_bar::ProgressBarBuilder::new(self, value)
504 }
505
506 pub fn checkbox<'b>(&mut self, value: &'b mut bool, label: &str) -> crate::controls::checkbox::CheckboxBuilder<'_, 'a, 'b> {
507 crate::controls::checkbox::CheckboxBuilder::new(self, value, label)
508 }
509
510 pub fn toggle<'b>(&mut self, value: &'b mut bool, label: impl Into<Option<&'b str>>) -> crate::controls::toggle::ToggleBuilder<'_, 'a, 'b> {
511 let l: Option<&str> = label.into();
512 crate::controls::toggle::ToggleBuilder::new(self, value, l)
513 }
514
515 pub fn radio<'b, T: PartialEq + Clone>(&mut self, state: &'b mut T, value: T, label: &str) -> crate::controls::radio::RadioBuilder<'_, 'a, 'b, T> {
516 crate::controls::radio::RadioBuilder::new(self, state, value, label)
517 }
518
519 pub fn dropdown<'b, T: PartialEq + Clone + ToString>(&mut self, selected: &'b mut T, options: Vec<T>) -> crate::controls::dropdown::DropdownBuilder<'_, 'a, 'b, T> {
520 crate::controls::dropdown::DropdownBuilder::new(self, selected, options)
521 }
522
523 pub fn text_area<'b>(&mut self, buffer: &'b mut String, id: impl std::hash::Hash) -> crate::text_area::TextAreaBuilder<'_, 'a, 'b> {
524 crate::text_area::TextAreaBuilder::new(self, buffer).id(id)
525 }
526
527 pub fn window<'b>(&mut self, title: &str, is_open: &'b mut bool, pos: &'b mut [f32; 2]) -> crate::window::FloatingWindowBuilder<'_, 'a, 'b> {
528 crate::window::FloatingWindowBuilder::new(self, title, is_open, pos)
529 }
530
531 pub fn menu_bar(&mut self) -> crate::controls::menu::MenuBarBuilder<'_, 'a> {
532 crate::controls::menu::MenuBarBuilder::new(self)
533 }
534
535 pub fn menu(&mut self, label: &str) -> crate::controls::menu::MenuBuilder<'_, 'a> {
536 crate::controls::menu::MenuBuilder::new(self, label)
537 }
538
539 pub fn sub_menu(&mut self, label: &str) -> crate::controls::menu::SubMenuBuilder<'_, 'a> {
540 crate::controls::menu::SubMenuBuilder::new(self, label)
541 }
542
543 pub fn menu_item(&mut self, label: &str) -> crate::controls::menu::MenuItemBuilder<'_, 'a> {
544 crate::controls::menu::MenuItemBuilder::new(self, label)
545 }
546
547 pub fn register_semantic(&mut self, node: zenthra_core::SemanticNode) {
549 if let Some(parent_id) = self.semantic_stack.last() {
551 if let Some(parent) = self.semantic_nodes.iter_mut().find(|n| n.id == *parent_id) {
552 parent.children.push(node.id);
553 }
554 }
555 self.semantic_nodes.push(node);
556 }
557
558 pub fn set_mouse(&mut self, x: f32, y: f32, down: bool) {
559 self.mouse_x = x;
560 self.mouse_y = y;
561 self.mouse_down = down;
562 }
563
564 pub fn mouse_in_rect(&self, x: f32, y: f32, w: f32, h: f32) -> bool {
565 let mx = self.mouse_x;
566 let my = self.mouse_y;
567 mx >= x && mx <= x + w &&
568 my >= y && my <= y + h
569 }
570
571 pub fn is_rect_visible(&self, rect: Rect) -> bool {
572 let rw = rect.size.width;
573 let rh = rect.size.height;
574 let rx = rect.origin.x + self.offset_x;
575 let ry = rect.origin.y + self.offset_y;
576
577 let bleed = 150.0;
579 rx + rw + bleed >= 0.0 && rx - bleed <= self.width &&
580 ry + rh + bleed >= 0.0 && ry - bleed <= self.height
581 }
582
583 pub fn physical_clip(&self, x: f32, y: f32, w: f32, h: f32) -> [f32; 4] {
585 let sf = self.scale_factor;
586 [x * sf, y * sf, w * sf, h * sf]
587 }
588
589 pub fn advance(&mut self, w: f32, h: f32, draw_start: usize) {
592 let draw_end = self.draws.len();
593 self.child_draw_ranges.push((draw_start, draw_end));
594 self.child_sizes.push((w, h));
595 self.child_origins.push((self.cursor_x, self.cursor_y));
596
597 let id_start = self.id_ranges.last().map(|(_, end)| *end).unwrap_or(0);
598 let id_end = self.id_log.len();
599 self.id_ranges.push((id_start, id_end));
600
601 self.last_w = w;
602 self.last_h = h;
603
604 match self.direction {
605 Direction::Column => {
606 self.line_height = self.line_height.max(h);
607 self.cursor_y += h;
608 self.cursor_x = self.base_x;
609 }
610 Direction::Row => {
611 self.cursor_x += w;
612 self.cursor_y = self.base_y;
613 self.line_height = self.line_height.max(h);
614 }
615 Direction::Stack => {
616 self.cursor_x = self.base_x;
617 self.cursor_y = self.base_y;
618 }
619 }
620 }
621
622 pub fn text(&mut self, content: &str) -> TextBuilder<'_, 'a> {
623 TextBuilder::new(self, content)
624 }
625
626 pub fn h1(&mut self, content: &str) -> TextBuilder<'_, 'a> {
627 self.text(content).size(40.0).bold()
628 }
629
630 pub fn h2(&mut self, content: &str) -> TextBuilder<'_, 'a> {
631 self.text(content).size(32.0).bold()
632 }
633
634 pub fn h3(&mut self, content: &str) -> TextBuilder<'_, 'a> {
635 self.text(content).size(24.0).bold()
636 }
637
638 pub fn h4(&mut self, content: &str) -> TextBuilder<'_, 'a> {
639 self.text(content).size(20.0).bold()
640 }
641
642 pub fn card(&mut self) -> crate::containers::card::CardBuilder<'_, 'a> {
643 crate::containers::card::CardBuilder::new(self)
644 }
645
646 pub fn panel<'b>(&mut self) -> crate::containers::panel::PanelBuilder<'_, 'a, 'b> {
647 crate::containers::panel::PanelBuilder::new(self)
648 }
649
650 pub fn card_header<F>(&mut self, title: &str, subtitle: &str, actions: F)
651 where F: FnOnce(&mut Ui) {
652 self.container()
653 .full_width()
654 .row()
655 .halign(zenthra_core::Align::SpaceBetween)
656 .valign(zenthra_core::Align::Center)
657 .padding_bottom(8.0)
658 .show(|ui| {
659 ui.container()
660 .column()
661 .show(|ui| {
662 ui.text(title)
663 .size(16.0)
664 .weight(crate::text::FontWeight::Bold)
665 .color(Color::WHITE)
666 .show();
667 if !subtitle.is_empty() {
668 ui.spacing(2.0);
669 ui.text(subtitle)
670 .size(12.0)
671 .color(Color::rgb(0.6, 0.6, 0.6))
672 .show();
673 }
674 });
675
676 ui.container()
677 .row()
678 .valign(zenthra_core::Align::Center)
679 .show(actions);
680 });
681
682 self.card_divider();
683 }
684
685 pub fn card_footer<F>(&mut self, f: F)
686 where F: FnOnce(&mut Ui) {
687 self.card_divider();
688 self.container()
689 .full_width()
690 .row()
691 .show(f);
692 }
693
694 pub fn card_divider(&mut self) {
695 self.spacing(8.0);
696 self.container()
697 .full_width()
698 .height(1.0)
699 .bg(Color::rgb(0.2, 0.2, 0.25))
700 .show(|_| {});
701 self.spacing(8.0);
702 }
703
704 pub fn stack(&mut self) -> crate::layout::StackBuilder<'_, 'a> {
705 crate::layout::StackBuilder::new(self)
706 }
707}
708
709pub fn get_shaper(font_system: &Arc<Mutex<FontSystem>>) -> CosmicFontProvider {
711 CosmicFontProvider::new_with_system(font_system.clone())
712}
713
714#[derive(Debug, Clone, PartialEq)]
715pub enum WidgetEvent {
716 Click,
717 Hover(bool),
718 Scroll(f32, f32),
719 Change(EventValue),
720}
721
722#[derive(Debug, Clone, PartialEq)]
723pub enum EventValue {
724 Bool(bool),
725 Float(f32),
726 String(String),
727}
728
729#[derive(Debug, Clone)]
730pub struct EventContext {
731 pub target: Id,
732 pub current_target: Id,
733 pub propagation_stopped: bool,
734}
735
736impl EventContext {
737 pub fn stop_propagation(&mut self) {
738 self.propagation_stopped = true;
739 }
740}
741
742#[derive(Debug, Clone, Copy, PartialEq, Eq)]
743pub enum EventPhase {
744 Capture,
745 Target,
746 Bubble,
747}
748
749pub struct EventHandler<'a> {
750 pub phase: EventPhase,
751 pub callback: Box<dyn FnMut(&mut EventContext, &WidgetEvent) + 'a>,
752}
753
754
755#[cfg(test)]
756mod tests {
757 use super::*;
758 use std::collections::HashMap;
759
760 #[test]
761 fn test_hover_viewport_bounds() {
762 let mut scroll_state = HashMap::new();
763 let mut cursor_state = HashMap::new();
764 let mut interaction_state = HashMap::new();
765 let layout_cache = HashMap::new();
766 let mut next_layout_cache = HashMap::new();
767 let screen_layout_cache = HashMap::new();
768 let mut next_screen_layout_cache = HashMap::new();
769 let widget_window_map = HashMap::new();
770 let mut next_widget_window_map = HashMap::new();
771 let image_sizes = HashMap::new();
772
773 let mut ui = Ui::new(
775 100, 100, 1.0, None, Vec::new(), None,
776 (50.0, 50.0), false,
777 &mut scroll_state,
778 &mut cursor_state,
779 &mut interaction_state,
780 None, false, 0.0,
781 &layout_cache,
782 &mut next_layout_cache,
783 &screen_layout_cache,
784 &mut next_screen_layout_cache,
785 &widget_window_map,
786 &mut next_widget_window_map,
787 &image_sizes,
788 );
789
790 let test_id = Id::from_u64(12345);
791
792 assert!(ui.is_hovered(test_id, 40.0, 40.0, 20.0, 20.0));
795
796 ui.current_viewport = Rect::new(0.0, 0.0, 30.0, 30.0);
798 assert!(!ui.is_hovered(test_id, 40.0, 40.0, 20.0, 20.0));
802 }
803
804 #[test]
805 fn test_event_propagation() {
806 let mut scroll_state = HashMap::new();
807 let mut cursor_state = HashMap::new();
808 let mut interaction_state = HashMap::new();
809 let layout_cache = HashMap::new();
810 let mut next_layout_cache = HashMap::new();
811 let screen_layout_cache = HashMap::new();
812 let mut next_screen_layout_cache = HashMap::new();
813 let widget_window_map = HashMap::new();
814 let mut next_widget_window_map = HashMap::new();
815 let image_sizes = HashMap::new();
816
817 let mut ui = Ui::new(
818 100, 100, 1.0, None, Vec::new(), None,
819 (50.0, 50.0), false,
820 &mut scroll_state,
821 &mut cursor_state,
822 &mut interaction_state,
823 None, false, 0.0,
824 &layout_cache,
825 &mut next_layout_cache,
826 &screen_layout_cache,
827 &mut next_screen_layout_cache,
828 &widget_window_map,
829 &mut next_widget_window_map,
830 &image_sizes,
831 );
832
833 let parent_id = Id::from_u64(1);
834 let middle_id = Id::from_u64(2);
835 let child_id = Id::from_u64(3);
836
837 ui.semantic_stack = vec![parent_id, middle_id];
839
840 use std::cell::RefCell;
841 use std::rc::Rc;
842 let trace = Rc::new(RefCell::new(Vec::new()));
843
844 {
845 let trace_capture = trace.clone();
846 ui.add_listener(parent_id, EventPhase::Capture, move |_, _| {
847 trace_capture.borrow_mut().push("parent_capture".to_string());
848 });
849 }
850 {
851 let trace_bubble = trace.clone();
852 ui.add_listener(parent_id, EventPhase::Bubble, move |_, _| {
853 trace_bubble.borrow_mut().push("parent_bubble".to_string());
854 });
855 }
856 {
857 let trace_capture = trace.clone();
858 ui.add_listener(middle_id, EventPhase::Capture, move |_, _| {
859 trace_capture.borrow_mut().push("middle_capture".to_string());
860 });
861 }
862 {
863 let trace_bubble = trace.clone();
864 ui.add_listener(middle_id, EventPhase::Bubble, move |_, _| {
865 trace_bubble.borrow_mut().push("middle_bubble".to_string());
866 });
867 }
868 {
869 let trace_target = trace.clone();
870 ui.add_listener(child_id, EventPhase::Target, move |_, _| {
871 trace_target.borrow_mut().push("child_target".to_string());
872 });
873 }
874
875 ui.dispatch_event(child_id, WidgetEvent::Click);
876
877 assert_eq!(
878 *trace.borrow(),
879 vec![
880 "parent_capture".to_string(),
881 "middle_capture".to_string(),
882 "child_target".to_string(),
883 "middle_bubble".to_string(),
884 "parent_bubble".to_string(),
885 ]
886 );
887
888 trace.borrow_mut().clear();
890 ui.event_listeners.clear();
891
892 {
893 let trace_capture = trace.clone();
894 ui.add_listener(parent_id, EventPhase::Capture, move |_, _| {
895 trace_capture.borrow_mut().push("parent_capture".to_string());
896 });
897 }
898 {
899 let trace_capture = trace.clone();
900 ui.add_listener(middle_id, EventPhase::Capture, move |ctx, _| {
901 trace_capture.borrow_mut().push("middle_capture".to_string());
902 ctx.stop_propagation();
903 });
904 }
905 {
906 let trace_target = trace.clone();
907 ui.add_listener(child_id, EventPhase::Target, move |_, _| {
908 trace_target.borrow_mut().push("child_target".to_string());
909 });
910 }
911
912 ui.dispatch_event(child_id, WidgetEvent::Click);
913
914 assert_eq!(
915 *trace.borrow(),
916 vec![
917 "parent_capture".to_string(),
918 "middle_capture".to_string(),
919 ]
920 );
921 }
922}