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