1use geometry_core::{Rect, Transform};
2use layout_core::{LayoutError, LayoutStyle, NodeId};
3use platform_core::{
4 Cursor, Event, Key, NamedKey, NumericValue, PointerButton, PointerSource, WindowCommand,
5};
6use reactive_core::{Effect, RwSignal, effect, signal};
7use renderer_core::{Color, RectStyle, ShapeStyle, Stroke};
8use theme_core::use_theme_tokens;
9use ui_tree::{Component, EventResult, RenderNode};
10
11use crate::child_host::{ChildSlot, DynHost};
12use crate::context::{new_container, track_layout};
13use crate::drag::DragGesture;
14use crate::focus::{self, FocusId};
15use crate::layout_item::{LayoutItem, TrackedChildren, register_container};
16use crate::pointer::dispatch_container_event;
17use crate::press::PressGesture;
18
19pub fn style_follows(node: NodeId, style: impl Fn() -> LayoutStyle + 'static) -> Effect {
25 effect(move || {
26 let _ = crate::context::set_layout_style(node, style());
27 })
28}
29
30struct StateStyle {
35 hover: Option<Box<dyn Fn(Rect) -> RectStyle>>,
37 is_hovered: RwSignal<bool>,
38 active: Option<Box<dyn Fn(Rect) -> RectStyle>>,
41 is_active: RwSignal<bool>,
42 disabled: Option<Box<dyn Fn(Rect) -> RectStyle>>,
44 focus: Option<Box<dyn Fn(Rect) -> RectStyle>>,
46}
47
48impl Default for StateStyle {
49 fn default() -> Self {
50 Self {
51 hover: None,
52 is_hovered: signal(false),
53 active: None,
54 is_active: signal(false),
55 disabled: None,
56 focus: None,
57 }
58 }
59}
60
61impl StateStyle {
62 fn repaints_on_pointer(&self) -> bool {
64 self.hover.is_some() || self.active.is_some()
65 }
66}
67
68#[derive(Default)]
70struct PointerHooks {
71 hover: Option<Box<dyn Fn(bool)>>,
73 moved: Option<Box<dyn Fn(f32, f32)>>,
75 scroll: Option<Box<dyn Fn(f32, f32)>>,
77 cursor: Option<Cursor>,
79}
80
81impl PointerHooks {
82 fn is_set(&self) -> bool {
85 self.hover.is_some()
86 || self.moved.is_some()
87 || self.scroll.is_some()
88 || self.cursor.is_some()
89 }
90}
91
92#[derive(Default)]
94struct Focusable {
95 id: Option<FocusId>,
98 _effect: Option<Effect>,
100 activates: bool,
103 scope: Option<focus::ScopeId>,
105}
106
107pub struct StyledContainer {
108 node: NodeId,
109 rect: RwSignal<Rect>,
110 style: Box<dyn Fn(Rect) -> RectStyle>,
111 state: StateStyle,
112 disabled_source: Option<Box<dyn Fn() -> bool>>,
114 opacity: Option<Box<dyn Fn() -> f32>>,
116 transform: Option<Box<dyn Fn(Rect) -> Option<[f32; 6]>>>,
118 children: TrackedChildren,
119 dyn_host: Option<DynHost>,
122 press: PressGesture,
124 kept_effects: Vec<Effect>,
126 drag: DragGesture,
128 pointer: PointerHooks,
129 on_key: Option<Box<dyn Fn(&Key)>>,
132 focusable: Focusable,
133 click_through: bool,
136}
137
138impl StyledContainer {
139 pub fn new(
140 layout_style: LayoutStyle,
141 style: impl Fn(Rect) -> RectStyle + 'static,
142 children: Vec<Box<dyn LayoutItem>>,
143 ) -> Result<Self, LayoutError> {
144 let (node, rect, children) = register_container(layout_style, children)?;
145 Ok(Self::assemble(node, rect, Box::new(style), children, None))
146 }
147
148 fn assemble(
151 node: NodeId,
152 rect: RwSignal<Rect>,
153 style: Box<dyn Fn(Rect) -> RectStyle>,
154 children: TrackedChildren,
155 dyn_host: Option<DynHost>,
156 ) -> Self {
157 Self {
158 node,
159 rect,
160 style,
161 state: StateStyle::default(),
162 disabled_source: None,
163 opacity: None,
164 transform: None,
165 children,
166 dyn_host,
167 press: PressGesture::default(),
168 kept_effects: Vec::new(),
169 drag: DragGesture::default(),
170 pointer: PointerHooks::default(),
171 on_key: None,
172 focusable: Focusable::default(),
173 click_through: false,
174 }
175 }
176
177 pub fn from_slots(
181 layout_style: LayoutStyle,
182 style: impl Fn(Rect) -> RectStyle + 'static,
183 slots: Vec<ChildSlot>,
184 ) -> Result<Self, LayoutError> {
185 let node = new_container(layout_style, &[])?;
186 let rect = track_layout(node).expect("new_container always registers a signal");
187 let dyn_host = DynHost::build(node, slots)?;
188 Ok(Self::assemble(
189 node,
190 rect,
191 Box::new(style),
192 Vec::new(),
193 Some(dyn_host),
194 ))
195 }
196
197 fn is_disabled(&self) -> bool {
200 self.disabled_source.as_ref().is_some_and(|f| f())
201 }
202
203 fn is_inert(&self) -> bool {
210 !self.press.is_set()
211 && !self.drag.is_set()
212 && !self.state.repaints_on_pointer()
213 && !self.pointer.is_set()
214 && self.on_key.is_none()
215 && self.focusable.id.is_none()
216 }
217
218 fn dispatch_children(&mut self, event: &Event) -> EventResult {
219 match &self.dyn_host {
220 Some(host) => host.dispatch(event),
221 None => dispatch_container_event(&mut self.children, event),
222 }
223 }
224
225 pub fn with_opacity(mut self, opacity: impl Fn() -> f32 + 'static) -> Self {
226 self.opacity = Some(Box::new(opacity));
227 self
228 }
229
230 pub fn with_transform(
233 mut self,
234 transform: impl Fn(Rect) -> Option<[f32; 6]> + 'static,
235 ) -> Self {
236 self.transform = Some(Box::new(transform));
237 self
238 }
239
240 pub fn hover_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
243 self.state.hover = Some(Box::new(f));
244 self
245 }
246
247 pub fn active_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
251 self.state.active = Some(Box::new(f));
252 self
253 }
254
255 pub fn disabled(mut self, f: impl Fn() -> bool + 'static) -> Self {
265 let f = std::rc::Rc::new(f);
266 self.disabled_source = Some({
267 let f = f.clone();
268 Box::new(move || f())
269 });
270 self.focusable.scope = Some(focus::register_scope_because(
272 self.node,
273 move || !f(),
274 false,
275 focus::ScopeReason::Disabled,
276 ));
277 self
278 }
279
280 pub fn disabled_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
282 self.state.disabled = Some(Box::new(f));
283 self
284 }
285
286 pub fn control(mut self, role: focus::Role) -> Self {
313 let id = *self.focusable.id.get_or_insert_with(focus::next_id);
314 focus::register_with_role(id, focus::FocusKind::Widget, self.node, role);
315 self.focusable.activates = true;
316 if self.state.focus.is_none() {
317 self.state.focus = Some(Box::new(|_r| default_focus_ring()));
318 }
319 self.mark_interactive();
320 self
321 }
322
323 pub fn toggled(self, state: impl Fn() -> bool + 'static) -> Self {
329 if let Some(id) = self.focusable.id {
330 focus::set_toggled(id, state);
331 }
332 self
333 }
334
335 pub fn valued(self, read: impl Fn() -> NumericValue + 'static) -> Self {
338 if let Some(id) = self.focusable.id {
339 focus::set_value(id, read);
340 }
341 self
342 }
343
344 pub fn focus_style(mut self, f: impl Fn(Rect) -> RectStyle + 'static) -> Self {
345 let id = *self.focusable.id.get_or_insert_with(focus::next_id);
347 focus::register_at(id, focus::FocusKind::Widget, self.node);
348 self.state.focus = Some(Box::new(f));
349 self
350 }
351
352 fn set_active(&self, active: bool) {
355 if self.state.active.is_some() && self.state.is_active.get() != active {
356 self.state.is_active.set(active);
357 }
358 }
359
360 fn end_containment(&mut self) {
364 self.press.cancel();
365 self.set_active(false);
366 let tracks_hover = self.state.hover.is_some()
367 || self.pointer.hover.is_some()
368 || self.pointer.cursor.is_some();
369 if tracks_hover && self.state.is_hovered.get() {
370 self.state.is_hovered.set(false);
371 if self.pointer.cursor.is_some() {
373 platform_core::push_window_command(WindowCommand::SetCursor(Cursor::Default));
374 }
375 if let Some(cb) = &self.pointer.hover {
376 cb(false);
377 }
378 }
379 }
380
381 pub fn cursor(mut self, cursor: Cursor) -> Self {
386 self.pointer.cursor = Some(cursor);
387 self
388 }
389
390 pub fn click_through(mut self, through: bool) -> Self {
403 self.click_through = through;
404 self
405 }
406
407 pub fn keeping(mut self, subscription: Effect) -> Self {
417 self.kept_effects.push(subscription);
418 self
419 }
420
421 pub fn styled_by(self, style: impl Fn() -> LayoutStyle + 'static) -> Self {
432 let node = self.node;
433 self.keeping(style_follows(node, style))
434 }
435
436 pub fn on_press(self, f: impl Fn() + 'static) -> Self {
439 self.maybe_on_press(Some(f))
440 }
441
442 pub fn maybe_on_press(mut self, f: Option<impl Fn() + 'static>) -> Self {
449 let Some(f) = f else { return self };
450 self.press.set(f);
451 self.mark_interactive();
452 self
453 }
454
455 pub fn on_alt_press(self, f: impl Fn(PointerButton) + 'static) -> Self {
462 self.maybe_on_alt_press(Some(f))
463 }
464
465 pub fn maybe_on_alt_press(mut self, f: Option<impl Fn(PointerButton) + 'static>) -> Self {
467 let Some(f) = f else { return self };
468 self.press.set_alt_press(f);
469 self.mark_interactive();
470 self
471 }
472
473 pub fn on_long_press(self, f: impl Fn() + 'static) -> Self {
478 self.maybe_on_long_press(Some(f))
479 }
480
481 pub fn maybe_on_long_press(mut self, f: Option<impl Fn() + 'static>) -> Self {
483 let Some(f) = f else { return self };
484 self.press.set_long_press(f);
485 self.mark_interactive();
486 self
487 }
488
489 pub fn on_drag_end(self, f: impl Fn(f32, f32) + 'static) -> Self {
500 self.maybe_on_drag_end(Some(f))
501 }
502
503 pub fn maybe_on_drag_end(mut self, f: Option<impl Fn(f32, f32) + 'static>) -> Self {
505 let Some(f) = f else { return self };
506 self.drag.set_end(f);
507 self.mark_interactive();
508 self
509 }
510
511 pub fn on_drag(self, f: impl Fn(f32, f32) + 'static) -> Self {
512 self.maybe_on_drag(Some(f))
513 }
514
515 pub fn maybe_on_drag(mut self, f: Option<impl Fn(f32, f32) + 'static>) -> Self {
517 let Some(f) = f else { return self };
518 self.drag.set(f);
519 self.mark_interactive();
520 self
521 }
522
523 pub fn drag_button(mut self, button: platform_core::PointerButton) -> Self {
530 self.drag.arm_with(&button);
531 self
532 }
533
534 pub fn drag_threshold(mut self, px: f32) -> Self {
544 self.drag.set_threshold(px);
545 self
546 }
547
548 fn mark_interactive(&self) {
552 crate::input_region::register_interactive(self.node, self.rect.read_only());
553 }
554
555 pub fn on_hover(self, f: impl Fn(bool) + 'static) -> Self {
562 self.maybe_on_hover(Some(f))
563 }
564
565 pub fn maybe_on_hover(mut self, f: Option<impl Fn(bool) + 'static>) -> Self {
567 let Some(f) = f else { return self };
568 self.pointer.hover = Some(Box::new(f));
569 self.mark_interactive();
570 self
571 }
572
573 pub fn on_pointer_move(self, f: impl Fn(f32, f32) + 'static) -> Self {
581 self.maybe_on_pointer_move(Some(f))
582 }
583
584 pub fn maybe_on_pointer_move(mut self, f: Option<impl Fn(f32, f32) + 'static>) -> Self {
586 let Some(f) = f else { return self };
587 self.pointer.moved = Some(Box::new(f));
588 self.mark_interactive();
589 self
590 }
591
592 pub fn on_scroll(self, f: impl Fn(f32, f32) + 'static) -> Self {
600 self.maybe_on_scroll(Some(f))
601 }
602
603 pub fn maybe_on_scroll(mut self, f: Option<impl Fn(f32, f32) + 'static>) -> Self {
605 let Some(f) = f else { return self };
606 self.pointer.scroll = Some(Box::new(f));
607 self.mark_interactive();
608 self
609 }
610
611 pub fn on_key(self, f: impl Fn(&Key) + 'static) -> Self {
619 self.maybe_on_key(Some(f))
620 }
621
622 pub fn maybe_on_key(mut self, f: Option<impl Fn(&Key) + 'static>) -> Self {
624 let Some(f) = f else { return self };
625 self.on_key = Some(Box::new(f));
626 self
627 }
628
629 pub fn on_focus(self, f: impl Fn(bool) + 'static) -> Self {
633 self.maybe_on_focus(Some(f))
634 }
635
636 pub fn maybe_on_focus(mut self, f: Option<impl Fn(bool) + 'static>) -> Self {
638 let Some(f) = f else { return self };
639 let id = *self.focusable.id.get_or_insert_with(focus::next_id);
640 focus::register_at(id, focus::FocusKind::Widget, self.node);
641 let last = std::rc::Rc::new(std::cell::Cell::new(focus::is_focused(id)));
643 self.focusable._effect = Some(effect(move || {
644 let now = focus::is_focused(id);
645 if now != last.get() {
646 last.set(now);
647 f(now);
648 }
649 }));
650 self
651 }
652}
653
654impl LayoutItem for StyledContainer {
655 fn layout_node(&self) -> NodeId {
656 self.node
657 }
658
659 fn pointer_opaque(&self) -> bool {
660 !self.click_through
661 }
662}
663
664impl Component for StyledContainer {
665 fn view(&self) -> RenderNode {
666 let r = self.rect.get();
667 let style = if let Some(disabled) = &self.state.disabled
669 && self.is_disabled()
670 {
671 disabled
672 } else if let Some(active) = &self.state.active
673 && self.state.is_active.get()
674 {
675 active
676 } else if let Some(hover) = &self.state.hover
677 && self.state.is_hovered.get()
678 {
679 hover
680 } else {
681 &self.style
682 };
683 let painted = match (&self.state.focus, self.focusable.id) {
684 (Some(ring), Some(id)) if focus::is_focus_visible(id) => {
685 let base = style(r);
686 let ring = ring(r);
687 RectStyle {
688 fill: ring.fill.or(base.fill),
689 stroke: ring.stroke.or(base.stroke),
690 shadow: ring.shadow.or(base.shadow),
691 radius: base.radius,
693 border_widths: if ring.stroke.is_some() {
697 ring.border_widths
698 } else {
699 base.border_widths
700 },
701 }
702 }
703 _ => style(r),
704 };
705 let background = RenderNode::rect(
706 Rect {
707 x: r.x,
708 y: r.y,
709 width: r.width,
710 height: r.height,
711 },
712 painted,
713 );
714 let content = match &self.dyn_host {
715 Some(host) => {
716 RenderNode::group(std::iter::once(background).chain(host.child_boundaries()))
717 }
718 None => RenderNode::group(
719 std::iter::once(background)
720 .chain(self.children.iter().map(|c| c.segment.boundary())),
721 ),
722 };
723 let opacity = self.opacity.as_ref().map_or(1.0, |o| o());
724 let composed = if opacity < 1.0 {
725 RenderNode::layer(opacity, 0.0, [content])
726 } else {
727 content
728 };
729 match self.transform.as_ref().and_then(|t| t(r)) {
730 Some(matrix) => RenderNode::transform_with(matrix, [composed]),
731 None => composed,
732 }
733 }
734
735 fn on_event(&mut self, event: &Event) -> EventResult {
736 if self.is_disabled() {
739 return match event {
740 Event::PointerMoved { .. }
741 | Event::PointerPressed { .. }
742 | Event::PointerReleased { .. }
743 | Event::Scrolled { .. } => {
744 self.end_containment();
745 self.drag.end(None);
746 EventResult::Ignored
747 }
748 _ => self.dispatch_children(event),
749 };
750 }
751 if self.is_inert() {
752 return self.dispatch_children(event);
753 }
754 let rect = self.rect.get();
755 match event {
756 Event::PointerMoved { x, y, source } => {
760 self.press.track_move(event);
761 let dragged = self.drag.moved(event, rect) == EventResult::Handled;
762 if self.drag.has_threshold() && self.drag.has_started() {
766 self.press.cancel();
767 }
768 let child = self.dispatch_children(event);
769 let inside =
772 rect.contains(*x as f32, *y as f32) && !crate::pointer::pointer_occluded();
773 if !inside {
775 self.set_active(false);
776 }
777 if inside && let Some(cb) = &self.pointer.moved {
778 cb(*x as f32 - rect.x, *y as f32 - rect.y);
779 }
780 let tracks_hover = self.state.hover.is_some()
781 || self.pointer.hover.is_some()
782 || self.pointer.cursor.is_some();
783 if tracks_hover
784 && matches!(source, PointerSource::Mouse)
785 && inside != self.state.is_hovered.get()
786 {
787 self.state.is_hovered.set(inside);
788 if let Some(cursor) = self.pointer.cursor {
789 platform_core::push_window_command(WindowCommand::SetCursor(if inside {
790 cursor
791 } else {
792 Cursor::Default
793 }));
794 }
795 if let Some(cb) = &self.pointer.hover {
796 cb(inside);
797 }
798 return EventResult::Handled;
799 }
800 if dragged { EventResult::Handled } else { child }
801 }
802 Event::PointerPressed { x, y, button, .. } => {
804 let primary = *button == PointerButton::Primary;
807 if !primary && !self.press.wants_alt() && !self.drag.arms(button) {
808 return self.dispatch_children(event);
809 }
810 if self.dispatch_children(event) == EventResult::Handled {
811 self.press.cancel();
812 self.drag.end(None);
813 return EventResult::Handled;
814 }
815 if primary && rect.contains(*x as f32, *y as f32) {
817 self.set_active(true);
818 }
819 let focused = match self.focusable.id {
821 Some(id) if primary && rect.contains(*x as f32, *y as f32) => {
822 focus::request_from_pointer(id);
823 true
824 }
825 _ => false,
826 };
827 let tapped =
828 self.press.is_set() && self.press.arm(event, rect) == EventResult::Handled;
829 let dragged =
830 self.drag.is_set() && self.drag.press(event, rect) == EventResult::Handled;
831 if tapped || dragged || focused {
832 EventResult::Handled
833 } else {
834 EventResult::Ignored
835 }
836 }
837 Event::PointerReleased { button, .. } => {
838 let primary = *button == PointerButton::Primary;
839 if !primary && !self.press.wants_alt() && !self.drag.arms(button) {
840 return self.dispatch_children(event);
841 }
842 if primary {
844 self.set_active(false);
845 }
846 if self.dispatch_children(event) == EventResult::Handled {
847 self.press.cancel();
848 self.drag.end(None);
849 return EventResult::Handled;
850 }
851 let released_at = match event {
854 Event::PointerReleased { x, y, .. } => {
855 Some((*x as f32 - rect.x, *y as f32 - rect.y))
856 }
857 _ => None,
858 };
859 let dragged = self.drag.arms(button) && self.drag.end(released_at);
860 let tapped =
861 self.press.is_set() && self.press.release(event, rect) == EventResult::Handled;
862 if tapped || dragged {
863 EventResult::Handled
864 } else {
865 EventResult::Ignored
866 }
867 }
868 Event::CursorLeft => {
870 self.end_containment();
871 self.dispatch_children(event)
872 }
873 Event::FocusChanged { is_focused: false } => {
875 self.end_containment();
876 self.drag.end(None);
877 self.dispatch_children(event)
878 }
879 Event::Scrolled { delta, x, y } => {
881 if self.dispatch_children(event) == EventResult::Handled {
882 return EventResult::Handled;
883 }
884 let Some(cb) = &self.pointer.scroll else {
885 return EventResult::Ignored;
886 };
887 if !rect.contains(*x as f32, *y as f32) {
888 return EventResult::Ignored;
889 }
890 let (dx, dy) = delta.pixels();
891 cb(dx, dy);
892 EventResult::Handled
893 }
894 Event::KeyPressed { key, modifiers } => {
896 if let Some(id) = self.focusable.id
898 && focus::is_focused(id)
899 && matches!(key, Key::Named(NamedKey::Tab))
900 {
901 if modifiers.is_shift {
902 focus::focus_prev();
903 } else {
904 focus::focus_next();
905 }
906 return EventResult::Handled;
907 }
908 if self.focusable.activates
913 && self.on_key.is_none()
914 && let Some(id) = self.focusable.id
915 && focus::is_focused(id)
916 && matches!(key, Key::Named(NamedKey::Enter | NamedKey::Space))
917 && self.press.activate()
918 {
919 return EventResult::Handled;
920 }
921 if let Some(cb) = &self.on_key
924 && !focus::text_entry_takes_key(key, *modifiers)
925 {
926 cb(key);
927 }
928 self.dispatch_children(event)
929 }
930 _ => self.dispatch_children(event),
931 }
932 }
933
934 fn debug_name(&self) -> &'static str {
935 "StyledContainer"
936 }
937}
938
939impl Drop for StyledContainer {
940 fn drop(&mut self) {
941 self.focusable._effect.take();
943 if let Some(id) = self.focusable.id {
944 focus::unregister(id);
945 }
946 if let Some(scope) = self.focusable.scope {
947 focus::unregister_scope(scope);
948 }
949 crate::input_region::unregister_interactive(self.node);
950 }
951}
952
953fn default_focus_ring() -> RectStyle {
960 let accent = use_theme_tokens()
961 .map(|t| t.primary())
962 .unwrap_or(Color::rgba(0.26, 0.38, 0.93, 1.0));
963 RectStyle::default().with_stroke(Stroke::new(accent, 2.0))
964}
965
966pub fn box_transform(
970 rect: Rect,
971 rotate_deg: f32,
972 scale_x: f32,
973 scale_y: f32,
974 translate_x: f32,
975 translate_y: f32,
976) -> Option<[f32; 6]> {
977 if rotate_deg == 0.0
978 && scale_x == 1.0
979 && scale_y == 1.0
980 && translate_x == 0.0
981 && translate_y == 0.0
982 {
983 return None;
984 }
985 let cx = rect.x + rect.width / 2.0;
986 let cy = rect.y + rect.height / 2.0;
987 let matrix = Transform::rotate_around(rotate_deg, cx, cy)
988 .then(Transform::scale_around(scale_x, scale_y, cx, cy))
989 .then(Transform::translate(translate_x, translate_y));
990 Some(matrix.to_array())
991}
992
993#[cfg(test)]
994mod tests {
995 use crate::context::reset_layout_runtime;
996 use std::cell::Cell;
997 use std::rc::Rc;
998
999 use layout_core::AvailableSpace;
1000 use platform_core::{PointerButton, PointerSource};
1001 use renderer_core::{Color, ShapeStyle};
1002 use theme_core::{Theme, ThemeTokens, set_theme, use_theme};
1003
1004 use super::*;
1005 use platform_core::ScrollDelta;
1006
1007 #[test]
1008 fn box_transform_identity_is_none() {
1009 let r = Rect {
1010 x: 0.0,
1011 y: 0.0,
1012 width: 10.0,
1013 height: 10.0,
1014 };
1015 assert!(box_transform(r, 0.0, 1.0, 1.0, 0.0, 0.0).is_none());
1016 }
1017
1018 #[test]
1019 fn box_transform_scale_pivots_on_center() {
1020 let r = Rect {
1021 x: 0.0,
1022 y: 0.0,
1023 width: 100.0,
1024 height: 100.0,
1025 };
1026 assert_eq!(
1028 box_transform(r, 0.0, 2.0, 2.0, 0.0, 0.0).unwrap(),
1029 [2.0, 0.0, 0.0, 2.0, -50.0, -50.0]
1030 );
1031 }
1032
1033 #[test]
1034 fn box_transform_translate_offsets_origin() {
1035 let r = Rect {
1036 x: 0.0,
1037 y: 0.0,
1038 width: 10.0,
1039 height: 10.0,
1040 };
1041 assert_eq!(
1042 box_transform(r, 0.0, 1.0, 1.0, 8.0, -4.0).unwrap(),
1043 [1.0, 0.0, 0.0, 1.0, 8.0, -4.0]
1044 );
1045 }
1046 use crate::container::Container;
1047 use crate::context::{compute_layout, track_layout};
1048
1049 #[test]
1053 fn a_control_joins_the_tab_order_answers_enter_and_says_what_it_is() {
1054 use std::cell::Cell;
1055
1056 reset_layout_runtime();
1057 focus::clear();
1058 let fired: Rc<Cell<u32>> = Rc::new(Cell::new(0));
1059 let sink = fired.clone();
1060 let mut card = StyledContainer::new(
1061 LayoutStyle::new().width(80.0).height(30.0),
1062 |_r| RectStyle::default(),
1063 vec![],
1064 )
1065 .unwrap()
1066 .control(focus::Role::CheckBox)
1067 .on_press(move || sink.set(sink.get() + 1));
1068 compute_layout(
1069 card.layout_node(),
1070 AvailableSpace::Definite(80.0),
1071 AvailableSpace::Definite(30.0),
1072 )
1073 .unwrap();
1074
1075 focus::focus_next();
1076 assert!(focus::current().is_some(), "Tab reaches it");
1077
1078 let key = |named| Event::KeyPressed {
1079 key: Key::Named(named),
1080 modifiers: platform_core::ModifiersState::default(),
1081 };
1082 assert_eq!(card.on_event(&key(NamedKey::Enter)), EventResult::Handled);
1083 assert_eq!(card.on_event(&key(NamedKey::Space)), EventResult::Handled);
1084 assert_eq!(fired.get(), 2, "Enter and Space each fire the press");
1085
1086 let exposed = focus::exposed();
1087 assert_eq!(exposed.len(), 1);
1088 assert_eq!(exposed[0].role, focus::Role::CheckBox);
1089 assert!(exposed[0].enabled);
1090 }
1091
1092 #[test]
1095 fn a_press_handler_alone_is_not_a_control() {
1096 reset_layout_runtime();
1097 focus::clear();
1098 let mut card = StyledContainer::new(
1099 LayoutStyle::new().width(80.0).height(30.0),
1100 |_r| RectStyle::default(),
1101 vec![],
1102 )
1103 .unwrap()
1104 .on_press(|| {});
1105 compute_layout(
1106 card.layout_node(),
1107 AvailableSpace::Definite(80.0),
1108 AvailableSpace::Definite(30.0),
1109 )
1110 .unwrap();
1111
1112 focus::focus_next();
1113 assert!(focus::exposed().is_empty(), "it is not a tab stop");
1114 assert_eq!(
1115 card.on_event(&Event::KeyPressed {
1116 key: Key::Named(NamedKey::Enter),
1117 modifiers: platform_core::ModifiersState::default(),
1118 }),
1119 EventResult::Ignored,
1120 "and Enter is left for whoever else wanted it"
1121 );
1122 }
1123
1124 fn press(x: f64, y: f64, source: PointerSource) -> Event {
1125 Event::PointerPressed {
1126 x,
1127 y,
1128 button: PointerButton::Primary,
1129 source,
1130 }
1131 }
1132 fn release(x: f64, y: f64, source: PointerSource) -> Event {
1133 Event::PointerReleased {
1134 x,
1135 y,
1136 button: PointerButton::Primary,
1137 source,
1138 }
1139 }
1140
1141 #[test]
1142 fn on_hover_fires_on_enter_and_leave() {
1143 let seen: Rc<Cell<Option<bool>>> = Rc::new(Cell::new(None));
1144 let sink = seen.clone();
1145 reset_layout_runtime();
1146 let inner = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![]).unwrap();
1147 let mut card = StyledContainer::new(
1148 LayoutStyle::new().flex_column().width(100.0).height(100.0),
1149 |_r| RectStyle::default(),
1150 vec![Box::new(inner)],
1151 )
1152 .unwrap()
1153 .on_hover(move |h| sink.set(Some(h)));
1154 let node = card.layout_node();
1155 compute_layout(
1156 node,
1157 AvailableSpace::Definite(100.0),
1158 AvailableSpace::Definite(100.0),
1159 )
1160 .unwrap();
1161
1162 card.on_event(&Event::PointerMoved {
1163 x: 50.0,
1164 y: 50.0,
1165 source: PointerSource::Mouse,
1166 });
1167 assert_eq!(seen.get(), Some(true), "entering fires on_hover(true)");
1168 card.on_event(&Event::CursorLeft);
1169 assert_eq!(seen.get(), Some(false), "leaving fires on_hover(false)");
1170 }
1171
1172 #[test]
1175 fn on_scroll_targets_by_position_and_normalises_lines() {
1176 let seen: Rc<Cell<(f32, f32)>> = Rc::new(Cell::new((0.0, 0.0)));
1177 let sink = seen.clone();
1178 reset_layout_runtime();
1179 let inner = Container::new(LayoutStyle::new().width(100.0).height(100.0), vec![]).unwrap();
1180 let mut card = StyledContainer::new(
1181 LayoutStyle::new().flex_column().width(100.0).height(100.0),
1182 |_r| RectStyle::default(),
1183 vec![Box::new(inner)],
1184 )
1185 .unwrap()
1186 .on_scroll(move |dx, dy| sink.set((dx, dy)));
1187 let node = card.layout_node();
1188 compute_layout(
1189 node,
1190 AvailableSpace::Definite(100.0),
1191 AvailableSpace::Definite(100.0),
1192 )
1193 .unwrap();
1194
1195 assert_eq!(
1196 card.on_event(&Event::Scrolled {
1197 delta: ScrollDelta::Pixels { x: 0.0, y: -30.0 },
1198 x: 300.0,
1199 y: 300.0,
1200 }),
1201 EventResult::Ignored,
1202 "a wheel event outside the box is ignored"
1203 );
1204 assert_eq!(seen.get(), (0.0, 0.0));
1205
1206 assert_eq!(
1207 card.on_event(&Event::Scrolled {
1208 delta: ScrollDelta::Pixels { x: 0.0, y: -30.0 },
1209 x: 50.0,
1210 y: 50.0,
1211 }),
1212 EventResult::Handled,
1213 "a wheel over the box is ours, with no move having preceded it"
1214 );
1215 assert_eq!(seen.get(), (0.0, -30.0));
1216
1217 card.on_event(&Event::Scrolled {
1219 delta: ScrollDelta::Lines { x: 0.0, y: 3.0 },
1220 x: 50.0,
1221 y: 50.0,
1222 });
1223 assert_eq!(seen.get(), (0.0, 60.0));
1224 }
1225
1226 #[test]
1227 fn on_key_fires_on_key_press() {
1228 let count = Rc::new(Cell::new(0u32));
1229 let sink = count.clone();
1230 reset_layout_runtime();
1231 let inner = Container::new(LayoutStyle::new().width(10.0).height(10.0), vec![]).unwrap();
1232 let mut card = StyledContainer::new(
1233 LayoutStyle::new().flex_column(),
1234 |_r| RectStyle::default(),
1235 vec![Box::new(inner)],
1236 )
1237 .unwrap()
1238 .on_key(move |_k| sink.set(sink.get() + 1));
1239 card.on_event(&Event::KeyPressed {
1240 key: Key::Char('a'),
1241 modifiers: platform_core::ModifiersState::default(),
1242 });
1243 assert_eq!(count.get(), 1, "a key press fires on_key");
1244 }
1245
1246 #[test]
1249 fn a_global_key_handler_stands_aside_while_a_field_has_the_caret() {
1250 let count = Rc::new(Cell::new(0u32));
1251 let sink = count.clone();
1252 reset_layout_runtime();
1253 focus::clear();
1254 let mut card = StyledContainer::new(
1255 LayoutStyle::new().flex_column(),
1256 |_r| RectStyle::default(),
1257 vec![],
1258 )
1259 .unwrap()
1260 .on_key(move |_k| sink.set(sink.get() + 1));
1261 let press = |key, modifiers| Event::KeyPressed { key, modifiers };
1262 let plain = platform_core::ModifiersState::default();
1263 let meta = platform_core::ModifiersState {
1264 is_meta: true,
1265 ..Default::default()
1266 };
1267
1268 card.on_event(&press(Key::Char('3'), plain));
1269 assert_eq!(count.get(), 1, "with nothing focused the shortcut fires");
1270
1271 let field = focus::next_id();
1272 focus::register_as(field, focus::FocusKind::TextEntry);
1273 focus::request(field);
1274 card.on_event(&press(Key::Char('3'), plain));
1275 assert_eq!(count.get(), 1, "typing into a field is not a shortcut");
1276 card.on_event(&press(Key::Char('s'), meta));
1277 assert_eq!(count.get(), 2, "a chord is a command, not text");
1278 card.on_event(&press(Key::Named(NamedKey::F5), plain));
1279 assert_eq!(count.get(), 3, "no editor takes F5");
1280
1281 focus::unregister(field);
1282 card.on_event(&press(Key::Char('3'), plain));
1283 assert_eq!(count.get(), 4, "the caret left, the shortcut is back");
1284 }
1285
1286 #[test]
1288 fn a_focused_button_does_not_swallow_shortcuts() {
1289 let count = Rc::new(Cell::new(0u32));
1290 let sink = count.clone();
1291 reset_layout_runtime();
1292 focus::clear();
1293 let mut card = StyledContainer::new(
1294 LayoutStyle::new().flex_column(),
1295 |_r| RectStyle::default(),
1296 vec![],
1297 )
1298 .unwrap()
1299 .on_key(move |_k| sink.set(sink.get() + 1));
1300 let button = focus::next_id();
1301 focus::register_as(button, focus::FocusKind::Widget);
1302 focus::request(button);
1303 card.on_event(&Event::KeyPressed {
1304 key: Key::Char('3'),
1305 modifiers: platform_core::ModifiersState::default(),
1306 });
1307 assert_eq!(count.get(), 1);
1308 focus::unregister(button);
1309 }
1310
1311 #[test]
1315 fn a_drag_starts_only_from_the_buttons_the_box_asked_for() {
1316 let seen = Rc::new(Cell::new(0u32));
1317 let sink = seen.clone();
1318 reset_layout_runtime();
1319 let mut plain = StyledContainer::new(
1320 LayoutStyle::new().width(100.0).height(100.0),
1321 |_r| RectStyle::default(),
1322 vec![],
1323 )
1324 .unwrap()
1325 .on_drag(move |_x, _y| sink.set(sink.get() + 1));
1326 compute_layout(
1327 plain.layout_node(),
1328 AvailableSpace::Definite(100.0),
1329 AvailableSpace::Definite(100.0),
1330 )
1331 .unwrap();
1332
1333 let press = |button: PointerButton| Event::PointerPressed {
1334 x: 50.0,
1335 y: 50.0,
1336 button,
1337 source: PointerSource::Mouse,
1338 };
1339 plain.on_event(&press(PointerButton::Secondary));
1340 assert_eq!(seen.get(), 0, "a secondary press is not this box's drag");
1341 plain.on_event(&press(PointerButton::Primary));
1342 assert_eq!(seen.get(), 1, "the primary one always is");
1343
1344 let count = Rc::new(Cell::new(0u32));
1345 let sink = count.clone();
1346 reset_layout_runtime();
1347 let mut viewport = StyledContainer::new(
1348 LayoutStyle::new().width(100.0).height(100.0),
1349 |_r| RectStyle::default(),
1350 vec![],
1351 )
1352 .unwrap()
1353 .on_drag(move |_x, _y| sink.set(sink.get() + 1))
1354 .drag_button(PointerButton::Secondary);
1355 compute_layout(
1356 viewport.layout_node(),
1357 AvailableSpace::Definite(100.0),
1358 AvailableSpace::Definite(100.0),
1359 )
1360 .unwrap();
1361 assert_eq!(
1362 viewport.on_event(&press(PointerButton::Secondary)),
1363 EventResult::Handled
1364 );
1365 assert_eq!(count.get(), 1, "the box asked for this button");
1366 assert_eq!(
1368 viewport.on_event(&Event::PointerReleased {
1369 x: 60.0,
1370 y: 60.0,
1371 button: PointerButton::Secondary,
1372 source: PointerSource::Mouse,
1373 }),
1374 EventResult::Handled
1375 );
1376 }
1377
1378 #[test]
1381 fn the_button_registry_holds_what_is_down() {
1382 crate::reset_pointer();
1383 assert!(!crate::pointer_buttons().any());
1384 crate::observe_pointer(&Event::PointerPressed {
1385 x: 0.0,
1386 y: 0.0,
1387 button: PointerButton::Secondary,
1388 source: PointerSource::Mouse,
1389 });
1390 assert!(crate::pointer_buttons().secondary);
1391 assert!(!crate::pointer_buttons().primary);
1392 crate::observe_pointer(&Event::FocusChanged { is_focused: false });
1394 assert!(!crate::pointer_buttons().any());
1395 }
1396
1397 #[test]
1398 fn on_pointer_move_reports_the_position_local_to_the_box() {
1399 let seen: Rc<Cell<Option<(f32, f32)>>> = Rc::new(Cell::new(None));
1400 let sink = seen.clone();
1401 reset_layout_runtime();
1402 let spacer = Container::new(LayoutStyle::new().width(100.0).height(20.0), vec![]).unwrap();
1404 let card = StyledContainer::new(
1405 LayoutStyle::new().flex_column().width(100.0).height(100.0),
1406 |_r| RectStyle::default(),
1407 vec![],
1408 )
1409 .unwrap()
1410 .on_pointer_move(move |x, y| sink.set(Some((x, y))));
1411 let mut root = Container::new(
1412 LayoutStyle::new().flex_column().width(100.0).height(120.0),
1413 vec![Box::new(spacer), Box::new(card)],
1414 )
1415 .unwrap();
1416 compute_layout(
1417 root.layout_node(),
1418 AvailableSpace::Definite(100.0),
1419 AvailableSpace::Definite(120.0),
1420 )
1421 .unwrap();
1422
1423 root.on_event(&Event::PointerMoved {
1424 x: 30.0,
1425 y: 50.0,
1426 source: PointerSource::Mouse,
1427 });
1428 assert_eq!(
1429 seen.get(),
1430 Some((30.0, 30.0)),
1431 "the box starts 20px down, so the y arrives 20 less — as on_drag reports it"
1432 );
1433
1434 seen.set(None);
1435 root.on_event(&Event::PointerMoved {
1436 x: 300.0,
1437 y: 300.0,
1438 source: PointerSource::Mouse,
1439 });
1440 assert_eq!(seen.get(), None, "a move outside the box is not its move");
1441 }
1442
1443 #[derive(Clone)]
1444 struct TestTheme(Color);
1445 impl Theme for TestTheme {
1446 fn as_any(&self) -> &dyn std::any::Any {
1447 self
1448 }
1449 }
1450 impl ThemeTokens for TestTheme {
1451 fn primary(&self) -> Color {
1452 self.0
1453 }
1454 fn on_primary(&self) -> Color {
1455 Color::WHITE
1456 }
1457 }
1458
1459 #[test]
1461 fn theme_button_click_force_tick_no_panic() {
1462 set_theme(TestTheme(Color::RED));
1463
1464 reset_layout_runtime();
1465 let btn = StyledContainer::new(
1467 LayoutStyle::new().width(50.0).height(30.0),
1468 |_r| RectStyle::default(),
1469 vec![],
1470 )
1471 .unwrap()
1472 .on_press(move || set_theme(TestTheme(Color::GREEN)));
1473 let btn_node = btn.layout_node();
1474 let inner = Container::new(
1475 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1476 vec![Box::new(btn)],
1477 )
1478 .unwrap();
1479 let card = StyledContainer::new(
1480 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1481 |_r| RectStyle::default().with_fill(use_theme::<TestTheme>().0),
1482 vec![Box::new(inner)],
1483 )
1484 .unwrap();
1485 let card_node = card.layout_node();
1486 compute_layout(
1487 card_node,
1488 AvailableSpace::Definite(200.0),
1489 AvailableSpace::Definite(100.0),
1490 )
1491 .unwrap();
1492 let br = track_layout(btn_node).unwrap().get();
1493
1494 let mut tree = crate::ComponentList::new(card);
1495 let _ = tree.commands();
1496
1497 reactive_core::begin_batch();
1498 let handled = tree.on_event(&Event::PointerPressed {
1499 x: (br.x + br.width / 2.0) as f64,
1500 y: (br.y + br.height / 2.0) as f64,
1501 button: PointerButton::Primary,
1502 source: PointerSource::Mouse,
1503 });
1504 if handled == EventResult::Handled {
1505 tree.bump_force_ticks();
1506 reactive_core::end_batch();
1507 reactive_core::begin_batch();
1508 }
1509 let _ = tree.commands();
1510 reactive_core::end_batch();
1511 }
1512
1513 #[test]
1515 fn on_press_fires_on_tap_not_press() {
1516 let flag = Rc::new(Cell::new(false));
1517 let f = flag.clone();
1518 reset_layout_runtime();
1519 let mut card = StyledContainer::new(
1520 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1521 |_r| RectStyle::default(),
1522 vec![],
1523 )
1524 .unwrap()
1525 .on_press(move || f.set(true));
1526 compute_layout(
1527 card.layout_node(),
1528 AvailableSpace::Definite(200.0),
1529 AvailableSpace::Definite(100.0),
1530 )
1531 .unwrap();
1532
1533 assert_eq!(
1534 card.on_event(&press(100.0, 50.0, PointerSource::Mouse)),
1535 EventResult::Handled
1536 );
1537 assert!(!flag.get(), "press alone must not fire on_press");
1538 assert_eq!(
1539 card.on_event(&release(100.0, 50.0, PointerSource::Mouse)),
1540 EventResult::Handled
1541 );
1542 assert!(flag.get(), "release inside the box fires on_press");
1543 }
1544
1545 #[test]
1548 fn on_long_press_fires_after_threshold_not_on_quick_release() {
1549 let long_flag = Rc::new(Cell::new(false));
1550 let tap_flag = Rc::new(Cell::new(false));
1551 let lf = long_flag.clone();
1552 let tf = tap_flag.clone();
1553 reset_layout_runtime();
1554 let mut card = StyledContainer::new(
1555 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1556 |_r| RectStyle::default(),
1557 vec![],
1558 )
1559 .unwrap()
1560 .on_press(move || tf.set(true))
1561 .on_long_press(move || lf.set(true));
1562 compute_layout(
1563 card.layout_node(),
1564 AvailableSpace::Definite(200.0),
1565 AvailableSpace::Definite(100.0),
1566 )
1567 .unwrap();
1568
1569 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1571 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
1572 assert!(tap_flag.get(), "a quick release still fires on_press");
1573 assert!(
1574 !long_flag.get(),
1575 "a quick release must not fire on_long_press"
1576 );
1577
1578 tap_flag.set(false);
1580 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1581 std::thread::sleep(std::time::Duration::from_millis(550));
1582 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
1583 assert!(
1584 long_flag.get(),
1585 "a release after the threshold fires on_long_press"
1586 );
1587 assert!(!tap_flag.get(), "a long press must not also fire on_press");
1588 }
1589
1590 fn press_with(x: f64, y: f64, button: PointerButton) -> Event {
1591 Event::PointerPressed {
1592 x,
1593 y,
1594 button,
1595 source: PointerSource::Mouse,
1596 }
1597 }
1598 fn release_with(x: f64, y: f64, button: PointerButton) -> Event {
1599 Event::PointerReleased {
1600 x,
1601 y,
1602 button,
1603 source: PointerSource::Mouse,
1604 }
1605 }
1606
1607 fn laid_out_box() -> StyledContainer {
1608 reset_layout_runtime();
1609 StyledContainer::new(
1610 LayoutStyle::new().width(100.0).height(100.0),
1611 |_r| RectStyle::default(),
1612 vec![],
1613 )
1614 .unwrap()
1615 }
1616
1617 fn settle(card: &mut StyledContainer) {
1618 compute_layout(
1619 card.layout_node(),
1620 AvailableSpace::Definite(100.0),
1621 AvailableSpace::Definite(100.0),
1622 )
1623 .unwrap();
1624 }
1625
1626 #[test]
1627 fn on_alt_press_reports_which_non_primary_button_tapped() {
1628 let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
1629 let sink = seen.clone();
1630 let mut card = laid_out_box().on_alt_press(move |b| sink.set(Some(b)));
1631 settle(&mut card);
1632
1633 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
1634 card.on_event(&release_with(50.0, 50.0, PointerButton::Secondary));
1635 assert_eq!(seen.take(), Some(PointerButton::Secondary));
1636
1637 card.on_event(&press_with(50.0, 50.0, PointerButton::Auxiliary));
1638 card.on_event(&release_with(50.0, 50.0, PointerButton::Auxiliary));
1639 assert_eq!(seen.take(), Some(PointerButton::Auxiliary));
1640 }
1641
1642 #[test]
1643 fn a_box_wanting_only_alt_presses_leaves_the_primary_one_alone() {
1644 let alt = Rc::new(Cell::new(false));
1645 let sink = alt.clone();
1646 let mut card = laid_out_box().on_alt_press(move |_| sink.set(true));
1647 settle(&mut card);
1648
1649 assert_eq!(
1650 card.on_event(&press_with(50.0, 50.0, PointerButton::Primary)),
1651 EventResult::Ignored,
1652 "a primary press must still fall through to whatever is behind the box"
1653 );
1654 card.on_event(&release_with(50.0, 50.0, PointerButton::Primary));
1655 assert!(!alt.get(), "the primary button is not an alt press");
1656 }
1657
1658 #[test]
1660 fn maybe_on_alt_press_of_none_lets_a_secondary_press_fall_through() {
1661 let mut card = laid_out_box().maybe_on_alt_press(None::<fn(PointerButton)>);
1662 settle(&mut card);
1663
1664 assert_eq!(
1665 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary)),
1666 EventResult::Ignored
1667 );
1668 }
1669
1670 #[test]
1671 fn a_plain_pressable_box_still_ignores_non_primary_buttons() {
1672 let tapped = Rc::new(Cell::new(false));
1673 let sink = tapped.clone();
1674 let mut card = laid_out_box().on_press(move || sink.set(true));
1675 settle(&mut card);
1676
1677 assert_eq!(
1678 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary)),
1679 EventResult::Ignored,
1680 "right-click keeps passing through a box that never asked for it"
1681 );
1682 card.on_event(&release_with(50.0, 50.0, PointerButton::Secondary));
1683 assert!(!tapped.get(), "on_press is a primary-button gesture");
1684 }
1685
1686 #[test]
1687 fn releasing_a_different_button_than_armed_completes_nothing() {
1688 let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
1689 let sink = seen.clone();
1690 let tapped = Rc::new(Cell::new(false));
1691 let tap_sink = tapped.clone();
1692 let mut card = laid_out_box()
1693 .on_press(move || tap_sink.set(true))
1694 .on_alt_press(move |b| sink.set(Some(b)));
1695 settle(&mut card);
1696
1697 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
1698 card.on_event(&release_with(50.0, 50.0, PointerButton::Primary));
1699 assert_eq!(
1700 seen.take(),
1701 None,
1702 "the right button armed it, the left cannot complete it"
1703 );
1704 assert!(!tapped.get());
1705 }
1706
1707 #[test]
1708 fn dragging_off_the_box_cancels_an_alt_press() {
1709 let seen: Rc<Cell<Option<PointerButton>>> = Rc::new(Cell::new(None));
1710 let sink = seen.clone();
1711 let mut card = laid_out_box().on_alt_press(move |b| sink.set(Some(b)));
1712 settle(&mut card);
1713
1714 card.on_event(&press_with(50.0, 50.0, PointerButton::Secondary));
1715 card.on_event(&Event::PointerMoved {
1716 x: 95.0,
1717 y: 95.0,
1718 source: PointerSource::Mouse,
1719 });
1720 card.on_event(&release_with(95.0, 95.0, PointerButton::Secondary));
1721 assert_eq!(
1722 seen.take(),
1723 None,
1724 "travel past the tap slop cancels an alt press just as it cancels a tap"
1725 );
1726 }
1727
1728 #[test]
1730 fn inner_button_press_wins_over_box() {
1731 let card_flag = Rc::new(Cell::new(false));
1732 let btn_flag = Rc::new(Cell::new(false));
1733 let cf = card_flag.clone();
1734 let bf = btn_flag.clone();
1735 reset_layout_runtime();
1736 let btn = StyledContainer::new(
1738 LayoutStyle::new().width(50.0).height(30.0),
1739 |_r| RectStyle::default(),
1740 vec![],
1741 )
1742 .unwrap()
1743 .on_press(move || bf.set(true));
1744 let btn_node = btn.layout_node();
1745 let mut card = StyledContainer::new(
1746 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1747 |_r| RectStyle::default(),
1748 vec![Box::new(btn)],
1749 )
1750 .unwrap()
1751 .on_press(move || cf.set(true));
1752 compute_layout(
1753 card.layout_node(),
1754 AvailableSpace::Definite(200.0),
1755 AvailableSpace::Definite(100.0),
1756 )
1757 .unwrap();
1758
1759 let br = track_layout(btn_node).unwrap().get();
1760 let (cx, cy) = (
1761 (br.x + br.width / 2.0) as f64,
1762 (br.y + br.height / 2.0) as f64,
1763 );
1764 card.on_event(&press(cx, cy, PointerSource::Mouse));
1765 card.on_event(&release(cx, cy, PointerSource::Mouse));
1766 assert!(btn_flag.get(), "the inner button should fire");
1767 assert!(
1768 !card_flag.get(),
1769 "the box on_press must not fire when a child handled the press"
1770 );
1771 }
1772
1773 #[test]
1777 fn a_focus_ring_is_drawn_over_the_state_that_won_not_instead_of_it() {
1778 reset_layout_runtime();
1779 let hover_fill = Color::rgba(0.9, 0.9, 0.9, 1.0);
1780 let ring = renderer_core::Stroke::new(Color::rgba(0.0, 0.4, 1.0, 1.0), 2.0);
1781 let mut card = StyledContainer::new(
1782 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1783 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1784 vec![],
1785 )
1786 .unwrap()
1787 .hover_style(move |_r| RectStyle::default().with_fill(hover_fill))
1788 .focus_style(move |_r| RectStyle {
1789 stroke: Some(ring),
1790 ..RectStyle::default()
1791 });
1792 compute_layout(
1793 card.layout_node(),
1794 AvailableSpace::Definite(200.0),
1795 AvailableSpace::Definite(100.0),
1796 )
1797 .unwrap();
1798
1799 let id = card.focusable.id.expect("a ring makes the box focusable");
1800 focus::request(id);
1801 card.on_event(&Event::PointerMoved {
1802 x: 100.0,
1803 y: 50.0,
1804 source: PointerSource::Mouse,
1805 });
1806
1807 let painted = rect_style(&card.view()).expect("the box paints a rect");
1808 assert_eq!(
1809 painted.fill,
1810 Some(renderer_core::Paint::Solid(hover_fill)),
1811 "the hover fill survives the ring"
1812 );
1813 assert_eq!(painted.stroke, Some(ring), "and the ring is drawn over it");
1814 focus::release(id);
1815 }
1816
1817 #[test]
1821 fn a_tap_takes_focus_without_drawing_a_ring() {
1822 reset_layout_runtime();
1823 let ring = renderer_core::Stroke::new(Color::rgba(0.0, 0.4, 1.0, 1.0), 2.0);
1824 let mut card = StyledContainer::new(
1825 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1826 |_r| RectStyle::default(),
1827 vec![],
1828 )
1829 .unwrap()
1830 .focus_style(move |_r| RectStyle {
1831 stroke: Some(ring),
1832 ..RectStyle::default()
1833 });
1834 compute_layout(
1835 card.layout_node(),
1836 AvailableSpace::Definite(200.0),
1837 AvailableSpace::Definite(100.0),
1838 )
1839 .unwrap();
1840 let id = card.focusable.id.expect("a ring makes the box focusable");
1841
1842 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1843 assert!(focus::is_focused(id), "the tap did take focus");
1844 assert_eq!(
1845 rect_style(&card.view()).and_then(|s| s.stroke),
1846 None,
1847 "but drew no ring for it"
1848 );
1849
1850 focus::request(id);
1852 assert_eq!(rect_style(&card.view()).and_then(|s| s.stroke), Some(ring));
1853 focus::release(id);
1854 }
1855
1856 #[test]
1861 fn a_disabled_box_neither_lights_up_nor_fires() {
1862 reset_layout_runtime();
1863 let presses = Rc::new(Cell::new(0u32));
1864 let sink = presses.clone();
1865 let enabled = signal(false);
1866 let flag = enabled.clone();
1867 let mut card = StyledContainer::new(
1868 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1869 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
1870 vec![],
1871 )
1872 .unwrap()
1873 .hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)))
1874 .on_press(move || sink.set(sink.get() + 1))
1875 .disabled(move || !flag.get());
1876 compute_layout(
1877 card.layout_node(),
1878 AvailableSpace::Definite(200.0),
1879 AvailableSpace::Definite(100.0),
1880 )
1881 .unwrap();
1882
1883 let base = fill_color(&card.view());
1884 card.on_event(&Event::PointerMoved {
1885 x: 100.0,
1886 y: 50.0,
1887 source: PointerSource::Mouse,
1888 });
1889 assert_eq!(
1890 fill_color(&card.view()),
1891 base,
1892 "a disabled box does not take the hover paint"
1893 );
1894 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1895 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
1896 assert_eq!(presses.get(), 0, "and its press callback never fires");
1897
1898 enabled.set(true);
1900 card.on_event(&Event::PointerMoved {
1901 x: 100.0,
1902 y: 50.0,
1903 source: PointerSource::Mouse,
1904 });
1905 assert_ne!(fill_color(&card.view()), base, "now it hovers");
1906 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
1907 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
1908 assert_eq!(presses.get(), 1);
1909 }
1910
1911 #[test]
1920 fn a_style_effect_that_reads_the_rect_its_own_layout_pass_just_wrote_settles_instead_of_panicking()
1921 {
1922 reset_layout_runtime();
1923 let card = StyledContainer::new(
1924 LayoutStyle::new().width(200.0).height(100.0),
1925 |_r| RectStyle::default(),
1926 vec![],
1927 )
1928 .unwrap();
1929 let node = card.layout_node();
1930 let seen = track_layout(node).expect("the container registers a rect signal");
1931 let settled = seen.clone();
1932 let runs = Rc::new(Cell::new(0u32));
1933 let counted = runs.clone();
1934 let card = card.styled_by(move || {
1936 counted.set(counted.get() + 1);
1937 let width = seen.get().width;
1938 LayoutStyle::new()
1939 .width(200.0)
1940 .height((width * 0.5).max(1.0))
1941 });
1942
1943 compute_layout(
1944 card.layout_node(),
1945 AvailableSpace::Definite(200.0),
1946 AvailableSpace::Definite(100.0),
1947 )
1948 .unwrap();
1949 compute_layout(
1950 card.layout_node(),
1951 AvailableSpace::Definite(200.0),
1952 AvailableSpace::Definite(100.0),
1953 )
1954 .unwrap();
1955
1956 assert!(runs.get() >= 1, "the style closure ran");
1957 assert_eq!(
1958 settled.peek().height,
1959 100.0,
1960 "and the rect it derives itself from came to rest instead of running away"
1961 );
1962 }
1963
1964 #[test]
1969 fn a_disabled_box_does_not_claim_the_pointer_shape() {
1970 use platform_core::take_window_commands;
1971
1972 reset_layout_runtime();
1973 let enabled = signal(false);
1974 let flag = enabled.clone();
1975 let mut card = StyledContainer::new(
1976 LayoutStyle::new().flex_column().width(200.0).height(100.0),
1977 |_r| RectStyle::default(),
1978 vec![],
1979 )
1980 .unwrap()
1981 .cursor(Cursor::Pointer)
1982 .disabled(move || !flag.get());
1983 compute_layout(
1984 card.layout_node(),
1985 AvailableSpace::Definite(200.0),
1986 AvailableSpace::Definite(100.0),
1987 )
1988 .unwrap();
1989
1990 let over = Event::PointerMoved {
1991 x: 100.0,
1992 y: 50.0,
1993 source: PointerSource::Mouse,
1994 };
1995 let _ = take_window_commands();
1996 card.on_event(&over);
1997 assert!(
1998 take_window_commands().is_empty(),
1999 "a disabled box asks for no cursor at all"
2000 );
2001
2002 enabled.set(true);
2003 card.on_event(&over);
2004 assert!(
2005 take_window_commands()
2006 .iter()
2007 .any(|c| matches!(c, WindowCommand::SetCursor(Cursor::Pointer))),
2008 "and asks for it again once it can be used"
2009 );
2010
2011 enabled.set(false);
2013 card.on_event(&over);
2014 assert!(
2015 take_window_commands()
2016 .iter()
2017 .any(|c| matches!(c, WindowCommand::SetCursor(Cursor::Default))),
2018 "the shape is given back when the box stops accepting the pointer"
2019 );
2020 }
2021
2022 #[test]
2025 fn disabling_a_hovered_box_takes_the_hover_back() {
2026 reset_layout_runtime();
2027 let enabled = signal(true);
2028 let flag = enabled.clone();
2029 let mut card = StyledContainer::new(
2030 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2031 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2032 vec![],
2033 )
2034 .unwrap()
2035 .hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)))
2036 .disabled(move || !flag.get());
2037 compute_layout(
2038 card.layout_node(),
2039 AvailableSpace::Definite(200.0),
2040 AvailableSpace::Definite(100.0),
2041 )
2042 .unwrap();
2043
2044 let base = fill_color(&card.view());
2045 let moved = Event::PointerMoved {
2046 x: 100.0,
2047 y: 50.0,
2048 source: PointerSource::Mouse,
2049 };
2050 card.on_event(&moved);
2051 assert_ne!(fill_color(&card.view()), base, "hovered while enabled");
2052
2053 enabled.set(false);
2054 card.on_event(&moved);
2055 assert_eq!(
2056 fill_color(&card.view()),
2057 base,
2058 "the highlight goes with the ability to act on it"
2059 );
2060 }
2061
2062 #[test]
2065 fn the_disabled_paint_wins_over_every_other_state() {
2066 reset_layout_runtime();
2067 let off = Color::rgba(0.5, 0.5, 0.5, 1.0);
2068 let mut card = StyledContainer::new(
2069 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2070 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2071 vec![],
2072 )
2073 .unwrap()
2074 .hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)))
2075 .active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.7, 0.7, 0.7, 1.0)))
2076 .disabled_style(move |_r| RectStyle::default().with_fill(off))
2077 .disabled(|| true);
2078 compute_layout(
2079 card.layout_node(),
2080 AvailableSpace::Definite(200.0),
2081 AvailableSpace::Definite(100.0),
2082 )
2083 .unwrap();
2084
2085 card.on_event(&Event::PointerMoved {
2086 x: 100.0,
2087 y: 50.0,
2088 source: PointerSource::Mouse,
2089 });
2090 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
2091 assert_eq!(fill_color(&card.view()), off);
2092 }
2093
2094 #[test]
2098 fn a_disabled_wrapper_shields_its_children() {
2099 reset_layout_runtime();
2100 let presses = Rc::new(Cell::new(0u32));
2101 let sink = presses.clone();
2102 let inner = StyledContainer::new(
2103 LayoutStyle::new().width(200.0).height(100.0),
2104 |_r| RectStyle::default(),
2105 vec![],
2106 )
2107 .unwrap()
2108 .on_press(move || sink.set(sink.get() + 1));
2109 let mut wrapper = StyledContainer::new(
2110 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2111 |_r| RectStyle::default(),
2112 vec![Box::new(inner)],
2113 )
2114 .unwrap()
2115 .disabled(|| true);
2116 compute_layout(
2117 wrapper.layout_node(),
2118 AvailableSpace::Definite(200.0),
2119 AvailableSpace::Definite(100.0),
2120 )
2121 .unwrap();
2122
2123 wrapper.on_event(&press(100.0, 50.0, PointerSource::Mouse));
2124 wrapper.on_event(&release(100.0, 50.0, PointerSource::Mouse));
2125 assert_eq!(presses.get(), 0);
2126 }
2127
2128 #[test]
2130 fn hover_style_swaps_on_mouse_move() {
2131 reset_layout_runtime();
2132 let mut card = StyledContainer::new(
2133 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2134 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2135 vec![],
2136 )
2137 .unwrap()
2138 .hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)));
2139 compute_layout(
2140 card.layout_node(),
2141 AvailableSpace::Definite(200.0),
2142 AvailableSpace::Definite(100.0),
2143 )
2144 .unwrap();
2145
2146 let normal = fill_color(&card.view());
2147 card.on_event(&Event::PointerMoved {
2148 x: 100.0,
2149 y: 50.0,
2150 source: PointerSource::Mouse,
2151 });
2152 let hovered = fill_color(&card.view());
2153 assert_ne!(normal, hovered, "hover should swap the fill");
2154
2155 card.on_event(&Event::PointerMoved {
2156 x: 9999.0,
2157 y: 9999.0,
2158 source: PointerSource::Mouse,
2159 });
2160 assert_eq!(
2161 fill_color(&card.view()),
2162 normal,
2163 "leaving the box restores the base fill"
2164 );
2165 }
2166
2167 #[test]
2169 fn touch_move_does_not_set_hover() {
2170 reset_layout_runtime();
2171 let mut card = StyledContainer::new(
2172 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2173 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2174 vec![],
2175 )
2176 .unwrap()
2177 .hover_style(|_r| RectStyle::default().with_fill(Color::rgba(0.9, 0.9, 0.9, 1.0)));
2178 compute_layout(
2179 card.layout_node(),
2180 AvailableSpace::Definite(200.0),
2181 AvailableSpace::Definite(100.0),
2182 )
2183 .unwrap();
2184
2185 let normal = fill_color(&card.view());
2186 card.on_event(&Event::PointerMoved {
2187 x: 100.0,
2188 y: 50.0,
2189 source: PointerSource::Touch { id: 1 },
2190 });
2191 assert_eq!(
2192 fill_color(&card.view()),
2193 normal,
2194 "a touch move must not trigger hover"
2195 );
2196 }
2197
2198 #[test]
2200 fn active_style_swaps_on_press_and_clears_on_release() {
2201 reset_layout_runtime();
2202 let mut card = StyledContainer::new(
2203 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2204 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2205 vec![],
2206 )
2207 .unwrap()
2208 .active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.5, 0.5, 0.5, 1.0)));
2209 compute_layout(
2210 card.layout_node(),
2211 AvailableSpace::Definite(200.0),
2212 AvailableSpace::Definite(100.0),
2213 )
2214 .unwrap();
2215
2216 let normal = fill_color(&card.view());
2217 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
2218 assert_ne!(
2219 normal,
2220 fill_color(&card.view()),
2221 "press swaps to the active fill"
2222 );
2223 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
2224 assert_eq!(
2225 fill_color(&card.view()),
2226 normal,
2227 "release restores the base fill"
2228 );
2229 }
2230
2231 #[test]
2234 fn active_style_takes_precedence_over_hover() {
2235 reset_layout_runtime();
2236 let hover = Color::rgba(0.9, 0.9, 0.9, 1.0);
2237 let active = Color::rgba(0.4, 0.4, 0.4, 1.0);
2238 let mut card = StyledContainer::new(
2239 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2240 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2241 vec![],
2242 )
2243 .unwrap()
2244 .hover_style(move |_r| RectStyle::default().with_fill(hover))
2245 .active_style(move |_r| RectStyle::default().with_fill(active));
2246 compute_layout(
2247 card.layout_node(),
2248 AvailableSpace::Definite(200.0),
2249 AvailableSpace::Definite(100.0),
2250 )
2251 .unwrap();
2252
2253 card.on_event(&Event::PointerMoved {
2254 x: 100.0,
2255 y: 50.0,
2256 source: PointerSource::Mouse,
2257 });
2258 assert_eq!(
2259 fill_color(&card.view()),
2260 hover,
2261 "hovering shows the hover fill"
2262 );
2263 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
2264 assert_eq!(
2265 fill_color(&card.view()),
2266 active,
2267 "pressing while hovered shows the active fill (precedence)"
2268 );
2269 card.on_event(&release(100.0, 50.0, PointerSource::Mouse));
2270 assert_eq!(
2271 fill_color(&card.view()),
2272 hover,
2273 "releasing inside falls back to the hover fill"
2274 );
2275 }
2276
2277 #[test]
2279 fn active_style_clears_when_press_drags_off() {
2280 reset_layout_runtime();
2281 let mut card = StyledContainer::new(
2282 LayoutStyle::new().flex_column().width(200.0).height(100.0),
2283 |_r| RectStyle::default().with_fill(Color::rgba(0.1, 0.1, 0.1, 1.0)),
2284 vec![],
2285 )
2286 .unwrap()
2287 .active_style(|_r| RectStyle::default().with_fill(Color::rgba(0.5, 0.5, 0.5, 1.0)));
2288 compute_layout(
2289 card.layout_node(),
2290 AvailableSpace::Definite(200.0),
2291 AvailableSpace::Definite(100.0),
2292 )
2293 .unwrap();
2294
2295 let normal = fill_color(&card.view());
2296 card.on_event(&press(100.0, 50.0, PointerSource::Mouse));
2297 assert_ne!(normal, fill_color(&card.view()), "press activates");
2298 card.on_event(&Event::PointerMoved {
2299 x: 9999.0,
2300 y: 9999.0,
2301 source: PointerSource::Mouse,
2302 });
2303 assert_eq!(
2304 fill_color(&card.view()),
2305 normal,
2306 "dragging off the box clears the pressed state"
2307 );
2308 }
2309
2310 fn rect_style(view: &RenderNode) -> Option<RectStyle> {
2312 let RenderNode::Group { children, .. } = view else {
2313 return None;
2314 };
2315 match children.first() {
2316 Some(RenderNode::Primitive(renderer_core::DrawCommand::Rect { style, .. })) => {
2317 Some(**style)
2318 }
2319 _ => None,
2320 }
2321 }
2322
2323 fn fill_color(view: &RenderNode) -> Color {
2324 let group = match view {
2325 RenderNode::Group { children, .. } => children,
2326 _ => panic!("expected Group"),
2327 };
2328 if let RenderNode::Primitive(renderer_core::DrawCommand::Rect { style, .. }) = &group[0] {
2329 if let Some(renderer_core::Paint::Solid(c)) = style.fill {
2330 return c;
2331 }
2332 }
2333 panic!("expected a solid-fill background rect");
2334 }
2335
2336 #[test]
2338 fn scroll_drag_does_not_press_box() {
2339 let flag = Rc::new(Cell::new(false));
2340 let f = flag.clone();
2341 reset_layout_runtime();
2342 let mut card = StyledContainer::new(
2343 LayoutStyle::new().flex_column().width(200.0).height(200.0),
2344 |_r| RectStyle::default(),
2345 vec![],
2346 )
2347 .unwrap()
2348 .on_press(move || f.set(true));
2349 compute_layout(
2350 card.layout_node(),
2351 AvailableSpace::Definite(200.0),
2352 AvailableSpace::Definite(200.0),
2353 )
2354 .unwrap();
2355
2356 let touch = PointerSource::Touch { id: 1 };
2357 card.on_event(&press(50.0, 20.0, touch.clone()));
2358 card.on_event(&Event::PointerMoved {
2359 x: 50.0,
2360 y: 120.0, source: touch.clone(),
2362 });
2363 card.on_event(&release(50.0, 120.0, touch));
2364 assert!(!flag.get(), "a scroll drag over the box must not press it");
2365 }
2366
2367 #[test]
2370 fn on_drag_reports_press_then_moves_until_release() {
2371 use std::cell::RefCell;
2372 let seen: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2373 let sink = seen.clone();
2374 reset_layout_runtime();
2375 let mut card = StyledContainer::new(
2376 LayoutStyle::new().flex_column().width(200.0).height(200.0),
2377 |_r| RectStyle::default(),
2378 vec![],
2379 )
2380 .unwrap()
2381 .on_drag(move |x, y| sink.borrow_mut().push((x, y)));
2382 compute_layout(
2383 card.layout_node(),
2384 AvailableSpace::Definite(200.0),
2385 AvailableSpace::Definite(200.0),
2386 )
2387 .unwrap();
2388
2389 let moved = |x: f64, y: f64| Event::PointerMoved {
2390 x,
2391 y,
2392 source: PointerSource::Mouse,
2393 };
2394 card.on_event(&press(40.0, 40.0, PointerSource::Mouse));
2395 card.on_event(&moved(80.0, 90.0));
2396 card.on_event(&moved(400.0, 400.0)); card.on_event(&release(400.0, 400.0, PointerSource::Mouse));
2398 card.on_event(&moved(10.0, 10.0)); assert_eq!(
2401 *seen.borrow(),
2402 vec![(40.0, 40.0), (80.0, 90.0), (400.0, 400.0)],
2403 "drag reports the press point then each move until release"
2404 );
2405 }
2406
2407 #[test]
2411 fn a_threshold_splits_a_click_from_a_drag_on_the_same_button() {
2412 use std::cell::Cell;
2413 use std::cell::RefCell;
2414
2415 let build = || {
2416 let clicks: Rc<Cell<u32>> = Rc::new(Cell::new(0));
2417 let drags: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2418 reset_layout_runtime();
2419 let (c, d) = (clicks.clone(), drags.clone());
2420 let card = StyledContainer::new(
2421 LayoutStyle::new().flex_column().width(200.0).height(200.0),
2422 |_r| RectStyle::default(),
2423 vec![],
2424 )
2425 .unwrap()
2426 .drag_threshold(4.0)
2427 .on_press(move || c.set(c.get() + 1))
2428 .on_drag(move |x, y| d.borrow_mut().push((x, y)));
2429 compute_layout(
2430 card.layout_node(),
2431 AvailableSpace::Definite(200.0),
2432 AvailableSpace::Definite(200.0),
2433 )
2434 .unwrap();
2435 (card, clicks, drags)
2436 };
2437 let moved = |x: f64, y: f64| Event::PointerMoved {
2438 x,
2439 y,
2440 source: PointerSource::Mouse,
2441 };
2442
2443 let (mut card, clicks, drags) = build();
2445 card.on_event(&press(40.0, 40.0, PointerSource::Mouse));
2446 card.on_event(&moved(41.0, 40.0));
2447 card.on_event(&release(41.0, 40.0, PointerSource::Mouse));
2448 assert_eq!(clicks.get(), 1, "the click survives the wobble");
2449 assert!(drags.borrow().is_empty(), "and nothing was dragged");
2450
2451 let (mut card, clicks, drags) = build();
2453 card.on_event(&press(40.0, 40.0, PointerSource::Mouse));
2454 card.on_event(&moved(90.0, 40.0));
2455 card.on_event(&release(90.0, 40.0, PointerSource::Mouse));
2456 assert_eq!(clicks.get(), 0, "a drag is not also a click");
2457 assert_eq!(*drags.borrow(), vec![(90.0, 40.0)]);
2458 }
2459
2460 #[test]
2464 fn drag_released_outside_bounds_ends_via_parent_dispatch() {
2465 use std::cell::RefCell;
2466 let seen: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2467 let sink = seen.clone();
2468 reset_layout_runtime();
2469 let child = StyledContainer::new(
2470 LayoutStyle::new().width(100.0).height(100.0),
2471 |_r| RectStyle::default(),
2472 vec![],
2473 )
2474 .unwrap()
2475 .on_drag(move |x, y| sink.borrow_mut().push((x, y)));
2476 let mut parent = Container::new(
2477 LayoutStyle::new().flex_column().width(300.0).height(300.0),
2478 vec![Box::new(child)],
2479 )
2480 .unwrap();
2481 compute_layout(
2482 parent.layout_node(),
2483 AvailableSpace::Definite(300.0),
2484 AvailableSpace::Definite(300.0),
2485 )
2486 .unwrap();
2487
2488 let moved = |x: f64, y: f64| Event::PointerMoved {
2489 x,
2490 y,
2491 source: PointerSource::Mouse,
2492 };
2493 parent.on_event(&press(50.0, 50.0, PointerSource::Mouse));
2495 parent.on_event(&moved(250.0, 250.0));
2496 parent.on_event(&release(250.0, 250.0, PointerSource::Mouse));
2497 parent.on_event(&moved(60.0, 60.0));
2499 assert_eq!(
2500 *seen.borrow(),
2501 vec![(50.0, 50.0), (250.0, 250.0)],
2502 "drag ended on the outside release; the post-release move must not fire"
2503 );
2504 }
2505
2506 #[test]
2509 fn on_drag_end_fires_once_with_the_release_position() {
2510 use std::cell::RefCell;
2511 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2512 let sink = ends.clone();
2513 reset_layout_runtime();
2514 let mut card = StyledContainer::new(
2515 LayoutStyle::new().width(100.0).height(100.0),
2516 |_r| RectStyle::default(),
2517 vec![],
2518 )
2519 .unwrap()
2520 .on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
2521 compute_layout(
2522 card.layout_node(),
2523 AvailableSpace::Definite(100.0),
2524 AvailableSpace::Definite(100.0),
2525 )
2526 .unwrap();
2527
2528 let moved = |x: f64, y: f64| Event::PointerMoved {
2529 x,
2530 y,
2531 source: PointerSource::Mouse,
2532 };
2533 card.on_event(&moved(10.0, 10.0));
2534 assert!(ends.borrow().is_empty(), "a move with no drag ends nothing");
2535
2536 card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
2537 card.on_event(&moved(70.0, 30.0));
2538 assert!(ends.borrow().is_empty(), "still dragging");
2539 card.on_event(&release(90.0, 40.0, PointerSource::Mouse));
2540 assert_eq!(
2541 *ends.borrow(),
2542 vec![(90.0, 40.0)],
2543 "the release position, not the last move — a drag can end past it"
2544 );
2545
2546 card.on_event(&release(95.0, 45.0, PointerSource::Mouse));
2548 assert_eq!(ends.borrow().len(), 1);
2549 }
2550
2551 #[test]
2556 fn a_drag_survives_the_cursor_leaving_the_window() {
2557 use std::cell::RefCell;
2558 let moves: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2559 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2560 let move_sink = moves.clone();
2561 let end_sink = ends.clone();
2562 reset_layout_runtime();
2563 let mut card = StyledContainer::new(
2564 LayoutStyle::new().width(100.0).height(100.0),
2565 |_r| RectStyle::default(),
2566 vec![],
2567 )
2568 .unwrap()
2569 .on_drag(move |x, y| move_sink.borrow_mut().push((x, y)))
2570 .on_drag_end(move |x, y| end_sink.borrow_mut().push((x, y)));
2571 compute_layout(
2572 card.layout_node(),
2573 AvailableSpace::Definite(100.0),
2574 AvailableSpace::Definite(100.0),
2575 )
2576 .unwrap();
2577
2578 card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
2579 card.on_event(&Event::PointerMoved {
2580 x: 60.0,
2581 y: 25.0,
2582 source: PointerSource::Mouse,
2583 });
2584 card.on_event(&Event::CursorLeft);
2585 assert!(
2586 ends.borrow().is_empty(),
2587 "leaving the window does not finish the drag"
2588 );
2589
2590 card.on_event(&Event::PointerMoved {
2592 x: -15.0,
2593 y: 25.0,
2594 source: PointerSource::Mouse,
2595 });
2596 assert_eq!(
2597 moves.borrow().last().copied(),
2598 Some((-15.0, 25.0)),
2599 "the drag is still reporting after the pointer left"
2600 );
2601
2602 card.on_event(&release(-15.0, 25.0, PointerSource::Mouse));
2603 assert_eq!(
2604 *ends.borrow(),
2605 vec![(-15.0, 25.0)],
2606 "the release is what ends it, wherever it lands"
2607 );
2608 }
2609
2610 #[test]
2615 fn losing_window_focus_ends_a_live_drag() {
2616 use std::cell::RefCell;
2617 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2618 let sink = ends.clone();
2619 reset_layout_runtime();
2620 let mut card = StyledContainer::new(
2621 LayoutStyle::new().width(100.0).height(100.0),
2622 |_r| RectStyle::default(),
2623 vec![],
2624 )
2625 .unwrap()
2626 .on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
2627 compute_layout(
2628 card.layout_node(),
2629 AvailableSpace::Definite(100.0),
2630 AvailableSpace::Definite(100.0),
2631 )
2632 .unwrap();
2633
2634 card.on_event(&press(20.0, 20.0, PointerSource::Mouse));
2635 card.on_event(&Event::PointerMoved {
2636 x: 60.0,
2637 y: 25.0,
2638 source: PointerSource::Mouse,
2639 });
2640 card.on_event(&Event::FocusChanged { is_focused: false });
2641 assert_eq!(
2642 *ends.borrow(),
2643 vec![(60.0, 25.0)],
2644 "the last position the drag reached, since the loss carries none of its own"
2645 );
2646
2647 card.on_event(&Event::FocusChanged { is_focused: true });
2649 card.on_event(&Event::PointerMoved {
2650 x: 70.0,
2651 y: 30.0,
2652 source: PointerSource::Mouse,
2653 });
2654 assert_eq!(ends.borrow().len(), 1);
2655 }
2656
2657 #[test]
2660 fn on_drag_end_works_without_an_on_drag() {
2661 use std::cell::RefCell;
2662 let ends: Rc<RefCell<Vec<(f32, f32)>>> = Rc::new(RefCell::new(Vec::new()));
2663 let sink = ends.clone();
2664 reset_layout_runtime();
2665 let mut card = StyledContainer::new(
2666 LayoutStyle::new().width(100.0).height(100.0),
2667 |_r| RectStyle::default(),
2668 vec![],
2669 )
2670 .unwrap()
2671 .on_drag_end(move |x, y| sink.borrow_mut().push((x, y)));
2672 compute_layout(
2673 card.layout_node(),
2674 AvailableSpace::Definite(100.0),
2675 AvailableSpace::Definite(100.0),
2676 )
2677 .unwrap();
2678 card.on_event(&press(10.0, 10.0, PointerSource::Mouse));
2679 card.on_event(&release(80.0, 10.0, PointerSource::Mouse));
2680 assert_eq!(*ends.borrow(), vec![(80.0, 10.0)]);
2681 }
2682
2683 #[test]
2685 fn on_focus_fires_on_gain_and_loss() {
2686 use std::cell::RefCell;
2687 let seen: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
2688 let sink = seen.clone();
2689 reset_layout_runtime();
2690 let mut card = StyledContainer::new(
2691 LayoutStyle::new().flex_column().width(100.0).height(100.0),
2692 |_r| RectStyle::default(),
2693 vec![],
2694 )
2695 .unwrap()
2696 .on_focus(move |f| sink.borrow_mut().push(f));
2697 compute_layout(
2698 card.layout_node(),
2699 AvailableSpace::Definite(100.0),
2700 AvailableSpace::Definite(100.0),
2701 )
2702 .unwrap();
2703
2704 card.on_event(&press(50.0, 50.0, PointerSource::Mouse)); crate::focus::clear(); assert_eq!(
2707 *seen.borrow(),
2708 vec![true, false],
2709 "on_focus fires true on gain then false on loss"
2710 );
2711 }
2712
2713 #[test]
2715 fn maybe_on_focus_of_none_does_not_join_the_tab_order() {
2716 reset_layout_runtime();
2717 focus::clear();
2718 let card = StyledContainer::new(
2719 LayoutStyle::new().width(80.0).height(30.0),
2720 |_r| RectStyle::default(),
2721 vec![],
2722 )
2723 .unwrap()
2724 .maybe_on_focus(None::<fn(bool)>);
2725 assert!(card.focusable.id.is_none(), "no handler, no focus id");
2726
2727 focus::focus_next();
2728 assert!(focus::exposed().is_empty(), "and it is not a tab stop");
2729 }
2730
2731 #[test]
2732 fn maybe_on_focus_of_some_fires_like_on_focus() {
2733 use std::cell::RefCell;
2734 let seen: Rc<RefCell<Vec<bool>>> = Rc::new(RefCell::new(Vec::new()));
2735 let sink = seen.clone();
2736 reset_layout_runtime();
2737 let mut card = StyledContainer::new(
2738 LayoutStyle::new().flex_column().width(100.0).height(100.0),
2739 |_r| RectStyle::default(),
2740 vec![],
2741 )
2742 .unwrap()
2743 .maybe_on_focus(Some(move |f| sink.borrow_mut().push(f)));
2744 compute_layout(
2745 card.layout_node(),
2746 AvailableSpace::Definite(100.0),
2747 AvailableSpace::Definite(100.0),
2748 )
2749 .unwrap();
2750
2751 card.on_event(&press(50.0, 50.0, PointerSource::Mouse));
2752 crate::focus::clear();
2753 assert_eq!(*seen.borrow(), vec![true, false]);
2754 }
2755
2756 #[test]
2759 fn pressable_publishes_rect_to_interactive_registry_and_withdraws_on_drop() {
2760 use crate::interactive_rects;
2761 reset_layout_runtime();
2762 let baseline = interactive_rects().len();
2763 let card = StyledContainer::new(
2764 LayoutStyle::new().width(120.0).height(40.0),
2765 |_r| RectStyle::default(),
2766 vec![],
2767 )
2768 .unwrap()
2769 .on_press(|| {});
2770 let node = card.layout_node();
2771 assert_eq!(
2773 interactive_rects().len(),
2774 baseline,
2775 "an unlaid-out pressable contributes no rect"
2776 );
2777 compute_layout(
2778 node,
2779 AvailableSpace::Definite(120.0),
2780 AvailableSpace::Definite(40.0),
2781 )
2782 .unwrap();
2783 let rects = interactive_rects();
2784 assert_eq!(rects.len(), baseline + 1);
2785 assert!(
2786 rects.iter().any(|r| r.width == 120.0 && r.height == 40.0),
2787 "a laid-out pressable reports its rect"
2788 );
2789 drop(card);
2790 assert_eq!(
2791 interactive_rects().len(),
2792 baseline,
2793 "dropping the pressable withdraws its rect"
2794 );
2795 }
2796
2797 #[test]
2801 fn a_kept_effect_lives_exactly_as_long_as_its_widget() {
2802 crate::reset_layout_runtime();
2803 reactive_core::reset_runtime();
2804 let source = signal(0i32);
2805 let seen = std::rc::Rc::new(std::cell::Cell::new(0i32));
2806
2807 let watched = source.clone();
2808 let sink = seen.clone();
2809 let boxed = StyledContainer::new(LayoutStyle::new(), |_r| RectStyle::default(), vec![])
2810 .unwrap()
2811 .keeping(effect(move || sink.set(watched.get())));
2812
2813 source.set(7);
2814 assert_eq!(seen.get(), 7, "the effect runs while the widget is alive");
2815
2816 drop(boxed);
2817 source.set(9);
2818 assert_eq!(
2819 seen.get(),
2820 7,
2821 "and stops when the widget goes, rather than firing at a node that is gone"
2822 );
2823 }
2824}