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 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 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 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 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 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 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 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 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 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 pub fn register_semantic(&mut self, node: zenthra_core::SemanticNode) {
485 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 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 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 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
645pub 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 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 assert!(ui.is_hovered(test_id, 40.0, 40.0, 20.0, 20.0));
731
732 ui.current_viewport = Rect::new(0.0, 0.0, 30.0, 30.0);
734 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 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 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}