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
38/// Requests the renderer to blur the scene behind this rect.
39/// The renderer will copy the current offscreen framebuffer region,
40/// run a Dual-Kawase blur pass, and composite it as the background
41/// before drawing the tinted/bordered rect on top.
42pub struct BackdropBlurDraw {
43    /// Position and size in logical pixels
44    pub x:      f32,
45    pub y:      f32,
46    pub width:  f32,
47    pub height: f32,
48    /// Per-corner radius: [top-left, top-right, bottom-right, bottom-left]
49    pub radius: [f32; 4],
50    /// Blur radius in logical pixels — maps to Kawase offset parameter
51    pub blur_radius: f32,
52    /// Clipping rect [x, y, w, h] in logical pixels
53    pub clip_rect: [f32; 4],
54    
55    // CSS-like backdrop filters:
56    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    /// Glassmorphism backdrop blur
80    BackdropBlur(BackdropBlurDraw),
81    /// Custom post-process shader effect
82    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        // 1. Capture Phase
271        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        // 2. Target Phase
286        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        // 3. Bubble Phase
299        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        // Check active modal blocking
344        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        // Check active overlays occlusion (if mouse is inside an overlay, block background widgets)
358        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 the mouse is inside this overlay screen rect
363                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                    // And if we are NOT inside this overlay stack context
366                    if !self.active_overlay_stack.contains(&other_id) {
367                        return true; // We are occluded by the overlay!
368                    }
369                }
370            }
371        }
372
373        // Check z-order occlusion
374        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            // Swap draws and overlays so that any pushes to self.draws go to self.overlays
407            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            // Swap back to restore self.draws and commit new draws to self.overlays
421            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    /// Moves the cursor down by `h` pixels WITHOUT registering a layout child.
476    /// Use this inside a lazy/virtual list to offset visible items below invisible ones.
477    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    /// Registers an invisible zero-draw child with the given size.
486    /// This forces the parent container's layout engine to account for this size
487    /// when calculating total content height for scroll area sizing.
488    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    /// Registers a widget in the semantic tree.
548    pub fn register_semantic(&mut self, node: zenthra_core::SemanticNode) {
549        // If we are inside a container, add this node as a child of the current parent
550        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        // Bleed allows items slightly off-screen to remain drawn (prevents flickering)
578        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    /// Converts a logical [x, y, w, h] rect to physical pixels for GPU clipping.
584    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    /// Called by widgets after pushing their draws.
590    /// Records draw range and advances cursor.
591    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
709/// A helper to get the default Zentype shaper for the current font system.
710pub 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        // 1. Create a UI with mouse position at (50.0, 50.0)
774        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        // Standard bounds: (40.0, 40.0, 20.0, 20.0). Mouse at (50.0, 50.0) is inside.
793        // Initially, current_viewport is (0.0, 0.0, 100.0, 100.0) which covers the mouse.
794        assert!(ui.is_hovered(test_id, 40.0, 40.0, 20.0, 20.0));
795
796        // 2. Set current_viewport to not cover the mouse (e.g. scrolled out of view)
797        ui.current_viewport = Rect::new(0.0, 0.0, 30.0, 30.0);
798        // The mouse (50, 50) is now outside the visible viewport.
799        // Even though the fallback bounds (40, 40, 20, 20) technically contain the mouse,
800        // the viewport check should restrict the hover.
801        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        // Simulate hierarchy
838        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        // Test stopping propagation in middle_capture phase
889        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}