Skip to main content

zenthra_widgets/
ui.rs

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