1use frust_core::{
14 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
15 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, ViewSeq, Widget, any,
16};
17use kurbo::{Point, Rect, Size};
18
19use crate::ChildKey;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub enum Axis {
24 Horizontal,
26 Vertical,
28}
29
30impl Axis {
31 fn main_of(self, size: Size) -> f64 {
33 match self {
34 Axis::Horizontal => size.width,
35 Axis::Vertical => size.height,
36 }
37 }
38
39 fn cross_of(self, size: Size) -> f64 {
41 match self {
42 Axis::Horizontal => size.height,
43 Axis::Vertical => size.width,
44 }
45 }
46
47 fn size(self, main: f64, cross: f64) -> Size {
49 match self {
50 Axis::Horizontal => Size::new(main, cross),
51 Axis::Vertical => Size::new(cross, main),
52 }
53 }
54
55 fn point(self, main: f64, cross: f64) -> Point {
57 match self {
58 Axis::Horizontal => Point::new(main, cross),
59 Axis::Vertical => Point::new(cross, main),
60 }
61 }
62}
63
64#[derive(Clone, Copy, Debug, PartialEq, Eq)]
66pub enum CrossAxisAlignment {
67 Start,
69 Center,
71 Stretch,
73}
74
75#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub enum MainAxisAlignment {
81 Start,
83}
84
85pub struct FlexChild<State: 'static> {
94 view: AnyView<State>,
95 flex: u32,
96 key: Option<ChildKey>,
97}
98
99pub fn flexible<State: 'static, V: View<State>>(flex: u32, view: V) -> FlexChild<State> {
101 FlexChild {
102 view: any(view),
103 flex,
104 key: None,
105 }
106}
107
108pub fn inflexible<State: 'static, V: View<State>>(view: V) -> FlexChild<State> {
110 FlexChild {
111 view: any(view),
112 flex: 0,
113 key: None,
114 }
115}
116
117pub fn keyed<State: 'static, V: View<State>>(
151 key: impl Into<ChildKey>,
152 view: V,
153) -> FlexChild<State> {
154 FlexChild {
155 view: any(view),
156 flex: 0,
157 key: Some(key.into()),
158 }
159}
160
161pub struct FlexView<State: 'static> {
163 direction: Axis,
164 cross: CrossAxisAlignment,
165 main: MainAxisAlignment,
166 children: Vec<FlexChild<State>>,
167}
168
169impl<State: 'static> FlexView<State> {
170 pub fn new(direction: Axis, children: Vec<FlexChild<State>>) -> Self {
173 Self {
174 direction,
175 cross: CrossAxisAlignment::Start,
176 main: MainAxisAlignment::Start,
177 children,
178 }
179 }
180
181 pub fn child<V: View<State>>(mut self, view: V) -> Self {
184 self.children.push(inflexible(view));
185 self
186 }
187
188 pub fn flex<V: View<State>>(mut self, flex: u32, view: V) -> Self {
191 self.children.push(flexible(flex, view));
192 self
193 }
194
195 pub fn keyed<V: View<State>>(mut self, key: impl Into<ChildKey>, view: V) -> Self {
198 self.children.push(keyed(key, view));
199 self
200 }
201
202 pub fn children<M>(mut self, children: impl ViewSeq<State, M>) -> Self {
211 let mut erased = Vec::new();
212 children.extend_views(&mut erased);
213 self.children
214 .extend(erased.into_iter().map(|view| FlexChild {
215 view,
216 flex: 0,
217 key: None,
218 }));
219 self
220 }
221
222 pub fn push(mut self, child: FlexChild<State>) -> Self {
225 self.children.push(child);
226 self
227 }
228
229 pub fn when(self, cond: bool, f: impl FnOnce(Self) -> Self) -> Self {
231 if cond { f(self) } else { self }
232 }
233
234 pub fn when_some<T>(self, opt: Option<T>, f: impl FnOnce(Self, T) -> Self) -> Self {
236 match opt {
237 Some(value) => f(self, value),
238 None => self,
239 }
240 }
241
242 pub fn cross_axis(mut self, cross: CrossAxisAlignment) -> Self {
244 self.cross = cross;
245 self
246 }
247
248 pub fn main_axis(mut self, main: MainAxisAlignment) -> Self {
250 self.main = main;
251 self
252 }
253}
254
255pub fn column<State: 'static>() -> FlexView<State> {
268 FlexView::new(Axis::Vertical, Vec::new())
269}
270
271pub fn row<State: 'static>() -> FlexView<State> {
273 FlexView::new(Axis::Horizontal, Vec::new())
274}
275
276#[allow(non_snake_case)]
296pub fn Row<State: 'static, M>(children: impl ViewSeq<State, M>) -> FlexView<State> {
297 FlexView::new(Axis::Horizontal, Vec::new()).children(children)
298}
299
300#[allow(non_snake_case)]
320pub fn Column<State: 'static, M>(children: impl ViewSeq<State, M>) -> FlexView<State> {
321 FlexView::new(Axis::Vertical, Vec::new()).children(children)
322}
323
324pub struct FlexWidget {
327 direction: Axis,
328 cross: CrossAxisAlignment,
329 main: MainAxisAlignment,
330 children: Vec<ChildPod>,
331 flex: Vec<u32>,
332}
333
334fn child_constraints(
340 axis: Axis,
341 main_min: f64,
342 main_max: f64,
343 cross_bound: f64,
344 stretch: bool,
345 cross_max: f64,
346) -> BoxConstraints {
347 let cross_min = if stretch { cross_bound } else { 0.0 };
348 let cross_hi = if stretch { cross_bound } else { cross_max };
349 BoxConstraints::new(
350 axis.size(main_min, cross_min),
351 axis.size(main_max, cross_hi),
352 )
353}
354
355impl<State: 'static> View<State> for FlexView<State> {
356 type Element = FlexWidget;
357
358 fn build(&self, ctx: &mut BuildCtx<'_>) -> FlexWidget {
359 let mut children = Vec::with_capacity(self.children.len());
360 let mut flex = Vec::with_capacity(self.children.len());
361 for child in &self.children {
362 children.push(crate::authoring::build_child(&child.view, ctx));
363 flex.push(child.flex);
364 }
365 FlexWidget {
366 direction: self.direction,
367 cross: self.cross,
368 main: self.main,
369 children,
370 flex,
371 }
372 }
373
374 fn rebuild(
375 &self,
376 prev: &Self,
377 element: &mut FlexWidget,
378 ctx: &mut BuildCtx<'_>,
379 ) -> ChangeFlags {
380 let mut flags = ChangeFlags::NONE;
381 if prev.direction != self.direction || prev.cross != self.cross || prev.main != self.main {
382 element.direction = self.direction;
383 element.cross = self.cross;
384 element.main = self.main;
385 flags |= ChangeFlags::LAYOUT;
386 }
387
388 flags |= crate::authoring::rebuild_children(
393 &prev.children,
394 &self.children,
395 &mut element.children,
396 ctx,
397 |child| &child.view,
398 |child| child.key,
399 );
400
401 let new_flex: Vec<u32> = self.children.iter().map(|child| child.flex).collect();
407 if new_flex != element.flex {
408 element.flex = new_flex;
409 flags |= ChangeFlags::LAYOUT;
410 }
411
412 flags
413 }
414
415 fn teardown(&self, element: &mut FlexWidget, ctx: &mut BuildCtx<'_>) {
416 for (child, pod) in self.children.iter().zip(element.children.iter_mut()) {
417 crate::authoring::teardown_child(&child.view, pod, ctx);
418 }
419 }
420}
421
422impl Widget for FlexWidget {
423 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
424 let axis = self.direction;
425 let max_main = axis.main_of(bc.max());
426 let max_cross = axis.cross_of(bc.max());
427 let stretch = self.cross == CrossAxisAlignment::Stretch;
428 let cross_bound = if max_cross.is_finite() {
431 max_cross
432 } else {
433 0.0
434 };
435
436 let mut allocated_main = 0.0f64;
439 let mut total_flex = 0u32;
440 let mut max_child_cross = 0.0f64;
441 for (i, pod) in self.children.iter_mut().enumerate() {
442 if self.flex[i] == 0 {
443 let cbc =
444 child_constraints(axis, 0.0, f64::INFINITY, cross_bound, stretch, max_cross);
445 let size = pod.layout_child(ctx, &cbc);
446 allocated_main += axis.main_of(size);
447 max_child_cross = max_child_cross.max(axis.cross_of(size));
448 } else {
449 total_flex += self.flex[i];
450 }
451 }
452
453 let free = if max_main.is_finite() {
457 (max_main - allocated_main).max(0.0)
458 } else {
459 0.0
460 };
461 if total_flex > 0 {
462 for (i, pod) in self.children.iter_mut().enumerate() {
463 if self.flex[i] > 0 {
464 let share = free * (self.flex[i] as f64) / (total_flex as f64);
465 let cbc =
466 child_constraints(axis, share, share, cross_bound, stretch, max_cross);
467 let size = pod.layout_child(ctx, &cbc);
468 max_child_cross = max_child_cross.max(axis.cross_of(size));
469 }
470 }
471 }
472
473 let main_size = if total_flex > 0 && max_main.is_finite() {
476 max_main
477 } else {
478 allocated_main
479 };
480 let cross_size = if stretch && max_cross.is_finite() {
482 max_cross
483 } else {
484 max_child_cross
485 };
486
487 let leading = match self.main {
490 MainAxisAlignment::Start => 0.0,
491 };
492 let mut main_pos = leading;
493 for pod in &mut self.children {
494 let child_cross = axis.cross_of(pod.size());
495 let cross_pos = match self.cross {
496 CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => 0.0,
497 CrossAxisAlignment::Center => (cross_size - child_cross) / 2.0,
498 };
499 pod.set_origin(axis.point(main_pos, cross_pos));
500 main_pos += axis.main_of(pod.size());
501 }
502
503 bc.constrain(axis.size(main_size, cross_size))
504 }
505
506 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
507 let cull = ctx
550 .visible_rect()
551 .map(|vr| vr.inflate(vr.width(), vr.height()));
552 let hero_in_flight = ctx.hero_active();
553 for pod in &mut self.children {
555 if let Some(warm) = cull {
556 let exempt = hero_in_flight || pod.is_focused();
557 if !exempt {
558 let child_abs =
559 Rect::from_origin_size(ctx.origin() + pod.origin().to_vec2(), pod.size());
560 if !child_abs.overlaps(warm) {
561 continue;
562 }
563 }
564 }
565 pod.paint_child(ctx, scene);
566 }
567 }
568
569 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
570 crate::authoring::route_event(&mut self.children, ctx, event)
572 }
573
574 fn semantics(&self, ctx: &mut SemanticsCtx) {
575 for pod in &self.children {
578 pod.semantics_child(ctx);
579 }
580 }
581
582 crate::authoring::visit_children!(children);
583}
584
585#[cfg(test)]
586mod tests {
587 use super::*;
588 use crate::test_support::{RecordingScene, leaf};
589
590 fn ctx(counter: &mut u64) -> BuildCtx<'_> {
591 BuildCtx::new(counter)
592 }
593
594 fn build<S: 'static>(view: &FlexView<S>) -> FlexWidget {
595 let mut counter = 0u64;
596 view.build(&mut ctx(&mut counter))
597 }
598
599 #[test]
600 fn distributes_free_space_by_flex_factors() {
601 let view: FlexView<()> = FlexView::new(
604 Axis::Horizontal,
605 vec![
606 flexible(2, leaf(1000.0, 20.0)),
607 flexible(1, leaf(1000.0, 20.0)),
608 ],
609 );
610 let mut w = build(&view);
611 let mut lctx = LayoutCtx::new();
612 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
613
614 assert_eq!(w.children[0].size().width, 200.0);
615 assert_eq!(w.children[1].size().width, 100.0);
616 assert_eq!(w.children[0].origin().x, 0.0);
617 assert_eq!(w.children[1].origin().x, 200.0);
618 assert_eq!(size.width, 300.0);
620 }
621
622 #[test]
623 fn mixes_inflexible_and_flexible() {
624 let view: FlexView<()> = FlexView::new(
627 Axis::Horizontal,
628 vec![
629 inflexible(leaf(50.0, 20.0)),
630 flexible(1, leaf(1000.0, 20.0)),
631 ],
632 );
633 let mut w = build(&view);
634 let mut lctx = LayoutCtx::new();
635 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
636
637 assert_eq!(w.children[0].size().width, 50.0);
638 assert_eq!(w.children[1].size().width, 150.0);
639 assert_eq!(w.children[0].origin().x, 0.0);
640 assert_eq!(w.children[1].origin().x, 50.0);
641 assert_eq!(size.width, 200.0);
642 }
643
644 #[test]
645 fn shrink_wraps_main_axis_without_flexible_children() {
646 let view: FlexView<()> = row()
649 .child(leaf(40.0, 10.0).into_any())
650 .child(leaf(20.0, 10.0).into_any());
651 let mut w = build(&view);
652 let mut lctx = LayoutCtx::new();
653 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
654 assert_eq!(size.width, 60.0);
655 assert_eq!(w.children[1].origin().x, 40.0);
656 }
657
658 #[test]
659 fn cross_axis_stretch_tightens_children() {
660 let view: FlexView<()> = row()
663 .child(leaf(30.0, 10.0).into_any())
664 .cross_axis(CrossAxisAlignment::Stretch);
665 let mut w = build(&view);
666 let mut lctx = LayoutCtx::new();
667 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 80.0)));
668 assert_eq!(w.children[0].size().height, 80.0);
669 assert_eq!(size.height, 80.0);
670 }
671
672 #[test]
673 fn cross_axis_start_packs_at_zero() {
674 let view: FlexView<()> = row()
676 .child(leaf(10.0, 40.0).into_any())
677 .child(leaf(10.0, 20.0).into_any())
678 .cross_axis(CrossAxisAlignment::Start);
679 let mut w = build(&view);
680 let mut lctx = LayoutCtx::new();
681 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
682 assert_eq!(size.height, 40.0);
683 assert_eq!(w.children[1].origin().y, 0.0);
684 }
685
686 #[test]
687 fn cross_axis_center_centers_shorter_children() {
688 let view: FlexView<()> = row()
690 .child(leaf(10.0, 40.0).into_any())
691 .child(leaf(10.0, 20.0).into_any())
692 .cross_axis(CrossAxisAlignment::Center);
693 let mut w = build(&view);
694 let mut lctx = LayoutCtx::new();
695 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
696 assert_eq!(w.children[1].origin().y, 10.0);
697 }
698
699 #[test]
700 fn column_lays_out_along_vertical_axis() {
701 let view: FlexView<()> = column()
703 .child(leaf(30.0, 15.0).into_any())
704 .child(leaf(30.0, 25.0).into_any());
705 let mut w = build(&view);
706 let mut lctx = LayoutCtx::new();
707 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 300.0)));
708 assert_eq!(size.height, 40.0); assert_eq!(w.children[0].origin().y, 0.0);
710 assert_eq!(w.children[1].origin().y, 15.0);
711 }
712
713 #[test]
714 fn paints_children_in_order() {
715 let view: FlexView<()> = row()
716 .child(leaf(20.0, 20.0).into_any())
717 .child(leaf(20.0, 20.0).into_any());
718 let mut w = build(&view);
719 let mut lctx = LayoutCtx::new();
720 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 200.0)));
721
722 let mut scene = RecordingScene::default();
723 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
724 w.paint(&mut pctx, &mut scene);
725 assert_eq!(scene.rects[0].0, Point::new(0.0, 0.0));
727 assert_eq!(scene.rects[1].0, Point::new(20.0, 0.0));
728 }
729
730 fn culling_column() -> FlexWidget {
735 let view: FlexView<()> = column()
736 .child(leaf(100.0, 100.0).into_any())
737 .child(leaf(100.0, 100.0).into_any())
738 .child(leaf(100.0, 100.0).into_any())
739 .child(leaf(100.0, 100.0).into_any())
740 .child(leaf(100.0, 100.0).into_any());
741 let mut w = build(&view);
742 let mut lctx = LayoutCtx::new();
743 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 500.0)));
744 w
745 }
746
747 #[test]
748 fn no_visible_rect_paints_every_child() {
749 let mut w = culling_column();
751 let mut scene = RecordingScene::default();
752 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 500.0));
753 w.paint(&mut pctx, &mut scene);
754 assert_eq!(scene.rects.len(), 5, "no cull → all five rows painted");
755 }
756
757 #[test]
758 fn culls_children_fully_outside_visible_rect_plus_margin() {
759 let mut w = culling_column();
765 let mut scene = RecordingScene::default();
766 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 500.0));
767 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
768 w.paint(&mut pctx, &mut scene);
769 assert_eq!(scene.rects.len(), 3, "two below-the-warm-band rows culled");
770 assert_eq!(scene.rects[0].0, Point::new(0.0, 0.0));
771 assert_eq!(scene.rects[1].0, Point::new(0.0, 100.0));
772 assert_eq!(scene.rects[2].0, Point::new(0.0, 200.0));
774 }
775
776 #[test]
777 fn margin_boundary_row_just_past_the_warm_band_is_culled() {
778 let mut w = culling_column();
784 let mut scene = RecordingScene::default();
785 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 500.0));
786 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 50.0)));
787 w.paint(&mut pctx, &mut scene);
788 assert_eq!(scene.rects.len(), 2, "only the top band rows survive");
789 assert_eq!(scene.rects[0].0, Point::new(0.0, 0.0));
790 assert_eq!(scene.rects[1].0, Point::new(0.0, 100.0));
791 }
792
793 #[test]
794 fn culled_child_still_receives_events_at_its_layout_geometry() {
795 let mut counter = 0u64;
800 let view: FlexView<Vec<u32>> = column()
801 .child(captor(0))
802 .child(captor(1))
803 .child(captor(2))
804 .child(captor(3))
805 .child(captor(4))
806 .child(captor(5))
807 .child(captor(6))
808 .child(captor(7));
809 let mut w = view.build(&mut ctx(&mut counter));
810 layout_column(&mut w);
811
812 let mut scene = RecordingScene::default();
815 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 8.0));
816 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(ROW_W, ROW_H)));
817 w.paint(&mut pctx, &mut scene);
818
819 let mut log: Vec<u32> = Vec::new();
822 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(7)));
823 assert!(
824 w.children[7].is_active(),
825 "a culled row still captures on Down"
826 );
827 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(7)));
828 assert_eq!(log, vec![7], "a culled-but-laid-out row still fires on Up");
829 }
830
831 struct ImeLeaf {
843 value: String,
844 painted: Rc<Cell<u32>>,
845 }
846 struct ImeLeafWidget {
848 value: String,
849 painted: Rc<Cell<u32>>,
850 }
851
852 impl View<()> for ImeLeaf {
853 type Element = ImeLeafWidget;
854 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeLeafWidget {
855 ImeLeafWidget {
856 value: self.value.clone(),
857 painted: self.painted.clone(),
858 }
859 }
860 fn rebuild(
861 &self,
862 _prev: &Self,
863 element: &mut ImeLeafWidget,
864 _ctx: &mut BuildCtx<'_>,
865 ) -> ChangeFlags {
866 element.value = self.value.clone();
869 ChangeFlags::NONE
870 }
871 }
872
873 impl Widget for ImeLeafWidget {
874 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
875 bc.constrain(Size::new(ROW_W, ROW_H))
876 }
877 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
878 use frust_core::{EditingState, ImeState};
879 self.painted.set(self.painted.get() + 1);
880 scene.fill_rect(ctx.origin(), ctx.size(), peniko::Color::BLACK);
881 ctx.publish_ime_state(ImeState {
882 active: true,
883 editing: EditingState {
884 text: self.value.clone(),
885 selection_base: -1,
886 selection_extent: -1,
887 composing_base: -1,
888 composing_extent: -1,
889 },
890 caret: None,
891 content_type: Default::default(),
892 suppress_soft_keyboard: false,
893 });
894 }
895 }
896
897 fn ime_column() -> (FlexWidget, [Rc<Cell<u32>>; 5]) {
900 let counts: [Rc<Cell<u32>>; 5] = std::array::from_fn(|_| Rc::new(Cell::new(0)));
901 let view: FlexView<()> = Column(
902 (0..5)
903 .map(|i| {
904 any(ImeLeaf {
905 value: format!("row{i}"),
906 painted: counts[i].clone(),
907 })
908 })
909 .collect::<Vec<_>>(),
910 );
911 let mut counter = 0u64;
912 let mut w = view.build(&mut ctx(&mut counter));
913 layout_column(&mut w);
914 (w, counts)
915 }
916
917 fn top_row_rect() -> Rect {
920 Rect::from_origin_size(Point::ZERO, Size::new(ROW_W, ROW_H))
921 }
922
923 #[test]
924 fn focused_child_beyond_warm_band_still_paints_and_republishes_ime() {
925 let (mut w, counts) = ime_column();
929 w.children[4].set_focused(true);
930
931 let mut scene = RecordingScene::default();
932 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
933 pctx.constrain_visible_rect(top_row_rect());
934 w.paint(&mut pctx, &mut scene);
935
936 assert_eq!(counts[0].get(), 1, "warm row 0 paints");
937 assert_eq!(counts[1].get(), 1, "warm row 1 paints");
938 assert_eq!(counts[2].get(), 1, "warm boundary row 2 paints");
939 assert_eq!(
940 counts[3].get(),
941 0,
942 "unfocused offscreen row 3 stays culled (suppression intact)"
943 );
944 assert_eq!(
945 counts[4].get(),
946 1,
947 "focused offscreen row 4 is exempt from culling and paints"
948 );
949 let ime = pctx
952 .take_ime_state()
953 .expect("focused row republished its IME");
954 assert_eq!(ime.editing.text, "row4");
955 }
956
957 #[test]
958 fn focused_cull_exemption_republishes_a_rebuild_mutation_immediately() {
959 let far = Rect::from_origin_size(Point::new(0.0, 10_000.0), Size::new(ROW_W, ROW_H));
964
965 let mut counter = 0u64;
967 let prev: FlexView<()> = column().child(ImeLeaf {
968 value: "v1".to_string(),
969 painted: Rc::new(Cell::new(0)),
970 });
971 let mut w = prev.build(&mut ctx(&mut counter));
972 layout_column(&mut w);
973 let next: FlexView<()> = column().child(ImeLeaf {
975 value: "v2".to_string(),
976 painted: Rc::new(Cell::new(0)),
977 });
978 next.rebuild(&prev, &mut w, &mut ctx(&mut counter));
979 let mut scene = RecordingScene::default();
980 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
981 pctx.constrain_visible_rect(far);
982 w.paint(&mut pctx, &mut scene);
983 assert!(
984 pctx.take_ime_state().is_none(),
985 "an unfocused, culled field never republishes — its IME goes stale"
986 );
987
988 let mut counter = 0u64;
990 let prev: FlexView<()> = column().child(ImeLeaf {
991 value: "v1".to_string(),
992 painted: Rc::new(Cell::new(0)),
993 });
994 let mut w = prev.build(&mut ctx(&mut counter));
995 layout_column(&mut w);
996 w.children[0].set_focused(true);
997 let next: FlexView<()> = column().child(ImeLeaf {
998 value: "v2".to_string(),
999 painted: Rc::new(Cell::new(0)),
1000 });
1001 next.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1002 let mut scene = RecordingScene::default();
1003 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
1004 pctx.constrain_visible_rect(far);
1005 w.paint(&mut pctx, &mut scene);
1006 let ime = pctx
1007 .take_ime_state()
1008 .expect("focused field republishes even while offscreen");
1009 assert_eq!(
1010 ime.editing.text, "v2",
1011 "the rebuild-mutated value republished immediately, not stale 'v1'"
1012 );
1013 }
1014
1015 #[test]
1016 fn animated_scale_child_straddling_the_cull_boundary_does_not_pop() {
1017 use frust_core::Curve;
1026 use std::time::Duration;
1027 let snap = crate::Timing::Duration(Duration::ZERO, Curve::Linear);
1028
1029 let view: FlexView<()> = column()
1032 .child(leaf(ROW_W, ROW_H).into_any())
1033 .child(leaf(ROW_W, ROW_H).into_any())
1034 .child(crate::motion::AnimatedScale(2.8, leaf(ROW_W, ROW_H)).timing(snap))
1035 .child(leaf(ROW_W, ROW_H).into_any())
1036 .child(crate::motion::AnimatedScale(2.8, leaf(ROW_W, ROW_H)).timing(snap));
1037 let mut counter = 0u64;
1038 let mut w = view.build(&mut ctx(&mut counter));
1039 layout_column(&mut w);
1040
1041 let mut scene = RecordingScene::default();
1042 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
1043 pctx.constrain_visible_rect(top_row_rect());
1044 w.paint(&mut pctx, &mut scene);
1045
1046 assert_eq!(
1049 scene.rects.len(),
1050 3,
1051 "the boundary AnimatedScale row paints on its layout box, the two \
1052 fully-outside rows (one of them also AnimatedScale) are culled"
1053 );
1054 assert!(
1057 scene.transforms.iter().any(|t| {
1058 let c = t.as_coeffs();
1059 (c[0] - 2.8).abs() < 1e-6 && (c[3] - 2.8).abs() < 1e-6
1060 }),
1061 "the boundary row composited at 2.8× without being culled by its \
1062 transform-overflowed visual bounds"
1063 );
1064 }
1065
1066 #[test]
1067 fn children_vec_diff_adds_removes_and_type_swaps() {
1068 let mut counter = 0u64;
1070 let prev: FlexView<()> = row()
1071 .child(leaf(10.0, 10.0).into_any())
1072 .child(leaf(10.0, 10.0).into_any());
1073 let mut w = prev.build(&mut ctx(&mut counter));
1074 assert_eq!(w.children.len(), 2);
1075
1076 let grown: FlexView<()> = row()
1078 .child(leaf(10.0, 10.0).into_any())
1079 .child(leaf(10.0, 10.0).into_any())
1080 .child(leaf(10.0, 10.0).into_any());
1081 let flags = grown.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1082 assert_eq!(w.children.len(), 3);
1083 assert!(flags.needs_layout());
1084
1085 let shrunk: FlexView<()> = row().child(leaf(10.0, 10.0).into_any());
1087 shrunk.rebuild(&grown, &mut w, &mut ctx(&mut counter));
1088 assert_eq!(w.children.len(), 1);
1089 assert_eq!(w.flex.len(), 1);
1090
1091 let swapped: FlexView<()> = row().child(crate::test_support::swap_leaf().into_any());
1093 swapped.rebuild(&shrunk, &mut w, &mut ctx(&mut counter));
1094 assert_eq!(w.children.len(), 1);
1095 let mut lctx = LayoutCtx::new();
1097 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
1098 assert_eq!(w.children[0].size(), Size::new(7.0, 7.0));
1099 }
1100
1101 use std::any::Any;
1111 use std::cell::Cell;
1112 use std::rc::Rc;
1113
1114 use frust_core::{
1115 EventCtx, Key, KeyEvent, Modifiers, PointerButton, PointerEvent, PointerPhase, any,
1116 };
1117
1118 const ROW_W: f64 = 50.0;
1119 const ROW_H: f64 = 20.0;
1120
1121 struct Captor {
1123 id: u32,
1124 }
1125 struct CaptorWidget {
1127 id: u32,
1128 armed: bool,
1129 }
1130
1131 fn captor(id: u32) -> impl View<Vec<u32>> {
1133 Captor { id }
1134 }
1135
1136 impl View<Vec<u32>> for Captor {
1137 type Element = CaptorWidget;
1138 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptorWidget {
1139 CaptorWidget {
1140 id: self.id,
1141 armed: false,
1142 }
1143 }
1144 fn rebuild(
1145 &self,
1146 _prev: &Self,
1147 element: &mut CaptorWidget,
1148 _ctx: &mut BuildCtx<'_>,
1149 ) -> ChangeFlags {
1150 element.id = self.id;
1151 ChangeFlags::NONE
1152 }
1153 }
1154
1155 impl Widget for CaptorWidget {
1156 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1157 bc.constrain(Size::new(ROW_W, ROW_H))
1158 }
1159 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1160 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1161 let InputEvent::Pointer(p) = event else {
1162 return EventResult::Ignored;
1163 };
1164 match p.phase {
1165 PointerPhase::Down => {
1166 self.armed = true;
1167 ctx.capture_pointer();
1168 EventResult::Handled
1169 }
1170 PointerPhase::Move => EventResult::Handled,
1171 PointerPhase::Up => {
1172 if self.armed {
1173 ctx.state_mut::<Vec<u32>>().push(self.id);
1174 }
1175 self.armed = false;
1176 EventResult::Handled
1177 }
1178 PointerPhase::Cancel => {
1179 self.armed = false;
1181 EventResult::Handled
1182 }
1183 }
1184 }
1185 }
1186
1187 struct Recorder {
1190 seen: Rc<Cell<u32>>,
1191 }
1192 struct RecorderWidget {
1194 seen: Rc<Cell<u32>>,
1195 }
1196
1197 impl View<Vec<u32>> for Recorder {
1198 type Element = RecorderWidget;
1199 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
1200 RecorderWidget {
1201 seen: self.seen.clone(),
1202 }
1203 }
1204 fn rebuild(
1205 &self,
1206 _prev: &Self,
1207 _element: &mut RecorderWidget,
1208 _ctx: &mut BuildCtx<'_>,
1209 ) -> ChangeFlags {
1210 ChangeFlags::NONE
1211 }
1212 }
1213
1214 impl Widget for RecorderWidget {
1215 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1216 bc.constrain(Size::new(ROW_W, ROW_H))
1217 }
1218 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1219 fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
1220 self.seen.set(self.seen.get() + 1);
1221 EventResult::Handled
1222 }
1223 }
1224
1225 fn ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
1226 InputEvent::Pointer(PointerEvent {
1227 phase,
1228 position: Point::new(x, y),
1229 button: PointerButton::Primary,
1230 })
1231 }
1232
1233 #[allow(clippy::ptr_arg)]
1238 fn dispatch(w: &mut FlexWidget, log: &mut Vec<u32>, event: &InputEvent) {
1239 let state: &mut dyn Any = log;
1240 let mut ectx = EventCtx::new(state, Point::ZERO, Size::new(ROW_W, ROW_H * 8.0));
1241 w.event(&mut ectx, event);
1242 }
1243
1244 fn layout_column(w: &mut FlexWidget) {
1246 let mut lctx = LayoutCtx::new();
1247 w.layout(
1248 &mut lctx,
1249 &BoxConstraints::loose(Size::new(ROW_W, ROW_H * 8.0)),
1250 );
1251 }
1252
1253 fn row_y(i: usize) -> f64 {
1255 i as f64 * ROW_H + ROW_H / 2.0
1256 }
1257
1258 #[test]
1259 fn append_after_preserves_captured_drag_before_change() {
1260 let mut counter = 0u64;
1266 let prev: FlexView<Vec<u32>> = column().child(captor(0)).child(captor(1)).child(captor(2));
1267 let mut w = prev.build(&mut ctx(&mut counter));
1268 layout_column(&mut w);
1269
1270 let mut log: Vec<u32> = Vec::new();
1271 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1272 assert!(w.children[1].is_active(), "row 1 captured the pointer");
1273
1274 let appended: FlexView<Vec<u32>> = column()
1276 .child(captor(0))
1277 .child(captor(1))
1278 .child(captor(2))
1279 .child(captor(3));
1280 appended.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1281 assert!(
1282 w.children[1].is_active(),
1283 "append-after preserves the captured row's active path (stable prefix)"
1284 );
1285
1286 layout_column(&mut w);
1288 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1289 assert_eq!(log, vec![1], "captured row fires on Up as normal");
1290 }
1291
1292 #[test]
1293 fn type_swap_before_armed_index_cancels_with_synthetic_cancel() {
1294 let mut counter = 0u64;
1300 let prev: FlexView<Vec<u32>> = column().child(captor(0)).child(captor(1)).child(captor(2));
1301 let mut w = prev.build(&mut ctx(&mut counter));
1302 layout_column(&mut w);
1303
1304 let mut log: Vec<u32> = Vec::new();
1305 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(2)));
1306 assert!(w.children[2].is_active(), "row 2 captured the pointer");
1307
1308 let seen = Rc::new(Cell::new(0u32));
1313 let swapped: FlexView<Vec<u32>> = column()
1314 .child(Recorder { seen })
1315 .child(captor(1))
1316 .child(captor(2));
1317 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1318 assert!(
1319 w.children.iter().all(|p| !p.is_active()),
1320 "swap before the armed index cancelled the tail's active path"
1321 );
1322
1323 layout_column(&mut w);
1324 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(2)));
1325 assert!(
1326 log.is_empty(),
1327 "no fire on Up — the armed row was synthetically cancelled"
1328 );
1329 }
1330
1331 #[test]
1332 fn structural_truncation_of_active_row_unwinds_without_panic() {
1333 let mut counter = 0u64;
1337 let prev: FlexView<Vec<u32>> = column()
1338 .child(captor(0))
1339 .child(captor(1))
1340 .child(captor(2))
1341 .child(captor(3));
1342 let mut w = prev.build(&mut ctx(&mut counter));
1343 layout_column(&mut w);
1344
1345 let mut log: Vec<u32> = Vec::new();
1346 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(2)));
1347 assert!(w.children[2].is_active());
1348
1349 let truncated: FlexView<Vec<u32>> = column().child(captor(0)).child(captor(1));
1351 truncated.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1352 assert_eq!(w.children.len(), 2);
1353 assert_eq!(w.flex.len(), 2);
1354 assert!(w.children.iter().all(|p| !p.is_active()));
1355
1356 layout_column(&mut w);
1358 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(2)));
1359 assert!(log.is_empty());
1360 }
1361
1362 #[test]
1363 fn type_swap_at_active_index_clears_without_notifying_fresh_widget() {
1364 let mut counter = 0u64;
1368 let prev: FlexView<Vec<u32>> = column().child(captor(0)).child(captor(1));
1369 let mut w = prev.build(&mut ctx(&mut counter));
1370 layout_column(&mut w);
1371
1372 let mut log: Vec<u32> = Vec::new();
1373 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1374 assert!(w.children[1].is_active());
1375
1376 let seen = Rc::new(Cell::new(0u32));
1378 let swapped: FlexView<Vec<u32>> = column()
1379 .child(captor(0))
1380 .child(Recorder { seen: seen.clone() });
1381 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1382
1383 assert!(!w.children[1].is_active(), "stale capture path dropped");
1384 assert_eq!(seen.get(), 0, "fresh widget received no synthetic event");
1385
1386 layout_column(&mut w);
1388 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1389 assert_eq!(seen.get(), 1, "fresh widget responds to a new gesture");
1390 }
1391
1392 #[test]
1393 fn content_only_rebuild_preserves_captured_drag() {
1394 let mut counter = 0u64;
1398 let prev: FlexView<Vec<u32>> = column().child(captor(0)).child(captor(1));
1399 let mut w = prev.build(&mut ctx(&mut counter));
1400 layout_column(&mut w);
1401
1402 let mut log: Vec<u32> = Vec::new();
1403 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1404 assert!(w.children[1].is_active());
1405
1406 let same: FlexView<Vec<u32>> = column().child(captor(0)).child(captor(1));
1408 same.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1409 assert!(
1410 w.children[1].is_active(),
1411 "content-only rebuild must NOT clear an in-flight capture"
1412 );
1413
1414 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1416 assert_eq!(log, vec![1], "captured row fires on Up as normal");
1417 }
1418
1419 #[test]
1428 fn focus_on_child_survives_append_after() {
1429 let mut counter = 0u64;
1434 let prev: FlexView<Vec<u32>> = column().child(FocusRow { id: 0 }).child(FocusRow { id: 1 });
1435 let mut w = prev.build(&mut ctx(&mut counter));
1436 layout_column(&mut w);
1437
1438 let mut log: Vec<u32> = Vec::new();
1439 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1440 assert!(w.children[0].is_focused(), "child 0 took focus");
1441
1442 let appended: FlexView<Vec<u32>> = column()
1444 .child(FocusRow { id: 0 })
1445 .child(FocusRow { id: 1 })
1446 .child(FocusRow { id: 2 });
1447 appended.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1448 assert!(
1449 w.children[0].is_focused(),
1450 "append-after preserves the focused child's recorded path"
1451 );
1452
1453 layout_column(&mut w);
1455 dispatch(&mut w, &mut log, &key_event());
1456 assert_eq!(log, vec![0], "Key still routes to the focused row");
1457 }
1458
1459 #[test]
1460 fn focus_on_child_survives_remove_after() {
1461 let mut counter = 0u64;
1464 let prev: FlexView<Vec<u32>> = column()
1465 .child(FocusRow { id: 0 })
1466 .child(FocusRow { id: 1 })
1467 .child(FocusRow { id: 2 });
1468 let mut w = prev.build(&mut ctx(&mut counter));
1469 layout_column(&mut w);
1470
1471 let mut log: Vec<u32> = Vec::new();
1472 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1473 assert!(w.children[0].is_focused());
1474
1475 let removed: FlexView<Vec<u32>> =
1477 column().child(FocusRow { id: 0 }).child(FocusRow { id: 1 });
1478 removed.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1479 assert!(
1480 w.children[0].is_focused(),
1481 "remove-after preserves the focused child's recorded path"
1482 );
1483
1484 layout_column(&mut w);
1485 dispatch(&mut w, &mut log, &key_event());
1486 assert_eq!(log, vec![0], "Key still routes to the focused row");
1487 }
1488
1489 #[test]
1490 fn focus_cleared_when_focused_index_type_swaps() {
1491 let mut counter = 0u64;
1494 let prev: FlexView<Vec<u32>> = column().child(FocusRow { id: 0 }).child(FocusRow { id: 1 });
1495 let mut w = prev.build(&mut ctx(&mut counter));
1496 layout_column(&mut w);
1497
1498 let mut log: Vec<u32> = Vec::new();
1499 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1500 assert!(w.children[1].is_focused(), "child 1 took focus");
1501
1502 let swapped: FlexView<Vec<u32>> = column().child(FocusRow { id: 0 }).child(captor(9));
1504 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1505 assert!(
1506 !w.children[1].is_focused(),
1507 "a type swap at the focused index clears its focus path"
1508 );
1509
1510 layout_column(&mut w);
1512 dispatch(&mut w, &mut log, &key_event());
1513 assert!(
1514 log.is_empty(),
1515 "Key reaches nobody after the focused index swaps"
1516 );
1517 }
1518
1519 struct Counter {
1530 id: u32,
1531 }
1532 struct CounterWidget {
1534 id: u32,
1535 count: u32,
1536 }
1537
1538 impl View<Vec<u32>> for Counter {
1539 type Element = CounterWidget;
1540 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CounterWidget {
1541 CounterWidget {
1542 id: self.id,
1543 count: 0,
1544 }
1545 }
1546 fn rebuild(
1547 &self,
1548 _prev: &Self,
1549 element: &mut CounterWidget,
1550 _ctx: &mut BuildCtx<'_>,
1551 ) -> ChangeFlags {
1552 element.id = self.id;
1555 ChangeFlags::NONE
1556 }
1557 }
1558
1559 impl Widget for CounterWidget {
1560 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1561 bc.constrain(Size::new(ROW_W, ROW_H))
1562 }
1563 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1564 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1565 let InputEvent::Pointer(p) = event else {
1566 return EventResult::Ignored;
1567 };
1568 if p.phase == PointerPhase::Down {
1569 self.count += 1;
1570 ctx.state_mut::<Vec<u32>>().push(self.count);
1571 EventResult::Handled
1572 } else {
1573 EventResult::Ignored
1574 }
1575 }
1576 }
1577
1578 fn kcounter(key: u64, id: u32) -> FlexChild<Vec<u32>> {
1580 keyed(key, Counter { id })
1581 }
1582
1583 fn keyed_column(children: Vec<FlexChild<Vec<u32>>>) -> FlexView<Vec<u32>> {
1585 FlexView::new(Axis::Vertical, children)
1586 }
1587
1588 #[test]
1589 fn keyed_reorder_preserves_widget_state() {
1590 let mut counter = 0u64;
1595 let prev = keyed_column(vec![kcounter(1, 1), kcounter(2, 2)]);
1596 let mut w = prev.build(&mut ctx(&mut counter));
1597 layout_column(&mut w);
1598
1599 let mut log: Vec<u32> = Vec::new();
1600 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1602 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1603 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1604 assert_eq!(log, vec![1, 2, 3], "count accumulates on the original row");
1605 log.clear();
1606
1607 let reordered = keyed_column(vec![kcounter(2, 2), kcounter(1, 1)]);
1609 reordered.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1610 assert_eq!(w.children.len(), 2);
1611 layout_column(&mut w);
1612
1613 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1616 assert_eq!(
1617 log,
1618 vec![4],
1619 "reordered row kept its internal state (4, not a reset 1)"
1620 );
1621 }
1622
1623 #[test]
1624 fn keyed_insert_above_preserves_existing_widget_state() {
1625 let mut counter = 0u64;
1628 let prev = keyed_column(vec![kcounter(1, 1), kcounter(2, 2)]);
1629 let mut w = prev.build(&mut ctx(&mut counter));
1630 layout_column(&mut w);
1631
1632 let mut log: Vec<u32> = Vec::new();
1633 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1635 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1636 assert_eq!(log, vec![1, 2]);
1637 log.clear();
1638
1639 let inserted = keyed_column(vec![kcounter(9, 9), kcounter(1, 1), kcounter(2, 2)]);
1641 inserted.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1642 assert_eq!(w.children.len(), 3);
1643 layout_column(&mut w);
1644
1645 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(2)));
1647 assert_eq!(log, vec![3], "shifted row preserved its state");
1648 log.clear();
1649
1650 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1652 assert_eq!(log, vec![1], "newly-inserted key builds a fresh widget");
1653 }
1654
1655 #[test]
1656 fn keyed_swap_preserves_both_widgets() {
1657 let mut counter = 0u64;
1659 let prev = keyed_column(vec![kcounter(1, 1), kcounter(2, 2)]);
1660 let mut w = prev.build(&mut ctx(&mut counter));
1661 layout_column(&mut w);
1662
1663 let mut log: Vec<u32> = Vec::new();
1664 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1666 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1667 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1668 assert_eq!(log, vec![1, 1, 2]);
1669 log.clear();
1670
1671 let swapped = keyed_column(vec![kcounter(2, 2), kcounter(1, 1)]);
1673 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1674 layout_column(&mut w);
1675
1676 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1678 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1679 assert_eq!(log, vec![3, 2], "both swapped rows kept their state");
1680 }
1681
1682 #[test]
1683 fn keyed_same_order_rebuild_is_not_structural() {
1684 let mut counter = 0u64;
1687 let prev: FlexView<Vec<u32>> = FlexView::new(
1688 Axis::Vertical,
1689 vec![keyed(1u64, Captor { id: 0 }), keyed(2u64, Captor { id: 1 })],
1690 );
1691 let mut w = prev.build(&mut ctx(&mut counter));
1692 layout_column(&mut w);
1693
1694 let mut log: Vec<u32> = Vec::new();
1695 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1696 assert!(w.children[1].is_active());
1697
1698 let same: FlexView<Vec<u32>> = FlexView::new(
1700 Axis::Vertical,
1701 vec![keyed(1u64, Captor { id: 0 }), keyed(2u64, Captor { id: 1 })],
1702 );
1703 same.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1704 assert!(
1705 w.children[1].is_active(),
1706 "same-order keyed rebuild must not clear an in-flight capture"
1707 );
1708
1709 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1710 assert_eq!(log, vec![1], "captured row fires on Up as normal");
1711 }
1712
1713 #[test]
1714 fn keyed_reorder_preserves_captured_drag() {
1715 let mut counter = 0u64;
1719 let prev: FlexView<Vec<u32>> = FlexView::new(
1720 Axis::Vertical,
1721 vec![keyed(1u64, Captor { id: 0 }), keyed(2u64, Captor { id: 1 })],
1722 );
1723 let mut w = prev.build(&mut ctx(&mut counter));
1724 layout_column(&mut w);
1725
1726 let mut log: Vec<u32> = Vec::new();
1727 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1729 assert!(w.children[0].is_active());
1730
1731 let reordered: FlexView<Vec<u32>> = FlexView::new(
1734 Axis::Vertical,
1735 vec![keyed(2u64, Captor { id: 1 }), keyed(1u64, Captor { id: 0 })],
1736 );
1737 reordered.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1738 assert!(
1739 w.children[1].is_active(),
1740 "keyed reorder carries the captured row's active path to its new index"
1741 );
1742
1743 layout_column(&mut w);
1745 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1746 assert_eq!(log, vec![0], "the relocated captured row fires on Up");
1747 }
1748
1749 #[test]
1750 fn keyed_removed_active_key_unwinds_without_fire() {
1751 let mut counter = 0u64;
1754 let prev: FlexView<Vec<u32>> = FlexView::new(
1755 Axis::Vertical,
1756 vec![
1757 keyed(1u64, Captor { id: 0 }),
1758 keyed(2u64, Captor { id: 1 }),
1759 keyed(3u64, Captor { id: 2 }),
1760 ],
1761 );
1762 let mut w = prev.build(&mut ctx(&mut counter));
1763 layout_column(&mut w);
1764
1765 let mut log: Vec<u32> = Vec::new();
1766 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1768 assert!(w.children[1].is_active());
1769
1770 let removed: FlexView<Vec<u32>> = FlexView::new(
1772 Axis::Vertical,
1773 vec![keyed(1u64, Captor { id: 0 }), keyed(3u64, Captor { id: 2 })],
1774 );
1775 removed.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1776 assert_eq!(w.children.len(), 2);
1777 assert_eq!(w.flex.len(), 2);
1778 assert!(w.children.iter().all(|p| !p.is_active()));
1779
1780 layout_column(&mut w);
1781 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1782 assert!(log.is_empty(), "removed active row does not fire on Up");
1783 }
1784
1785 #[test]
1786 fn keyed_reorder_preserves_focus_and_key_routing() {
1787 let mut counter = 0u64;
1791 let prev: FlexView<Vec<u32>> = FlexView::new(
1792 Axis::Vertical,
1793 vec![
1794 keyed(1u64, FocusRow { id: 1 }),
1795 keyed(2u64, FocusRow { id: 2 }),
1796 ],
1797 );
1798 let mut w = prev.build(&mut ctx(&mut counter));
1799 layout_column(&mut w);
1800
1801 let mut log: Vec<u32> = Vec::new();
1802 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1804 assert!(w.children[0].is_focused());
1805
1806 let reordered: FlexView<Vec<u32>> = FlexView::new(
1809 Axis::Vertical,
1810 vec![
1811 keyed(2u64, FocusRow { id: 2 }),
1812 keyed(1u64, FocusRow { id: 1 }),
1813 ],
1814 );
1815 reordered.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1816 assert!(
1817 w.children[1].is_focused(),
1818 "the focused key-1 row keeps focus at its new index"
1819 );
1820 assert!(!w.children[0].is_focused());
1821
1822 layout_column(&mut w);
1824 dispatch(&mut w, &mut log, &key_event());
1825 assert_eq!(log, vec![1], "Key routes to the relocated focused row");
1826 }
1827
1828 #[test]
1829 fn keyed_removed_focused_key_clears_focus() {
1830 let mut counter = 0u64;
1833 let prev: FlexView<Vec<u32>> = FlexView::new(
1834 Axis::Vertical,
1835 vec![
1836 keyed(1u64, FocusRow { id: 1 }),
1837 keyed(2u64, FocusRow { id: 2 }),
1838 ],
1839 );
1840 let mut w = prev.build(&mut ctx(&mut counter));
1841 layout_column(&mut w);
1842
1843 let mut log: Vec<u32> = Vec::new();
1844 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1846 assert!(w.children[1].is_focused());
1847
1848 let removed: FlexView<Vec<u32>> =
1850 FlexView::new(Axis::Vertical, vec![keyed(1u64, FocusRow { id: 1 })]);
1851 removed.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1852 assert_eq!(w.children.len(), 1);
1853 assert!(
1854 w.children.iter().all(|p| !p.is_focused()),
1855 "the removed focused row leaves no focus path behind"
1856 );
1857
1858 layout_column(&mut w);
1859 dispatch(&mut w, &mut log, &key_event());
1860 assert!(
1861 log.is_empty(),
1862 "Key reaches nobody after the focused key is removed"
1863 );
1864 }
1865
1866 struct FocusRow {
1869 id: u32,
1870 }
1871 struct FocusRowWidget {
1873 id: u32,
1874 }
1875
1876 impl View<Vec<u32>> for FocusRow {
1877 type Element = FocusRowWidget;
1878 fn build(&self, _ctx: &mut BuildCtx<'_>) -> FocusRowWidget {
1879 FocusRowWidget { id: self.id }
1880 }
1881 fn rebuild(
1882 &self,
1883 _prev: &Self,
1884 element: &mut FocusRowWidget,
1885 _ctx: &mut BuildCtx<'_>,
1886 ) -> ChangeFlags {
1887 element.id = self.id;
1888 ChangeFlags::NONE
1889 }
1890 }
1891
1892 impl Widget for FocusRowWidget {
1893 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1894 bc.constrain(Size::new(ROW_W, ROW_H))
1895 }
1896 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1897 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1898 match event {
1899 InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
1900 ctx.request_focus();
1901 EventResult::Handled
1902 }
1903 InputEvent::Key(_) => {
1906 ctx.state_mut::<Vec<u32>>().push(self.id);
1907 EventResult::Handled
1908 }
1909 _ => EventResult::Ignored,
1910 }
1911 }
1912 }
1913
1914 fn key_event() -> InputEvent {
1916 InputEvent::Key(KeyEvent {
1917 key: Key::Character("a".to_string()),
1918 modifiers: Modifiers::default(),
1919 repeat: false,
1920 })
1921 }
1922}