1use geometry_core::{Rect, Transform};
2use layout_core::{LayoutError, LayoutStyle, NodeId};
3use platform_core::{Event, Key, NamedKey, PointerButton, PointerSource, ScrollDelta};
4use reactive_core::{Effect, RwSignal, effect, signal};
5use renderer_core::RectStyle;
6use ui_tree::{Component, EventResult, RenderNode};
7
8use crate::child_host::{ChildSlot, DynHost};
9use crate::context::{new_container, track_layout};
10use crate::drag::DragGesture;
11use crate::focus::{self, FocusId};
12use crate::layout_item::{LayoutItem, TrackedChildren, register_container};
13use crate::pointer::dispatch_container_event;
14use crate::press::PressGesture;
15
16pub struct StyledContainer {
17 node: NodeId,
18 rect: RwSignal<Rect>,
19 style: Box<dyn Fn(Rect) -> RectStyle>,
20 hover_style: Option<Box<dyn Fn(Rect) -> RectStyle>>,
22 is_hovered: RwSignal<bool>,
23 active_style: Option<Box<dyn Fn(Rect) -> RectStyle>>,
26 is_active: RwSignal<bool>,
27 opacity: Box<dyn Fn() -> f32>,
29 transform: Box<dyn Fn(Rect) -> Option<[f32; 6]>>,
31 children: TrackedChildren,
32 dyn_host: Option<DynHost>,
35 press: PressGesture,
37 drag: DragGesture,
39 on_hover: Option<Box<dyn Fn(bool)>>,
41 on_scroll: Option<Box<dyn Fn(f32, f32)>>,
43 on_key: Option<Box<dyn Fn(&Key)>>,
46 focus_id: Option<FocusId>,
49 _focus_effect: Option<Effect>,
51}
52
53impl StyledContainer {
54 pub fn new(
55 layout_style: LayoutStyle,
56 style: impl Fn(Rect) -> RectStyle + 'static,
57 children: Vec<Box<dyn LayoutItem>>,
58 ) -> Result<Self, LayoutError> {
59 let (node, rect, children) = register_container(layout_style, children)?;
60 Ok(Self {
61 node,
62 rect,
63 style: Box::new(style),
64 hover_style: None,
65 is_hovered: signal(false),
66 active_style: None,
67 is_active: signal(false),
68 opacity: Box::new(|| 1.0),
69 transform: Box::new(|_| None),
70 children,
71 dyn_host: None,
72 press: PressGesture::default(),
73 drag: DragGesture::default(),
74 on_hover: None,
75 on_scroll: None,
76 on_key: None,
77 focus_id: None,
78 _focus_effect: None,
79 })
80 }
81
82 pub fn from_slots(
86 layout_style: LayoutStyle,
87 style: impl Fn(Rect) -> RectStyle + 'static,
88 slots: Vec<ChildSlot>,
89 ) -> Result<Self, LayoutError> {
90 let node = new_container(layout_style, &[])?;
91 let rect = track_layout(node).expect("new_container always registers a signal");
92 let dyn_host = DynHost::build(node, slots)?;
93 Ok(Self {
94 node,
95 rect,
96 style: Box::new(style),
97 hover_style: None,
98 is_hovered: signal(false),
99 active_style: None,
100 is_active: signal(false),
101 opacity: Box::new(|| 1.0),
102 transform: Box::new(|_| None),
103 children: Vec::new(),
104 dyn_host: Some(dyn_host),
105 press: PressGesture::default(),
106 drag: DragGesture::default(),
107 on_hover: None,
108 on_scroll: None,
109 on_key: None,
110 focus_id: None,
111 _focus_effect: None,
112 })
113 }
114
115 fn dispatch_children(&mut self, event: &Event) -> EventResult {
116 match &self.dyn_host {
117 Some(host) => host.dispatch(event),
118 None => dispatch_container_event(&mut self.children, event),
119 }
120 }
121
122 pub fn with_opacity(mut self, opacity: impl Fn() -> f32 + 'static) -> Self {
123 self.opacity = Box::new(opacity);
124 self
125 }
126
127 pub fn with_transform(
130 mut self,
131 transform: impl Fn(Rect) -> Option<[f32; 6]> + 'static,
132 ) -> Self {
133 self.transform = Box::new(transform);
134 self
135 }
136
137 pub fn on_hover_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
140 self.hover_style = Some(Box::new(f));
141 self
142 }
143
144 pub fn on_active_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
148 self.active_style = Some(Box::new(f));
149 self
150 }
151
152 fn set_active(&self, active: bool) {
155 if self.active_style.is_some() && self.is_active.get() != active {
156 self.is_active.set(active);
157 }
158 }
159
160 pub fn on_press(mut self, f: impl Fn() + 'static) -> Self {
163 self.press.set(f);
164 self.mark_interactive();
165 self
166 }
167
168 pub fn on_alt_press(mut self, f: impl Fn(PointerButton) + 'static) -> Self {
175 self.press.set_alt_press(f);
176 self.mark_interactive();
177 self
178 }
179
180 pub fn on_long_press(mut self, f: impl Fn() + 'static) -> Self {
185 self.press.set_long_press(f);
186 self.mark_interactive();
187 self
188 }
189
190 pub fn on_drag_end(mut self, f: impl Fn(f32, f32) + 'static) -> Self {
201 self.drag.set_end(f);
202 self.mark_interactive();
203 self
204 }
205
206 pub fn on_drag(mut self, f: impl Fn(f32, f32) + 'static) -> Self {
207 self.drag.set(f);
208 self.mark_interactive();
209 self
210 }
211
212 fn mark_interactive(&self) {
216 crate::input_region::register_interactive(self.node, self.rect.read_only());
217 }
218
219 pub fn on_hover(mut self, f: impl Fn(bool) + 'static) -> Self {
226 self.on_hover = Some(Box::new(f));
227 self.mark_interactive();
228 self
229 }
230
231 pub fn on_scroll(mut self, f: impl Fn(f32, f32) + 'static) -> Self {
239 self.on_scroll = Some(Box::new(f));
240 self.mark_interactive();
241 self
242 }
243
244 pub fn on_key(mut self, f: impl Fn(&Key) + 'static) -> Self {
247 self.on_key = Some(Box::new(f));
248 self
249 }
250
251 pub fn on_focus(mut self, f: impl Fn(bool) + 'static) -> Self {
255 let id = *self.focus_id.get_or_insert_with(focus::next_id);
256 focus::register(id);
257 let last = std::rc::Rc::new(std::cell::Cell::new(focus::is_focused(id)));
259 self._focus_effect = Some(effect(move || {
260 let now = focus::is_focused(id);
261 if now != last.get() {
262 last.set(now);
263 f(now);
264 }
265 }));
266 self
267 }
268}
269
270impl LayoutItem for StyledContainer {
271 fn layout_node(&self) -> NodeId {
272 self.node
273 }
274}
275
276impl Component for StyledContainer {
277 fn view(&self) -> RenderNode {
278 let r = self.rect.get();
279 let style = if let Some(active) = &self.active_style
282 && self.is_active.get()
283 {
284 active
285 } else if let Some(hover) = &self.hover_style
286 && self.is_hovered.get()
287 {
288 hover
289 } else {
290 &self.style
291 };
292 let background = RenderNode::rect(
293 Rect {
294 x: r.x,
295 y: r.y,
296 width: r.width,
297 height: r.height,
298 },
299 style(r),
300 );
301 let content = match &self.dyn_host {
302 Some(host) => {
303 RenderNode::group(std::iter::once(background).chain(host.child_boundaries()))
304 }
305 None => RenderNode::group(
306 std::iter::once(background)
307 .chain(self.children.iter().map(|c| c.segment.boundary())),
308 ),
309 };
310 let opacity = (self.opacity)();
311 let composed = if opacity < 1.0 {
312 RenderNode::layer(opacity, 0.0, [content])
313 } else {
314 content
315 };
316 match (self.transform)(r) {
317 Some(matrix) => RenderNode::transform_with(matrix, [composed]),
318 None => composed,
319 }
320 }
321
322 fn on_event(&mut self, event: &Event) -> EventResult {
323 if !self.press.is_set()
325 && !self.drag.is_set()
326 && self.hover_style.is_none()
327 && self.active_style.is_none()
328 && self.on_hover.is_none()
329 && self.on_scroll.is_none()
330 && self.on_key.is_none()
331 && self.focus_id.is_none()
332 {
333 return self.dispatch_children(event);
334 }
335 let rect = self.rect.get();
336 match event {
337 Event::PointerMoved { x, y, source } => {
341 self.press.track_move(event);
342 let dragged = self.drag.moved(event, rect) == EventResult::Handled;
343 let child = self.dispatch_children(event);
344 let inside = rect.contains(*x as f32, *y as f32);
345 if !inside {
347 self.set_active(false);
348 }
349 let tracks_hover = self.hover_style.is_some()
350 || self.on_hover.is_some()
351 || self.on_scroll.is_some();
352 if tracks_hover
353 && matches!(source, PointerSource::Mouse)
354 && inside != self.is_hovered.get()
355 {
356 self.is_hovered.set(inside);
357 if let Some(cb) = &self.on_hover {
358 cb(inside);
359 }
360 return EventResult::Handled;
361 }
362 if dragged { EventResult::Handled } else { child }
363 }
364 Event::PointerPressed { x, y, button, .. } => {
366 let primary = *button == PointerButton::Primary;
369 if !primary && !self.press.wants_alt() {
370 return self.dispatch_children(event);
371 }
372 if self.dispatch_children(event) == EventResult::Handled {
373 self.press.cancel();
374 self.drag.end(None);
375 return EventResult::Handled;
376 }
377 if primary && rect.contains(*x as f32, *y as f32) {
379 self.set_active(true);
380 }
381 let focused = match self.focus_id {
383 Some(id) if primary && rect.contains(*x as f32, *y as f32) => {
384 focus::request(id);
385 true
386 }
387 _ => false,
388 };
389 let tapped =
390 self.press.is_set() && self.press.arm(event, rect) == EventResult::Handled;
391 let dragged = primary
392 && self.drag.is_set()
393 && self.drag.press(event, rect) == EventResult::Handled;
394 if tapped || dragged || focused {
395 EventResult::Handled
396 } else {
397 EventResult::Ignored
398 }
399 }
400 Event::PointerReleased { button, .. } => {
401 let primary = *button == PointerButton::Primary;
402 if !primary && !self.press.wants_alt() {
403 return self.dispatch_children(event);
404 }
405 if primary {
407 self.set_active(false);
408 }
409 if self.dispatch_children(event) == EventResult::Handled {
410 self.press.cancel();
411 self.drag.end(None);
412 return EventResult::Handled;
413 }
414 let released_at = match event {
417 Event::PointerReleased { x, y, .. } => {
418 Some((*x as f32 - rect.x, *y as f32 - rect.y))
419 }
420 _ => None,
421 };
422 let dragged = primary && self.drag.end(released_at);
423 let tapped =
424 self.press.is_set() && self.press.release(event, rect) == EventResult::Handled;
425 if tapped || dragged {
426 EventResult::Handled
427 } else {
428 EventResult::Ignored
429 }
430 }
431 Event::CursorLeft => {
432 self.press.cancel();
433 self.drag.end(None);
434 self.set_active(false);
435 let tracks_hover = self.hover_style.is_some()
436 || self.on_hover.is_some()
437 || self.on_scroll.is_some();
438 if tracks_hover && self.is_hovered.get() {
439 self.is_hovered.set(false);
440 if let Some(cb) = &self.on_hover {
441 cb(false);
442 }
443 }
444 self.dispatch_children(event)
445 }
446 Event::Scrolled { delta } => {
448 if self.dispatch_children(event) == EventResult::Handled {
449 return EventResult::Handled;
450 }
451 let Some(cb) = &self.on_scroll else {
452 return EventResult::Ignored;
453 };
454 if !self.is_hovered.get() {
455 return EventResult::Ignored;
456 }
457 let (dx, dy) = match delta {
458 ScrollDelta::Lines { x, y } => (*x * 20.0, *y * 20.0),
459 ScrollDelta::Pixels { x, y } => (*x, *y),
460 };
461 cb(dx, dy);
462 EventResult::Handled
463 }
464 Event::KeyPressed { key, modifiers } => {
466 if let Some(id) = self.focus_id
468 && focus::is_focused(id)
469 && matches!(key, Key::Named(NamedKey::Tab))
470 {
471 if modifiers.is_shift {
472 focus::focus_prev();
473 } else {
474 focus::focus_next();
475 }
476 return EventResult::Handled;
477 }
478 if let Some(cb) = &self.on_key {
479 cb(key);
480 }
481 self.dispatch_children(event)
482 }
483 _ => self.dispatch_children(event),
484 }
485 }
486
487 fn debug_name(&self) -> &'static str {
488 "StyledContainer"
489 }
490}
491
492impl Drop for StyledContainer {
493 fn drop(&mut self) {
494 self._focus_effect.take();
496 if let Some(id) = self.focus_id {
497 focus::unregister(id);
498 }
499 crate::input_region::unregister_interactive(self.node);
500 }
501}
502
503pub fn box_transform(
507 rect: Rect,
508 rotate_deg: f32,
509 scale_x: f32,
510 scale_y: f32,
511 translate_x: f32,
512 translate_y: f32,
513) -> Option<[f32; 6]> {
514 if rotate_deg == 0.0
515 && scale_x == 1.0
516 && scale_y == 1.0
517 && translate_x == 0.0
518 && translate_y == 0.0
519 {
520 return None;
521 }
522 let cx = rect.x + rect.width / 2.0;
523 let cy = rect.y + rect.height / 2.0;
524 let matrix = Transform::rotate_around(rotate_deg, cx, cy)
525 .then(Transform::scale_around(scale_x, scale_y, cx, cy))
526 .then(Transform::translate(translate_x, translate_y));
527 Some(matrix.to_array())
528}
529
530#[cfg(test)]
531mod tests {
532 use crate::context::reset_layout_runtime;
533 use std::cell::Cell;
534 use std::rc::Rc;
535
536 use layout_core::AvailableSpace;
537 use platform_core::{PointerButton, PointerSource};
538 use renderer_core::{Color, ShapeStyle};
539 use theme_core::{Theme, ThemeTokens, set_theme, use_theme};
540
541 use super::*;
542
543 #[test]
544 fn box_transform_identity_is_none() {
545 let r = Rect {
546 x: 0.0,
547 y: 0.0,
548 width: 10.0,
549 height: 10.0,
550 };
551 assert!(box_transform(r, 0.0, 1.0, 1.0, 0.0, 0.0).is_none());
552 }
553
554 #[test]
555 fn box_transform_scale_pivots_on_center() {
556 let r = Rect {
557 x: 0.0,
558 y: 0.0,
559 width: 100.0,
560 height: 100.0,
561 };
562 assert_eq!(
564 box_transform(r, 0.0, 2.0, 2.0, 0.0, 0.0).unwrap(),
565 [2.0, 0.0, 0.0, 2.0, -50.0, -50.0]
566 );
567 }
568
569 #[test]
570 fn box_transform_translate_offsets_origin() {
571 let r = Rect {
572 x: 0.0,
573 y: 0.0,
574 width: 10.0,
575 height: 10.0,
576 };
577 assert_eq!(
578 box_transform(r, 0.0, 1.0, 1.0, 8.0, -4.0).unwrap(),
579 [1.0, 0.0, 0.0, 1.0, 8.0, -4.0]
580 );
581 }
582 use crate::container::Container;
583 use crate::context::{compute_layout, track_layout};
584
585 fn press(x: f64, y: f64, source: PointerSource) -> Event {
586 Event::PointerPressed {
587 x,
588 y,
589 button: PointerButton::Primary,
590 source,
591 }
592 }
593 fn release(x: f64, y: f64, source: PointerSource) -> Event {
594 Event::PointerReleased {
595 x,
596 y,
597 button: PointerButton::Primary,
598 source,
599 }
600 }
601
602 #[test]
603 fn on_hover_fires_on_enter_and_leave() {
604 let seen: Rc<Cell<Option<bool>>> = Rc::new(Cell::new(None));
605 let sink = seen.clone();
606 reset_layout_runtime();
607 let inner = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![]).unwrap();
608 let mut card = StyledContainer::new(
609 LayoutStyle::new().flex_column().width(100.0).height(100.0),
610 |_r| RectStyle::default(),
611 vec![Box::new(inner)],
612 )
613 .unwrap()
614 .on_hover(move |h| sink.set(Some(h)));
615 let node = card.layout_node();
616 compute_layout(
617 node,
618 AvailableSpace::Definite(100.0),
619 AvailableSpace::Definite(100.0),
620 )
621 .unwrap();
622
623 card.on_event(&Event::PointerMoved {
624 x: 50.0,
625 y: 50.0,
626 source: PointerSource::Mouse,
627 });
628 assert_eq!(seen.get(), Some(true), "entering fires on_hover(true)");
629 card.on_event(&Event::CursorLeft);
630 assert_eq!(seen.get(), Some(false), "leaving fires on_hover(false)");
631 }
632
633 #[test]
634 fn on_scroll_fires_only_while_hovered_and_normalises_lines() {
635 let seen: Rc<Cell<(f32, f32)>> = Rc::new(Cell::new((0.0, 0.0)));
636 let sink = seen.clone();
637 reset_layout_runtime();
638 let inner = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![]).unwrap();
639 let mut card = StyledContainer::new(
640 LayoutStyle::new().flex_column().width(100.0).height(100.0),
641 |_r| RectStyle::default(),
642 vec![Box::new(inner)],
643 )
644 .unwrap()
645 .on_scroll(move |dx, dy| sink.set((dx, dy)));
646 let node = card.layout_node();
647 compute_layout(
648 node,
649 AvailableSpace::Definite(100.0),
650 AvailableSpace::Definite(100.0),
651 )
652 .unwrap();
653
654 assert_eq!(
656 card.on_event(&Event::Scrolled {
657 delta: ScrollDelta::Pixels { x: 0.0, y: -30.0 },
658 }),
659 EventResult::Ignored,
660 "a wheel event outside the box is ignored"
661 );
662 assert_eq!(seen.get(), (0.0, 0.0));
663
664 card.on_event(&Event::PointerMoved {
665 x: 50.0,
666 y: 50.0,
667 source: PointerSource::Mouse,
668 });
669 assert_eq!(
670 card.on_event(&Event::Scrolled {
671 delta: ScrollDelta::Pixels { x: 0.0, y: -30.0 },
672 }),
673 EventResult::Handled,
674 "hovering makes the wheel ours"
675 );
676 assert_eq!(seen.get(), (0.0, -30.0));
677
678 card.on_event(&Event::Scrolled {
680 delta: ScrollDelta::Lines { x: 0.0, y: 3.0 },
681 });
682 assert_eq!(seen.get(), (0.0, 60.0));
683
684 card.on_event(&Event::CursorLeft);
686 assert_eq!(
687 card.on_event(&Event::Scrolled {
688 delta: ScrollDelta::Pixels { x: 0.0, y: -10.0 },
689 }),
690 EventResult::Ignored,
691 "the wheel is released once the pointer leaves"
692 );
693 assert_eq!(seen.get(), (0.0, 60.0), "the handler did not fire again");
694 }
695
696 #[test]
697 fn on_key_fires_on_key_press() {
698 let count = Rc::new(Cell::new(0u32));
699 let sink = count.clone();
700 reset_layout_runtime();
701 let inner = Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap();
702 let mut card = StyledContainer::new(
703 LayoutStyle::new().flex_column(),
704 |_r| RectStyle::default(),
705 vec![Box::new(inner)],
706 )
707 .unwrap()
708 .on_key(move |_k| sink.set(sink.get() + 1));
709 card.on_event(&Event::KeyPressed {
710 key: Key::Char('a'),
711 modifiers: platform_core::ModifiersState::default(),
712 });
713 assert_eq!(count.get(), 1, "a key press fires on_key");
714 }
715
716 #[derive(Clone)]
717 struct TestTheme(Color);
718 impl Theme for TestTheme {
719 fn as_any(&self) -> &dyn std::any::Any {
720 self
721 }
722 }
723 impl ThemeTokens for TestTheme {
724 fn primary(&self) -> Color {
725 self.0
726 }
727 fn on_primary(&self) -> Color {
728 Color::WHITE
729 }
730 }
731
732 #[test]
734 fn theme_button_click_force_tick_no_panic() {
735 set_theme(TestTheme(Color::RED));
736
737 reset_layout_runtime();
738 let btn = StyledContainer::new(
740 LayoutStyle::new().width(50.0).height(30.0),
741 |_r| RectStyle::default(),
742 vec![],
743 )
744 .unwrap()
745 .on_press(move || set_theme(TestTheme(Color::GREEN)));
746 let btn_node = btn.layout_node();
747 let inner = Container::new(
748 LayoutStyle::new().flex_column().width(200.0).height(100.0),
749 vec![Box::new(btn)],
750 )
751 .unwrap();
752 let card = StyledContainer::new(
753 LayoutStyle::new().flex_column().width(200.0).height(100.0),
754 |_r| RectStyle::default().with_fill(use_theme::<TestTheme>().0),
755 vec![Box::new(inner)],
756 )
757 .unwrap();
758 let card_node = card.layout_node();
759 compute_layout(
760 card_node,
761 AvailableSpace::Definite(200.0),
762 AvailableSpace::Definite(100.0),
763 )
764 .unwrap();
765 let br = track_layout(btn_node).unwrap().get();
766
767 let mut tree = crate::ComponentList::new(card);
768 let _ = tree.commands();
769
770 reactive_core::begin_batch();
771 let handled = tree.on_event(&Event::PointerPressed {
772 x: (br.x + br.width / 2.0) as f64,
773 y: (br.y + br.height / 2.0) as f64,
774 button: PointerButton::Primary,
775 source: PointerSource::Mouse,
776 });
777 if handled == EventResult::Handled {
778 tree.bump_force_ticks();
779 reactive_core::end_batch();
780 reactive_core::begin_batch();
781 }
782 let _ = tree.commands();
783 reactive_core::end_batch();
784 }
785
786 #[test]
788 fn on_press_fires_on_tap_not_press() {
789 let flag = Rc::new(Cell::new(false));
790 let f = flag.clone();
791 reset_layout_runtime();
792 let mut card = StyledContainer::new(
793 LayoutStyle::new().flex_column().width(200.0).height(100.0),
794 |_r| RectStyle::default(),
795 vec![],
796 )
797 .unwrap()
798 .on_press(move || f.set(true));
799 compute_layout(
800 card.layout_node(),
801 AvailableSpace::Definite(200.0),
802 AvailableSpace::Definite(100.0),
803 )
804 .unwrap();
805
806 assert_eq!(
807 card.on_event(&press(100.0, 50.0, PointerSource::Mouse)),
808 EventResult::Handled
809 );
810 assert!(!flag.get(), "press alone must not fire on_press");
811 assert_eq!(
812 card.on_event(&release(100.0, 50.0, PointerSource::Mouse)),
813 EventResult::Handled
814 );
815 assert!(flag.get(), "release inside the box fires on_press");
816 }
817
818 #[test]
821 fn on_long_press_fires_after_threshold_not_on_quick_release() {
822 let long_flag = Rc::new(Cell::new(false));
823 let tap_flag = Rc::new(Cell::new(false));
824 let lf = long_flag.clone();
825 let tf = tap_flag.clone();
826 reset_layout_runtime();
827 let mut card = StyledContainer::new(
828 LayoutStyle::new().flex_column().width(200.0).height(100.0),
829 |_r| RectStyle::default(),
830 vec![],
831 )
832 .unwrap()
833 .on_press(move || tf.set(true))
834 .on_long_press(move || lf.set(true));
835 compute_layout(
836 card.layout_node(),
837 AvailableSpace::Definite(200.0),
838 AvailableSpace::Definite(100.0),
839 )
840 .unwrap();
841
842 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
844 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
845 assert!(tap_flag.get(), "a quick release still fires on_press");
846 assert!(
847 !long_flag.get(),
848 "a quick release must not fire on_long_press"
849 );
850
851 tap_flag.set(false);
853 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
854 std::thread::sleep(std::time::Duration::from_millis(550));
855 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
856 assert!(
857 long_flag.get(),
858 "a release after the threshold fires on_long_press"
859 );
860 assert!(!tap_flag.get(), "a long press must not also fire on_press");
861 }
862
863 fn press_with(x: f64, y: f64, button: PointerButton) -> Event {
864 Event::PointerPressed {
865 x,
866 y,
867 button,
868 source: PointerSource::Mouse,
869 }
870 }
871 fn release_with(x: f64, y: f64, button: PointerButton) -> Event {
872 Event::PointerReleased {
873 x,
874 y,
875 button,
876 source: PointerSource::Mouse,
877 }
878 }
879
880 fn laid_out_box() -> StyledContainer {
881 reset_layout_runtime();
882 StyledContainer::new(
883 LayoutStyle::new().width(100.0).height(100.0),
884 |_r| RectStyle::default(),
885 vec![],
886 )
887 .unwrap()
888 }
889
890 fn settle(card: &mut StyledContainer) {
891 compute_layout(
892 card.layout_node(),
893 AvailableSpace::Definite(100.0),
894 AvailableSpace::Definite(100.0),
895 )
896 .unwrap();
897 }
898
899 #[test]
900 fn on_alt_press_reports_which_non_primary_button_tapped() {
901 let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
902 let sink = seen.clone();
903 let mut card = laid_out_box().on_alt_press(move |b| sink.set(Some(b)));
904 settle(&mut card);
905
906 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
907 card.on_event(&release_with(50.0, 50.0, PointerButton::Secondary));
908 assert_eq!(seen.take(), Some(PointerButton::Secondary));
909
910 card.on_event(&press_with(50.0, 50.0, PointerButton::Auxiliary));
911 card.on_event(&release_with(50.0, 50.0, PointerButton::Auxiliary));
912 assert_eq!(seen.take(), Some(PointerButton::Auxiliary));
913 }
914
915 #[test]
916 fn a_box_wanting_only_alt_presses_leaves_the_primary_one_alone() {
917 let alt = Rc::new(Cell::new(false));
918 let sink = alt.clone();
919 let mut card = laid_out_box().on_alt_press(move |_| sink.set(true));
920 settle(&mut card);
921
922 assert_eq!(
923 card.on_event(&press_with(50.0, 50.0, PointerButton::Primary)),
924 EventResult::Ignored,
925 "a primary press must still fall through to whatever is behind the box"
926 );
927 card.on_event(&release_with(50.0, 50.0, PointerButton::Primary));
928 assert!(!alt.get(), "the primary button is not an alt press");
929 }
930
931 #[test]
932 fn a_plain_pressable_box_still_ignores_non_primary_buttons() {
933 let tapped = Rc::new(Cell::new(false));
934 let sink = tapped.clone();
935 let mut card = laid_out_box().on_press(move || sink.set(true));
936 settle(&mut card);
937
938 assert_eq!(
939 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary)),
940 EventResult::Ignored,
941 "right-click keeps passing through a box that never asked for it"
942 );
943 card.on_event(&release_with(50.0, 50.0, PointerButton::Secondary));
944 assert!(!tapped.get(), "on_press is a primary-button gesture");
945 }
946
947 #[test]
948 fn releasing_a_different_button_than_armed_completes_nothing() {
949 let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
950 let sink = seen.clone();
951 let tapped = Rc::new(Cell::new(false));
952 let tap_sink = tapped.clone();
953 let mut card = laid_out_box()
954 .on_press(move || tap_sink.set(true))
955 .on_alt_press(move |b| sink.set(Some(b)));
956 settle(&mut card);
957
958 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
959 card.on_event(&release_with(50.0, 50.0, PointerButton::Primary));
960 assert_eq!(
961 seen.take(),
962 None,
963 "the right button armed it, the left cannot complete it"
964 );
965 assert!(!tapped.get());
966 }
967
968 #[test]
969 fn dragging_off_the_box_cancels_an_alt_press() {
970 let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
971 let sink = seen.clone();
972 let mut card = laid_out_box().on_alt_press(move |b| sink.set(Some(b)));
973 settle(&mut card);
974
975 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
976 card.on_event(&Event::PointerMoved {
977 x: 95.0,
978 y: 95.0,
979 source: PointerSource::Mouse,
980 });
981 card.on_event(&release_with(95.0, 95.0, PointerButton::Secondary));
982 assert_eq!(
983 seen.take(),
984 None,
985 "travel past the tap slop cancels an alt press just as it cancels a tap"
986 );
987 }
988
989 #[test]
991 fn inner_button_press_wins_over_box() {
992 let card_flag = Rc::new(Cell::new(false));
993 let btn_flag = Rc::new(Cell::new(false));
994 let cf = card_flag.clone();
995 let bf = btn_flag.clone();
996 reset_layout_runtime();
997 let btn = StyledContainer::new(
999 LayoutStyle::new().width(50.0).height(30.0),
1000 |_r| RectStyle::default(),
1001 vec![],
1002 )
1003 .unwrap()
1004 .on_press(move || bf.set(true));
1005 let btn_node = btn.layout_node();
1006 let mut card = StyledContainer::new(
1007 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1008 |_r| RectStyle::default(),
1009 vec![Box::new(btn)],
1010 )
1011 .unwrap()
1012 .on_press(move || cf.set(true));
1013 compute_layout(
1014 card.layout_node(),
1015 AvailableSpace::Definite(200.0),
1016 AvailableSpace::Definite(100.0),
1017 )
1018 .unwrap();
1019
1020 let br = track_layout(btn_node).unwrap().get();
1021 let (cx, cy) = (
1022 (br.x + br.width / 2.0) as f64,
1023 (br.y + br.height / 2.0) as f64,
1024 );
1025 card.on_event(&press(cx, cy, PointerSource::Mouse));
1026 card.on_event(&release(cx, cy, PointerSource::Mouse));
1027 assert!(btn_flag.get(), "the inner button should fire");
1028 assert!(
1029 !card_flag.get(),
1030 "the box on_press must not fire when a child handled the press"
1031 );
1032 }
1033
1034 #[test]
1036 fn hover_style_swaps_on_mouse_move() {
1037 reset_layout_runtime();
1038 let mut card = StyledContainer::new(
1039 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1040 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1041 vec![],
1042 )
1043 .unwrap()
1044 .on_hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)));
1045 compute_layout(
1046 card.layout_node(),
1047 AvailableSpace::Definite(200.0),
1048 AvailableSpace::Definite(100.0),
1049 )
1050 .unwrap();
1051
1052 let normal = fill_color(&card.view());
1053 card.on_event(&Event::PointerMoved {
1054 x: 100.0,
1055 y: 50.0,
1056 source: PointerSource::Mouse,
1057 });
1058 let hovered = fill_color(&card.view());
1059 assert_ne!(normal, hovered, "hover should swap the fill");
1060
1061 card.on_event(&Event::PointerMoved {
1062 x: 9999.0,
1063 y: 9999.0,
1064 source: PointerSource::Mouse,
1065 });
1066 assert_eq!(
1067 fill_color(&card.view()),
1068 normal,
1069 "leaving the box restores the base fill"
1070 );
1071 }
1072
1073 #[test]
1075 fn touch_move_does_not_set_hover() {
1076 reset_layout_runtime();
1077 let mut card = StyledContainer::new(
1078 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1079 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1080 vec![],
1081 )
1082 .unwrap()
1083 .on_hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)));
1084 compute_layout(
1085 card.layout_node(),
1086 AvailableSpace::Definite(200.0),
1087 AvailableSpace::Definite(100.0),
1088 )
1089 .unwrap();
1090
1091 let normal = fill_color(&card.view());
1092 card.on_event(&Event::PointerMoved {
1093 x: 100.0,
1094 y: 50.0,
1095 source: PointerSource::Touch { id: 1 },
1096 });
1097 assert_eq!(
1098 fill_color(&card.view()),
1099 normal,
1100 "a touch move must not trigger hover"
1101 );
1102 }
1103
1104 #[test]
1106 fn active_style_swaps_on_press_and_clears_on_release() {
1107 reset_layout_runtime();
1108 let mut card = StyledContainer::new(
1109 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1110 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1111 vec![],
1112 )
1113 .unwrap()
1114 .on_active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.5, 0.5, 0.5, 1.0)));
1115 compute_layout(
1116 card.layout_node(),
1117 AvailableSpace::Definite(200.0),
1118 AvailableSpace::Definite(100.0),
1119 )
1120 .unwrap();
1121
1122 let normal = fill_color(&card.view());
1123 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1124 assert_ne!(
1125 normal,
1126 fill_color(&card.view()),
1127 "press swaps to the active fill"
1128 );
1129 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
1130 assert_eq!(
1131 fill_color(&card.view()),
1132 normal,
1133 "release restores the base fill"
1134 );
1135 }
1136
1137 #[test]
1140 fn active_style_takes_precedence_over_hover() {
1141 reset_layout_runtime();
1142 let hover = Color::rgba(0.9, 0.9, 0.9, 1.0);
1143 let active = Color::rgba(0.4, 0.4, 0.4, 1.0);
1144 let mut card = StyledContainer::new(
1145 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1146 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1147 vec![],
1148 )
1149 .unwrap()
1150 .on_hover_style(move |_r| RectStyle::default().with_fill(hover))
1151 .on_active_style(move |_r| RectStyle::default().with_fill(active));
1152 compute_layout(
1153 card.layout_node(),
1154 AvailableSpace::Definite(200.0),
1155 AvailableSpace::Definite(100.0),
1156 )
1157 .unwrap();
1158
1159 card.on_event(&Event::PointerMoved {
1160 x: 100.0,
1161 y: 50.0,
1162 source: PointerSource::Mouse,
1163 });
1164 assert_eq!(
1165 fill_color(&card.view()),
1166 hover,
1167 "hovering shows the hover fill"
1168 );
1169 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1170 assert_eq!(
1171 fill_color(&card.view()),
1172 active,
1173 "pressing while hovered shows the active fill (precedence)"
1174 );
1175 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
1176 assert_eq!(
1177 fill_color(&card.view()),
1178 hover,
1179 "releasing inside falls back to the hover fill"
1180 );
1181 }
1182
1183 #[test]
1185 fn active_style_clears_when_press_drags_off() {
1186 reset_layout_runtime();
1187 let mut card = StyledContainer::new(
1188 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1189 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1190 vec![],
1191 )
1192 .unwrap()
1193 .on_active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.5, 0.5, 0.5, 1.0)));
1194 compute_layout(
1195 card.layout_node(),
1196 AvailableSpace::Definite(200.0),
1197 AvailableSpace::Definite(100.0),
1198 )
1199 .unwrap();
1200
1201 let normal = fill_color(&card.view());
1202 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1203 assert_ne!(normal, fill_color(&card.view()), "press activates");
1204 card.on_event(&Event::PointerMoved {
1205 x: 9999.0,
1206 y: 9999.0,
1207 source: PointerSource::Mouse,
1208 });
1209 assert_eq!(
1210 fill_color(&card.view()),
1211 normal,
1212 "dragging off the box clears the pressed state"
1213 );
1214 }
1215
1216 fn fill_color(view: &RenderNode) -> Color {
1217 let group = match view {
1218 RenderNode::Group { children, .. } => children,
1219 _ => panic!("expected Group"),
1220 };
1221 if let RenderNode::Primitive(renderer_core::DrawCommand::Rect { style, .. }) = &group[0] {
1222 if let Some(renderer_core::Paint::Solid(c)) = style.fill {
1223 return c;
1224 }
1225 }
1226 panic!("expected a solid-fill background rect");
1227 }
1228
1229 #[test]
1231 fn scroll_drag_does_not_press_box() {
1232 let flag = Rc::new(Cell::new(false));
1233 let f = flag.clone();
1234 reset_layout_runtime();
1235 let mut card = StyledContainer::new(
1236 LayoutStyle::new().flex_column().width(200.0).height(200.0),
1237 |_r| RectStyle::default(),
1238 vec![],
1239 )
1240 .unwrap()
1241 .on_press(move || f.set(true));
1242 compute_layout(
1243 card.layout_node(),
1244 AvailableSpace::Definite(200.0),
1245 AvailableSpace::Definite(200.0),
1246 )
1247 .unwrap();
1248
1249 let touch = PointerSource::Touch { id: 1 };
1250 card.on_event(&press(50.0, 20.0, touch.clone()));
1251 card.on_event(&Event::PointerMoved {
1252 x: 50.0,
1253 y: 120.0, source: touch.clone(),
1255 });
1256 card.on_event(&release(50.0, 120.0, touch));
1257 assert!(!flag.get(), "a scroll drag over the box must not press it");
1258 }
1259
1260 #[test]
1263 fn on_drag_reports_press_then_moves_until_release() {
1264 use std::cell::RefCell;
1265 let seen: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
1266 let sink = seen.clone();
1267 reset_layout_runtime();
1268 let mut card = StyledContainer::new(
1269 LayoutStyle::new().flex_column().width(200.0).height(200.0),
1270 |_r| RectStyle::default(),
1271 vec![],
1272 )
1273 .unwrap()
1274 .on_drag(move |x, y| sink.borrow_mut().push((x, y)));
1275 compute_layout(
1276 card.layout_node(),
1277 AvailableSpace::Definite(200.0),
1278 AvailableSpace::Definite(200.0),
1279 )
1280 .unwrap();
1281
1282 let moved = |x: f64, y: f64| Event::PointerMoved {
1283 x,
1284 y,
1285 source: PointerSource::Mouse,
1286 };
1287 card.on_event(&press(40.0, 40.0, PointerSource::Mouse));
1288 card.on_event(&moved(80.0, 90.0));
1289 card.on_event(&moved(400.0, 400.0)); card.on_event(&release(400.0, 400.0, PointerSource::Mouse));
1291 card.on_event(&moved(10.0, 10.0)); assert_eq!(
1294 *seen.borrow(),
1295 vec![(40.0, 40.0), (80.0, 90.0), (400.0, 400.0)],
1296 "drag reports the press point then each move until release"
1297 );
1298 }
1299
1300 #[test]
1304 fn drag_released_outside_bounds_ends_via_parent_dispatch() {
1305 use std::cell::RefCell;
1306 let seen: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
1307 let sink = seen.clone();
1308 reset_layout_runtime();
1309 let child = StyledContainer::new(
1310 LayoutStyle::new().width(100.0).height(100.0),
1311 |_r| RectStyle::default(),
1312 vec![],
1313 )
1314 .unwrap()
1315 .on_drag(move |x, y| sink.borrow_mut().push((x, y)));
1316 let mut parent = Container::new(
1317 LayoutStyle::new().flex_column().width(300.0).height(300.0),
1318 vec![Box::new(child)],
1319 )
1320 .unwrap();
1321 compute_layout(
1322 parent.layout_node(),
1323 AvailableSpace::Definite(300.0),
1324 AvailableSpace::Definite(300.0),
1325 )
1326 .unwrap();
1327
1328 let moved = |x: f64, y: f64| Event::PointerMoved {
1329 x,
1330 y,
1331 source: PointerSource::Mouse,
1332 };
1333 parent.on_event(&press(50.0, 50.0, PointerSource::Mouse));
1335 parent.on_event(&moved(250.0, 250.0));
1336 parent.on_event(&release(250.0, 250.0, PointerSource::Mouse));
1337 parent.on_event(&moved(60.0, 60.0));
1339 assert_eq!(
1340 *seen.borrow(),
1341 vec![(50.0, 50.0), (250.0, 250.0)],
1342 "drag ended on the outside release; the post-release move must not fire"
1343 );
1344 }
1345
1346 #[test]
1349 fn on_drag_end_fires_once_with_the_release_position() {
1350 use std::cell::RefCell;
1351 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
1352 let sink = ends.clone();
1353 reset_layout_runtime();
1354 let mut card = StyledContainer::new(
1355 LayoutStyle::new().width(100.0).height(100.0),
1356 |_r| RectStyle::default(),
1357 vec![],
1358 )
1359 .unwrap()
1360 .on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
1361 compute_layout(
1362 card.layout_node(),
1363 AvailableSpace::Definite(100.0),
1364 AvailableSpace::Definite(100.0),
1365 )
1366 .unwrap();
1367
1368 let moved = |x: f64, y: f64| Event::PointerMoved {
1369 x,
1370 y,
1371 source: PointerSource::Mouse,
1372 };
1373 card.on_event(&moved(10.0, 10.0));
1374 assert!(ends.borrow().is_empty(), "a move with no drag ends nothing");
1375
1376 card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
1377 card.on_event(&moved(70.0, 30.0));
1378 assert!(ends.borrow().is_empty(), "still dragging");
1379 card.on_event(&release(90.0, 40.0, PointerSource::Mouse));
1380 assert_eq!(
1381 *ends.borrow(),
1382 vec![(90.0, 40.0)],
1383 "the release position, not the last move — a drag can end past it"
1384 );
1385
1386 card.on_event(&release(95.0, 45.0, PointerSource::Mouse));
1388 assert_eq!(ends.borrow().len(), 1);
1389 }
1390
1391 #[test]
1394 fn on_drag_end_still_fires_when_the_cursor_leaves() {
1395 use std::cell::RefCell;
1396 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
1397 let sink = ends.clone();
1398 reset_layout_runtime();
1399 let mut card = StyledContainer::new(
1400 LayoutStyle::new().width(100.0).height(100.0),
1401 |_r| RectStyle::default(),
1402 vec![],
1403 )
1404 .unwrap()
1405 .on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
1406 compute_layout(
1407 card.layout_node(),
1408 AvailableSpace::Definite(100.0),
1409 AvailableSpace::Definite(100.0),
1410 )
1411 .unwrap();
1412
1413 card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
1414 card.on_event(&Event::PointerMoved {
1415 x: 60.0,
1416 y: 25.0,
1417 source: PointerSource::Mouse,
1418 });
1419 card.on_event(&Event::CursorLeft);
1420 assert_eq!(
1421 *ends.borrow(),
1422 vec![(60.0, 25.0)],
1423 "the last position the drag reached"
1424 );
1425 }
1426
1427 #[test]
1430 fn on_drag_end_works_without_an_on_drag() {
1431 use std::cell::RefCell;
1432 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
1433 let sink = ends.clone();
1434 reset_layout_runtime();
1435 let mut card = StyledContainer::new(
1436 LayoutStyle::new().width(100.0).height(100.0),
1437 |_r| RectStyle::default(),
1438 vec![],
1439 )
1440 .unwrap()
1441 .on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
1442 compute_layout(
1443 card.layout_node(),
1444 AvailableSpace::Definite(100.0),
1445 AvailableSpace::Definite(100.0),
1446 )
1447 .unwrap();
1448 card.on_event(&press(10.0, 10.0, PointerSource::Mouse));
1449 card.on_event(&release(80.0, 10.0, PointerSource::Mouse));
1450 assert_eq!(*ends.borrow(), vec![(80.0, 10.0)]);
1451 }
1452
1453 #[test]
1455 fn on_focus_fires_on_gain_and_loss() {
1456 use std::cell::RefCell;
1457 let seen: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
1458 let sink = seen.clone();
1459 reset_layout_runtime();
1460 let mut card = StyledContainer::new(
1461 LayoutStyle::new().flex_column().width(100.0).height(100.0),
1462 |_r| RectStyle::default(),
1463 vec![],
1464 )
1465 .unwrap()
1466 .on_focus(move |f| sink.borrow_mut().push(f));
1467 compute_layout(
1468 card.layout_node(),
1469 AvailableSpace::Definite(100.0),
1470 AvailableSpace::Definite(100.0),
1471 )
1472 .unwrap();
1473
1474 card.on_event(&press(50.0, 50.0, PointerSource::Mouse)); crate::focus::clear(); assert_eq!(
1477 *seen.borrow(),
1478 vec![true, false],
1479 "on_focus fires true on gain then false on loss"
1480 );
1481 }
1482
1483 #[test]
1486 fn pressable_publishes_rect_to_interactive_registry_and_withdraws_on_drop() {
1487 use crate::interactive_rects;
1488 reset_layout_runtime();
1489 let baseline = interactive_rects().len();
1490 let card = StyledContainer::new(
1491 LayoutStyle::new().width(120.0).height(40.0),
1492 |_r| RectStyle::default(),
1493 vec![],
1494 )
1495 .unwrap()
1496 .on_press(|| {});
1497 let node = card.layout_node();
1498 assert_eq!(
1500 interactive_rects().len(),
1501 baseline,
1502 "an unlaid-out pressable contributes no rect"
1503 );
1504 compute_layout(
1505 node,
1506 AvailableSpace::Definite(120.0),
1507 AvailableSpace::Definite(40.0),
1508 )
1509 .unwrap();
1510 let rects = interactive_rects();
1511 assert_eq!(rects.len(), baseline + 1);
1512 assert!(
1513 rects.iter().any(|r| r.width == 120.0 && r.height == 40.0),
1514 "a laid-out pressable reports its rect"
1515 );
1516 drop(card);
1517 assert_eq!(
1518 interactive_rects().len(),
1519 baseline,
1520 "dropping the pressable withdraws its rect"
1521 );
1522 }
1523}