1use frust_core::{
14 AnyView, BoxConstraints, BuildCtx, ChangeFlags, ChildPod, EventCtx, EventResult, InputEvent,
15 LayoutCtx, PaintCtx, PaintScene, SemanticsCtx, View, 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>>(
144 key: impl Into<ChildKey>,
145 view: V,
146) -> FlexChild<State> {
147 FlexChild {
148 view: any(view),
149 flex: 0,
150 key: Some(key.into()),
151 }
152}
153
154pub struct FlexView<State: 'static> {
156 direction: Axis,
157 cross: CrossAxisAlignment,
158 main: MainAxisAlignment,
159 children: Vec<FlexChild<State>>,
160}
161
162impl<State: 'static> FlexView<State> {
163 pub fn new(direction: Axis, children: Vec<FlexChild<State>>) -> Self {
166 Self {
167 direction,
168 cross: CrossAxisAlignment::Start,
169 main: MainAxisAlignment::Start,
170 children,
171 }
172 }
173
174 pub fn cross_axis(mut self, cross: CrossAxisAlignment) -> Self {
176 self.cross = cross;
177 self
178 }
179
180 pub fn main_axis(mut self, main: MainAxisAlignment) -> Self {
182 self.main = main;
183 self
184 }
185}
186
187#[allow(non_snake_case)]
190pub fn Row<State: 'static>(children: Vec<AnyView<State>>) -> FlexView<State> {
191 FlexView::new(
192 Axis::Horizontal,
193 children
194 .into_iter()
195 .map(|view| FlexChild {
196 view,
197 flex: 0,
198 key: None,
199 })
200 .collect(),
201 )
202}
203
204#[allow(non_snake_case)]
207pub fn Column<State: 'static>(children: Vec<AnyView<State>>) -> FlexView<State> {
208 FlexView::new(
209 Axis::Vertical,
210 children
211 .into_iter()
212 .map(|view| FlexChild {
213 view,
214 flex: 0,
215 key: None,
216 })
217 .collect(),
218 )
219}
220
221pub struct FlexWidget {
224 direction: Axis,
225 cross: CrossAxisAlignment,
226 main: MainAxisAlignment,
227 children: Vec<ChildPod>,
228 flex: Vec<u32>,
229}
230
231fn child_constraints(
237 axis: Axis,
238 main_min: f64,
239 main_max: f64,
240 cross_bound: f64,
241 stretch: bool,
242 cross_max: f64,
243) -> BoxConstraints {
244 let cross_min = if stretch { cross_bound } else { 0.0 };
245 let cross_hi = if stretch { cross_bound } else { cross_max };
246 BoxConstraints::new(
247 axis.size(main_min, cross_min),
248 axis.size(main_max, cross_hi),
249 )
250}
251
252impl<State: 'static> View<State> for FlexView<State> {
253 type Element = FlexWidget;
254
255 fn build(&self, ctx: &mut BuildCtx<'_>) -> FlexWidget {
256 let mut children = Vec::with_capacity(self.children.len());
257 let mut flex = Vec::with_capacity(self.children.len());
258 for child in &self.children {
259 children.push(crate::authoring::build_child(&child.view, ctx));
260 flex.push(child.flex);
261 }
262 FlexWidget {
263 direction: self.direction,
264 cross: self.cross,
265 main: self.main,
266 children,
267 flex,
268 }
269 }
270
271 fn rebuild(
272 &self,
273 prev: &Self,
274 element: &mut FlexWidget,
275 ctx: &mut BuildCtx<'_>,
276 ) -> ChangeFlags {
277 let mut flags = ChangeFlags::NONE;
278 if prev.direction != self.direction || prev.cross != self.cross || prev.main != self.main {
279 element.direction = self.direction;
280 element.cross = self.cross;
281 element.main = self.main;
282 flags |= ChangeFlags::LAYOUT;
283 }
284
285 flags |= crate::authoring::rebuild_children(
290 &prev.children,
291 &self.children,
292 &mut element.children,
293 ctx,
294 |child| &child.view,
295 |child| child.key,
296 );
297
298 let new_flex: Vec<u32> = self.children.iter().map(|child| child.flex).collect();
304 if new_flex != element.flex {
305 element.flex = new_flex;
306 flags |= ChangeFlags::LAYOUT;
307 }
308
309 flags
310 }
311
312 fn teardown(&self, element: &mut FlexWidget, ctx: &mut BuildCtx<'_>) {
313 for (child, pod) in self.children.iter().zip(element.children.iter_mut()) {
314 crate::authoring::teardown_child(&child.view, pod, ctx);
315 }
316 }
317}
318
319impl Widget for FlexWidget {
320 fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
321 let axis = self.direction;
322 let max_main = axis.main_of(bc.max());
323 let max_cross = axis.cross_of(bc.max());
324 let stretch = self.cross == CrossAxisAlignment::Stretch;
325 let cross_bound = if max_cross.is_finite() {
328 max_cross
329 } else {
330 0.0
331 };
332
333 let mut allocated_main = 0.0f64;
336 let mut total_flex = 0u32;
337 let mut max_child_cross = 0.0f64;
338 for (i, pod) in self.children.iter_mut().enumerate() {
339 if self.flex[i] == 0 {
340 let cbc =
341 child_constraints(axis, 0.0, f64::INFINITY, cross_bound, stretch, max_cross);
342 let size = pod.layout_child(ctx, &cbc);
343 allocated_main += axis.main_of(size);
344 max_child_cross = max_child_cross.max(axis.cross_of(size));
345 } else {
346 total_flex += self.flex[i];
347 }
348 }
349
350 let free = if max_main.is_finite() {
354 (max_main - allocated_main).max(0.0)
355 } else {
356 0.0
357 };
358 if total_flex > 0 {
359 for (i, pod) in self.children.iter_mut().enumerate() {
360 if self.flex[i] > 0 {
361 let share = free * (self.flex[i] as f64) / (total_flex as f64);
362 let cbc =
363 child_constraints(axis, share, share, cross_bound, stretch, max_cross);
364 let size = pod.layout_child(ctx, &cbc);
365 max_child_cross = max_child_cross.max(axis.cross_of(size));
366 }
367 }
368 }
369
370 let main_size = if total_flex > 0 && max_main.is_finite() {
373 max_main
374 } else {
375 allocated_main
376 };
377 let cross_size = if stretch && max_cross.is_finite() {
379 max_cross
380 } else {
381 max_child_cross
382 };
383
384 let leading = match self.main {
387 MainAxisAlignment::Start => 0.0,
388 };
389 let mut main_pos = leading;
390 for pod in &mut self.children {
391 let child_cross = axis.cross_of(pod.size());
392 let cross_pos = match self.cross {
393 CrossAxisAlignment::Start | CrossAxisAlignment::Stretch => 0.0,
394 CrossAxisAlignment::Center => (cross_size - child_cross) / 2.0,
395 };
396 pod.set_origin(axis.point(main_pos, cross_pos));
397 main_pos += axis.main_of(pod.size());
398 }
399
400 bc.constrain(axis.size(main_size, cross_size))
401 }
402
403 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
404 let cull = ctx
447 .visible_rect()
448 .map(|vr| vr.inflate(vr.width(), vr.height()));
449 let hero_in_flight = ctx.hero_active();
450 for pod in &mut self.children {
452 if let Some(warm) = cull {
453 let exempt = hero_in_flight || pod.is_focused();
454 if !exempt {
455 let child_abs =
456 Rect::from_origin_size(ctx.origin() + pod.origin().to_vec2(), pod.size());
457 if !child_abs.overlaps(warm) {
458 continue;
459 }
460 }
461 }
462 pod.paint_child(ctx, scene);
463 }
464 }
465
466 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
467 crate::authoring::route_event(&mut self.children, ctx, event)
469 }
470
471 fn semantics(&self, ctx: &mut SemanticsCtx) {
472 for pod in &self.children {
475 pod.semantics_child(ctx);
476 }
477 }
478
479 crate::authoring::visit_children!(children);
480}
481
482#[cfg(test)]
483mod tests {
484 use super::*;
485 use crate::test_support::{RecordingScene, leaf};
486
487 fn ctx(counter: &mut u64) -> BuildCtx<'_> {
488 BuildCtx::new(counter)
489 }
490
491 fn build<S: 'static>(view: &FlexView<S>) -> FlexWidget {
492 let mut counter = 0u64;
493 view.build(&mut ctx(&mut counter))
494 }
495
496 #[test]
497 fn distributes_free_space_by_flex_factors() {
498 let view: FlexView<()> = FlexView::new(
501 Axis::Horizontal,
502 vec![
503 flexible(2, leaf(1000.0, 20.0)),
504 flexible(1, leaf(1000.0, 20.0)),
505 ],
506 );
507 let mut w = build(&view);
508 let mut lctx = LayoutCtx::new();
509 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
510
511 assert_eq!(w.children[0].size().width, 200.0);
512 assert_eq!(w.children[1].size().width, 100.0);
513 assert_eq!(w.children[0].origin().x, 0.0);
514 assert_eq!(w.children[1].origin().x, 200.0);
515 assert_eq!(size.width, 300.0);
517 }
518
519 #[test]
520 fn mixes_inflexible_and_flexible() {
521 let view: FlexView<()> = FlexView::new(
524 Axis::Horizontal,
525 vec![
526 inflexible(leaf(50.0, 20.0)),
527 flexible(1, leaf(1000.0, 20.0)),
528 ],
529 );
530 let mut w = build(&view);
531 let mut lctx = LayoutCtx::new();
532 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
533
534 assert_eq!(w.children[0].size().width, 50.0);
535 assert_eq!(w.children[1].size().width, 150.0);
536 assert_eq!(w.children[0].origin().x, 0.0);
537 assert_eq!(w.children[1].origin().x, 50.0);
538 assert_eq!(size.width, 200.0);
539 }
540
541 #[test]
542 fn shrink_wraps_main_axis_without_flexible_children() {
543 let view: FlexView<()> = Row(vec![
546 leaf(40.0, 10.0).into_any(),
547 leaf(20.0, 10.0).into_any(),
548 ]);
549 let mut w = build(&view);
550 let mut lctx = LayoutCtx::new();
551 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(300.0, 100.0)));
552 assert_eq!(size.width, 60.0);
553 assert_eq!(w.children[1].origin().x, 40.0);
554 }
555
556 #[test]
557 fn cross_axis_stretch_tightens_children() {
558 let view: FlexView<()> =
561 Row(vec![leaf(30.0, 10.0).into_any()]).cross_axis(CrossAxisAlignment::Stretch);
562 let mut w = build(&view);
563 let mut lctx = LayoutCtx::new();
564 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 80.0)));
565 assert_eq!(w.children[0].size().height, 80.0);
566 assert_eq!(size.height, 80.0);
567 }
568
569 #[test]
570 fn cross_axis_start_packs_at_zero() {
571 let view: FlexView<()> = Row(vec![
573 leaf(10.0, 40.0).into_any(),
574 leaf(10.0, 20.0).into_any(),
575 ])
576 .cross_axis(CrossAxisAlignment::Start);
577 let mut w = build(&view);
578 let mut lctx = LayoutCtx::new();
579 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
580 assert_eq!(size.height, 40.0);
581 assert_eq!(w.children[1].origin().y, 0.0);
582 }
583
584 #[test]
585 fn cross_axis_center_centers_shorter_children() {
586 let view: FlexView<()> = Row(vec![
588 leaf(10.0, 40.0).into_any(),
589 leaf(10.0, 20.0).into_any(),
590 ])
591 .cross_axis(CrossAxisAlignment::Center);
592 let mut w = build(&view);
593 let mut lctx = LayoutCtx::new();
594 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 100.0)));
595 assert_eq!(w.children[1].origin().y, 10.0);
596 }
597
598 #[test]
599 fn column_lays_out_along_vertical_axis() {
600 let view: FlexView<()> = Column(vec![
602 leaf(30.0, 15.0).into_any(),
603 leaf(30.0, 25.0).into_any(),
604 ]);
605 let mut w = build(&view);
606 let mut lctx = LayoutCtx::new();
607 let size = w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 300.0)));
608 assert_eq!(size.height, 40.0); assert_eq!(w.children[0].origin().y, 0.0);
610 assert_eq!(w.children[1].origin().y, 15.0);
611 }
612
613 #[test]
614 fn paints_children_in_order() {
615 let view: FlexView<()> = Row(vec![
616 leaf(20.0, 20.0).into_any(),
617 leaf(20.0, 20.0).into_any(),
618 ]);
619 let mut w = build(&view);
620 let mut lctx = LayoutCtx::new();
621 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(200.0, 200.0)));
622
623 let mut scene = RecordingScene::default();
624 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(200.0, 200.0));
625 w.paint(&mut pctx, &mut scene);
626 assert_eq!(scene.rects[0].0, Point::new(0.0, 0.0));
628 assert_eq!(scene.rects[1].0, Point::new(20.0, 0.0));
629 }
630
631 fn culling_column() -> FlexWidget {
636 let view: FlexView<()> = Column(vec![
637 leaf(100.0, 100.0).into_any(),
638 leaf(100.0, 100.0).into_any(),
639 leaf(100.0, 100.0).into_any(),
640 leaf(100.0, 100.0).into_any(),
641 leaf(100.0, 100.0).into_any(),
642 ]);
643 let mut w = build(&view);
644 let mut lctx = LayoutCtx::new();
645 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(100.0, 500.0)));
646 w
647 }
648
649 #[test]
650 fn no_visible_rect_paints_every_child() {
651 let mut w = culling_column();
653 let mut scene = RecordingScene::default();
654 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 500.0));
655 w.paint(&mut pctx, &mut scene);
656 assert_eq!(scene.rects.len(), 5, "no cull → all five rows painted");
657 }
658
659 #[test]
660 fn culls_children_fully_outside_visible_rect_plus_margin() {
661 let mut w = culling_column();
667 let mut scene = RecordingScene::default();
668 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 500.0));
669 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 100.0)));
670 w.paint(&mut pctx, &mut scene);
671 assert_eq!(scene.rects.len(), 3, "two below-the-warm-band rows culled");
672 assert_eq!(scene.rects[0].0, Point::new(0.0, 0.0));
673 assert_eq!(scene.rects[1].0, Point::new(0.0, 100.0));
674 assert_eq!(scene.rects[2].0, Point::new(0.0, 200.0));
676 }
677
678 #[test]
679 fn margin_boundary_row_just_past_the_warm_band_is_culled() {
680 let mut w = culling_column();
686 let mut scene = RecordingScene::default();
687 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(100.0, 500.0));
688 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(100.0, 50.0)));
689 w.paint(&mut pctx, &mut scene);
690 assert_eq!(scene.rects.len(), 2, "only the top band rows survive");
691 assert_eq!(scene.rects[0].0, Point::new(0.0, 0.0));
692 assert_eq!(scene.rects[1].0, Point::new(0.0, 100.0));
693 }
694
695 #[test]
696 fn culled_child_still_receives_events_at_its_layout_geometry() {
697 let mut counter = 0u64;
702 let view: FlexView<Vec<u32>> = Column(vec![
703 captor(0),
704 captor(1),
705 captor(2),
706 captor(3),
707 captor(4),
708 captor(5),
709 captor(6),
710 captor(7),
711 ]);
712 let mut w = view.build(&mut ctx(&mut counter));
713 layout_column(&mut w);
714
715 let mut scene = RecordingScene::default();
718 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 8.0));
719 pctx.constrain_visible_rect(Rect::from_origin_size(Point::ZERO, Size::new(ROW_W, ROW_H)));
720 w.paint(&mut pctx, &mut scene);
721
722 let mut log: Vec<u32> = Vec::new();
725 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(7)));
726 assert!(
727 w.children[7].is_active(),
728 "a culled row still captures on Down"
729 );
730 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(7)));
731 assert_eq!(log, vec![7], "a culled-but-laid-out row still fires on Up");
732 }
733
734 struct ImeLeaf {
746 value: String,
747 painted: Rc<Cell<u32>>,
748 }
749 struct ImeLeafWidget {
751 value: String,
752 painted: Rc<Cell<u32>>,
753 }
754
755 impl View<()> for ImeLeaf {
756 type Element = ImeLeafWidget;
757 fn build(&self, _ctx: &mut BuildCtx<'_>) -> ImeLeafWidget {
758 ImeLeafWidget {
759 value: self.value.clone(),
760 painted: self.painted.clone(),
761 }
762 }
763 fn rebuild(
764 &self,
765 _prev: &Self,
766 element: &mut ImeLeafWidget,
767 _ctx: &mut BuildCtx<'_>,
768 ) -> ChangeFlags {
769 element.value = self.value.clone();
772 ChangeFlags::NONE
773 }
774 }
775
776 impl Widget for ImeLeafWidget {
777 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
778 bc.constrain(Size::new(ROW_W, ROW_H))
779 }
780 fn paint(&mut self, ctx: &mut PaintCtx, scene: &mut dyn PaintScene) {
781 use frust_core::{EditingState, ImeState};
782 self.painted.set(self.painted.get() + 1);
783 scene.fill_rect(ctx.origin(), ctx.size(), peniko::Color::BLACK);
784 ctx.publish_ime_state(ImeState {
785 active: true,
786 editing: EditingState {
787 text: self.value.clone(),
788 selection_base: -1,
789 selection_extent: -1,
790 composing_base: -1,
791 composing_extent: -1,
792 },
793 caret: None,
794 content_type: Default::default(),
795 suppress_soft_keyboard: false,
796 });
797 }
798 }
799
800 fn ime_column() -> (FlexWidget, [Rc<Cell<u32>>; 5]) {
803 let counts: [Rc<Cell<u32>>; 5] = std::array::from_fn(|_| Rc::new(Cell::new(0)));
804 let view: FlexView<()> = Column(
805 (0..5)
806 .map(|i| {
807 any(ImeLeaf {
808 value: format!("row{i}"),
809 painted: counts[i].clone(),
810 })
811 })
812 .collect(),
813 );
814 let mut counter = 0u64;
815 let mut w = view.build(&mut ctx(&mut counter));
816 layout_column(&mut w);
817 (w, counts)
818 }
819
820 fn top_row_rect() -> Rect {
823 Rect::from_origin_size(Point::ZERO, Size::new(ROW_W, ROW_H))
824 }
825
826 #[test]
827 fn focused_child_beyond_warm_band_still_paints_and_republishes_ime() {
828 let (mut w, counts) = ime_column();
832 w.children[4].set_focused(true);
833
834 let mut scene = RecordingScene::default();
835 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
836 pctx.constrain_visible_rect(top_row_rect());
837 w.paint(&mut pctx, &mut scene);
838
839 assert_eq!(counts[0].get(), 1, "warm row 0 paints");
840 assert_eq!(counts[1].get(), 1, "warm row 1 paints");
841 assert_eq!(counts[2].get(), 1, "warm boundary row 2 paints");
842 assert_eq!(
843 counts[3].get(),
844 0,
845 "unfocused offscreen row 3 stays culled (suppression intact)"
846 );
847 assert_eq!(
848 counts[4].get(),
849 1,
850 "focused offscreen row 4 is exempt from culling and paints"
851 );
852 let ime = pctx
855 .take_ime_state()
856 .expect("focused row republished its IME");
857 assert_eq!(ime.editing.text, "row4");
858 }
859
860 #[test]
861 fn focused_cull_exemption_republishes_a_rebuild_mutation_immediately() {
862 let far = Rect::from_origin_size(Point::new(0.0, 10_000.0), Size::new(ROW_W, ROW_H));
867
868 let mut counter = 0u64;
870 let prev: FlexView<()> = Column(vec![any(ImeLeaf {
871 value: "v1".to_string(),
872 painted: Rc::new(Cell::new(0)),
873 })]);
874 let mut w = prev.build(&mut ctx(&mut counter));
875 layout_column(&mut w);
876 let next: FlexView<()> = Column(vec![any(ImeLeaf {
878 value: "v2".to_string(),
879 painted: Rc::new(Cell::new(0)),
880 })]);
881 next.rebuild(&prev, &mut w, &mut ctx(&mut counter));
882 let mut scene = RecordingScene::default();
883 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
884 pctx.constrain_visible_rect(far);
885 w.paint(&mut pctx, &mut scene);
886 assert!(
887 pctx.take_ime_state().is_none(),
888 "an unfocused, culled field never republishes — its IME goes stale"
889 );
890
891 let mut counter = 0u64;
893 let prev: FlexView<()> = Column(vec![any(ImeLeaf {
894 value: "v1".to_string(),
895 painted: Rc::new(Cell::new(0)),
896 })]);
897 let mut w = prev.build(&mut ctx(&mut counter));
898 layout_column(&mut w);
899 w.children[0].set_focused(true);
900 let next: FlexView<()> = Column(vec![any(ImeLeaf {
901 value: "v2".to_string(),
902 painted: Rc::new(Cell::new(0)),
903 })]);
904 next.rebuild(&prev, &mut w, &mut ctx(&mut counter));
905 let mut scene = RecordingScene::default();
906 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
907 pctx.constrain_visible_rect(far);
908 w.paint(&mut pctx, &mut scene);
909 let ime = pctx
910 .take_ime_state()
911 .expect("focused field republishes even while offscreen");
912 assert_eq!(
913 ime.editing.text, "v2",
914 "the rebuild-mutated value republished immediately, not stale 'v1'"
915 );
916 }
917
918 #[test]
919 fn animated_scale_child_straddling_the_cull_boundary_does_not_pop() {
920 use frust_core::Curve;
929 use std::time::Duration;
930 let snap = crate::Timing::Duration(Duration::ZERO, Curve::Linear);
931
932 let view: FlexView<()> = Column(vec![
935 leaf(ROW_W, ROW_H).into_any(),
936 leaf(ROW_W, ROW_H).into_any(),
937 any(crate::motion::AnimatedScale(2.8, leaf(ROW_W, ROW_H)).timing(snap)),
938 leaf(ROW_W, ROW_H).into_any(),
939 any(crate::motion::AnimatedScale(2.8, leaf(ROW_W, ROW_H)).timing(snap)),
940 ]);
941 let mut counter = 0u64;
942 let mut w = view.build(&mut ctx(&mut counter));
943 layout_column(&mut w);
944
945 let mut scene = RecordingScene::default();
946 let mut pctx = PaintCtx::new(Point::ZERO, Size::new(ROW_W, ROW_H * 5.0));
947 pctx.constrain_visible_rect(top_row_rect());
948 w.paint(&mut pctx, &mut scene);
949
950 assert_eq!(
953 scene.rects.len(),
954 3,
955 "the boundary AnimatedScale row paints on its layout box, the two \
956 fully-outside rows (one of them also AnimatedScale) are culled"
957 );
958 assert!(
961 scene.transforms.iter().any(|t| {
962 let c = t.as_coeffs();
963 (c[0] - 2.8).abs() < 1e-6 && (c[3] - 2.8).abs() < 1e-6
964 }),
965 "the boundary row composited at 2.8× without being culled by its \
966 transform-overflowed visual bounds"
967 );
968 }
969
970 #[test]
971 fn children_vec_diff_adds_removes_and_type_swaps() {
972 let mut counter = 0u64;
974 let prev: FlexView<()> = Row(vec![
975 leaf(10.0, 10.0).into_any(),
976 leaf(10.0, 10.0).into_any(),
977 ]);
978 let mut w = prev.build(&mut ctx(&mut counter));
979 assert_eq!(w.children.len(), 2);
980
981 let grown: FlexView<()> = Row(vec![
983 leaf(10.0, 10.0).into_any(),
984 leaf(10.0, 10.0).into_any(),
985 leaf(10.0, 10.0).into_any(),
986 ]);
987 let flags = grown.rebuild(&prev, &mut w, &mut ctx(&mut counter));
988 assert_eq!(w.children.len(), 3);
989 assert!(flags.needs_layout());
990
991 let shrunk: FlexView<()> = Row(vec![leaf(10.0, 10.0).into_any()]);
993 shrunk.rebuild(&grown, &mut w, &mut ctx(&mut counter));
994 assert_eq!(w.children.len(), 1);
995 assert_eq!(w.flex.len(), 1);
996
997 let swapped: FlexView<()> = Row(vec![crate::test_support::swap_leaf().into_any()]);
999 swapped.rebuild(&shrunk, &mut w, &mut ctx(&mut counter));
1000 assert_eq!(w.children.len(), 1);
1001 let mut lctx = LayoutCtx::new();
1003 w.layout(&mut lctx, &BoxConstraints::loose(Size::new(500.0, 500.0)));
1004 assert_eq!(w.children[0].size(), Size::new(7.0, 7.0));
1005 }
1006
1007 use std::any::Any;
1017 use std::cell::Cell;
1018 use std::rc::Rc;
1019
1020 use frust_core::{
1021 EventCtx, Key, KeyEvent, Modifiers, PointerButton, PointerEvent, PointerPhase, any,
1022 };
1023
1024 const ROW_W: f64 = 50.0;
1025 const ROW_H: f64 = 20.0;
1026
1027 struct Captor {
1029 id: u32,
1030 }
1031 struct CaptorWidget {
1033 id: u32,
1034 armed: bool,
1035 }
1036
1037 fn captor(id: u32) -> AnyView<Vec<u32>> {
1039 any(Captor { id })
1040 }
1041
1042 impl View<Vec<u32>> for Captor {
1043 type Element = CaptorWidget;
1044 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CaptorWidget {
1045 CaptorWidget {
1046 id: self.id,
1047 armed: false,
1048 }
1049 }
1050 fn rebuild(
1051 &self,
1052 _prev: &Self,
1053 element: &mut CaptorWidget,
1054 _ctx: &mut BuildCtx<'_>,
1055 ) -> ChangeFlags {
1056 element.id = self.id;
1057 ChangeFlags::NONE
1058 }
1059 }
1060
1061 impl Widget for CaptorWidget {
1062 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1063 bc.constrain(Size::new(ROW_W, ROW_H))
1064 }
1065 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1066 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1067 let InputEvent::Pointer(p) = event else {
1068 return EventResult::Ignored;
1069 };
1070 match p.phase {
1071 PointerPhase::Down => {
1072 self.armed = true;
1073 ctx.capture_pointer();
1074 EventResult::Handled
1075 }
1076 PointerPhase::Move => EventResult::Handled,
1077 PointerPhase::Up => {
1078 if self.armed {
1079 ctx.state_mut::<Vec<u32>>().push(self.id);
1080 }
1081 self.armed = false;
1082 EventResult::Handled
1083 }
1084 PointerPhase::Cancel => {
1085 self.armed = false;
1087 EventResult::Handled
1088 }
1089 }
1090 }
1091 }
1092
1093 struct Recorder {
1096 seen: Rc<Cell<u32>>,
1097 }
1098 struct RecorderWidget {
1100 seen: Rc<Cell<u32>>,
1101 }
1102
1103 impl View<Vec<u32>> for Recorder {
1104 type Element = RecorderWidget;
1105 fn build(&self, _ctx: &mut BuildCtx<'_>) -> RecorderWidget {
1106 RecorderWidget {
1107 seen: self.seen.clone(),
1108 }
1109 }
1110 fn rebuild(
1111 &self,
1112 _prev: &Self,
1113 _element: &mut RecorderWidget,
1114 _ctx: &mut BuildCtx<'_>,
1115 ) -> ChangeFlags {
1116 ChangeFlags::NONE
1117 }
1118 }
1119
1120 impl Widget for RecorderWidget {
1121 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1122 bc.constrain(Size::new(ROW_W, ROW_H))
1123 }
1124 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1125 fn event(&mut self, _ctx: &mut EventCtx, _event: &InputEvent) -> EventResult {
1126 self.seen.set(self.seen.get() + 1);
1127 EventResult::Handled
1128 }
1129 }
1130
1131 fn ev(phase: PointerPhase, x: f64, y: f64) -> InputEvent {
1132 InputEvent::Pointer(PointerEvent {
1133 phase,
1134 position: Point::new(x, y),
1135 button: PointerButton::Primary,
1136 })
1137 }
1138
1139 #[allow(clippy::ptr_arg)]
1144 fn dispatch(w: &mut FlexWidget, log: &mut Vec<u32>, event: &InputEvent) {
1145 let state: &mut dyn Any = log;
1146 let mut ectx = EventCtx::new(state, Point::ZERO, Size::new(ROW_W, ROW_H * 8.0));
1147 w.event(&mut ectx, event);
1148 }
1149
1150 fn layout_column(w: &mut FlexWidget) {
1152 let mut lctx = LayoutCtx::new();
1153 w.layout(
1154 &mut lctx,
1155 &BoxConstraints::loose(Size::new(ROW_W, ROW_H * 8.0)),
1156 );
1157 }
1158
1159 fn row_y(i: usize) -> f64 {
1161 i as f64 * ROW_H + ROW_H / 2.0
1162 }
1163
1164 #[test]
1165 fn append_after_preserves_captured_drag_before_change() {
1166 let mut counter = 0u64;
1172 let prev: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1), captor(2)]);
1173 let mut w = prev.build(&mut ctx(&mut counter));
1174 layout_column(&mut w);
1175
1176 let mut log: Vec<u32> = Vec::new();
1177 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1178 assert!(w.children[1].is_active(), "row 1 captured the pointer");
1179
1180 let appended: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1), captor(2), captor(3)]);
1182 appended.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1183 assert!(
1184 w.children[1].is_active(),
1185 "append-after preserves the captured row's active path (stable prefix)"
1186 );
1187
1188 layout_column(&mut w);
1190 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1191 assert_eq!(log, vec![1], "captured row fires on Up as normal");
1192 }
1193
1194 #[test]
1195 fn type_swap_before_armed_index_cancels_with_synthetic_cancel() {
1196 let mut counter = 0u64;
1202 let prev: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1), captor(2)]);
1203 let mut w = prev.build(&mut ctx(&mut counter));
1204 layout_column(&mut w);
1205
1206 let mut log: Vec<u32> = Vec::new();
1207 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(2)));
1208 assert!(w.children[2].is_active(), "row 2 captured the pointer");
1209
1210 let seen = Rc::new(Cell::new(0u32));
1215 let swapped: FlexView<Vec<u32>> =
1216 Column(vec![any(Recorder { seen }), captor(1), captor(2)]);
1217 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1218 assert!(
1219 w.children.iter().all(|p| !p.is_active()),
1220 "swap before the armed index cancelled the tail's active path"
1221 );
1222
1223 layout_column(&mut w);
1224 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(2)));
1225 assert!(
1226 log.is_empty(),
1227 "no fire on Up — the armed row was synthetically cancelled"
1228 );
1229 }
1230
1231 #[test]
1232 fn structural_truncation_of_active_row_unwinds_without_panic() {
1233 let mut counter = 0u64;
1237 let prev: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1), captor(2), captor(3)]);
1238 let mut w = prev.build(&mut ctx(&mut counter));
1239 layout_column(&mut w);
1240
1241 let mut log: Vec<u32> = Vec::new();
1242 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(2)));
1243 assert!(w.children[2].is_active());
1244
1245 let truncated: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1)]);
1247 truncated.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1248 assert_eq!(w.children.len(), 2);
1249 assert_eq!(w.flex.len(), 2);
1250 assert!(w.children.iter().all(|p| !p.is_active()));
1251
1252 layout_column(&mut w);
1254 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(2)));
1255 assert!(log.is_empty());
1256 }
1257
1258 #[test]
1259 fn type_swap_at_active_index_clears_without_notifying_fresh_widget() {
1260 let mut counter = 0u64;
1264 let prev: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1)]);
1265 let mut w = prev.build(&mut ctx(&mut counter));
1266 layout_column(&mut w);
1267
1268 let mut log: Vec<u32> = Vec::new();
1269 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1270 assert!(w.children[1].is_active());
1271
1272 let seen = Rc::new(Cell::new(0u32));
1274 let swapped: FlexView<Vec<u32>> =
1275 Column(vec![captor(0), any(Recorder { seen: seen.clone() })]);
1276 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1277
1278 assert!(!w.children[1].is_active(), "stale capture path dropped");
1279 assert_eq!(seen.get(), 0, "fresh widget received no synthetic event");
1280
1281 layout_column(&mut w);
1283 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1284 assert_eq!(seen.get(), 1, "fresh widget responds to a new gesture");
1285 }
1286
1287 #[test]
1288 fn content_only_rebuild_preserves_captured_drag() {
1289 let mut counter = 0u64;
1293 let prev: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1)]);
1294 let mut w = prev.build(&mut ctx(&mut counter));
1295 layout_column(&mut w);
1296
1297 let mut log: Vec<u32> = Vec::new();
1298 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1299 assert!(w.children[1].is_active());
1300
1301 let same: FlexView<Vec<u32>> = Column(vec![captor(0), captor(1)]);
1303 same.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1304 assert!(
1305 w.children[1].is_active(),
1306 "content-only rebuild must NOT clear an in-flight capture"
1307 );
1308
1309 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1311 assert_eq!(log, vec![1], "captured row fires on Up as normal");
1312 }
1313
1314 #[test]
1323 fn focus_on_child_survives_append_after() {
1324 let mut counter = 0u64;
1329 let prev: FlexView<Vec<u32>> =
1330 Column(vec![any(FocusRow { id: 0 }), any(FocusRow { id: 1 })]);
1331 let mut w = prev.build(&mut ctx(&mut counter));
1332 layout_column(&mut w);
1333
1334 let mut log: Vec<u32> = Vec::new();
1335 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1336 assert!(w.children[0].is_focused(), "child 0 took focus");
1337
1338 let appended: FlexView<Vec<u32>> = Column(vec![
1340 any(FocusRow { id: 0 }),
1341 any(FocusRow { id: 1 }),
1342 any(FocusRow { id: 2 }),
1343 ]);
1344 appended.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1345 assert!(
1346 w.children[0].is_focused(),
1347 "append-after preserves the focused child's recorded path"
1348 );
1349
1350 layout_column(&mut w);
1352 dispatch(&mut w, &mut log, &key_event());
1353 assert_eq!(log, vec![0], "Key still routes to the focused row");
1354 }
1355
1356 #[test]
1357 fn focus_on_child_survives_remove_after() {
1358 let mut counter = 0u64;
1361 let prev: FlexView<Vec<u32>> = Column(vec![
1362 any(FocusRow { id: 0 }),
1363 any(FocusRow { id: 1 }),
1364 any(FocusRow { id: 2 }),
1365 ]);
1366 let mut w = prev.build(&mut ctx(&mut counter));
1367 layout_column(&mut w);
1368
1369 let mut log: Vec<u32> = Vec::new();
1370 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1371 assert!(w.children[0].is_focused());
1372
1373 let removed: FlexView<Vec<u32>> =
1375 Column(vec![any(FocusRow { id: 0 }), any(FocusRow { id: 1 })]);
1376 removed.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1377 assert!(
1378 w.children[0].is_focused(),
1379 "remove-after preserves the focused child's recorded path"
1380 );
1381
1382 layout_column(&mut w);
1383 dispatch(&mut w, &mut log, &key_event());
1384 assert_eq!(log, vec![0], "Key still routes to the focused row");
1385 }
1386
1387 #[test]
1388 fn focus_cleared_when_focused_index_type_swaps() {
1389 let mut counter = 0u64;
1392 let prev: FlexView<Vec<u32>> =
1393 Column(vec![any(FocusRow { id: 0 }), any(FocusRow { id: 1 })]);
1394 let mut w = prev.build(&mut ctx(&mut counter));
1395 layout_column(&mut w);
1396
1397 let mut log: Vec<u32> = Vec::new();
1398 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1399 assert!(w.children[1].is_focused(), "child 1 took focus");
1400
1401 let swapped: FlexView<Vec<u32>> = Column(vec![any(FocusRow { id: 0 }), captor(9)]);
1403 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1404 assert!(
1405 !w.children[1].is_focused(),
1406 "a type swap at the focused index clears its focus path"
1407 );
1408
1409 layout_column(&mut w);
1411 dispatch(&mut w, &mut log, &key_event());
1412 assert!(
1413 log.is_empty(),
1414 "Key reaches nobody after the focused index swaps"
1415 );
1416 }
1417
1418 struct Counter {
1429 id: u32,
1430 }
1431 struct CounterWidget {
1433 id: u32,
1434 count: u32,
1435 }
1436
1437 impl View<Vec<u32>> for Counter {
1438 type Element = CounterWidget;
1439 fn build(&self, _ctx: &mut BuildCtx<'_>) -> CounterWidget {
1440 CounterWidget {
1441 id: self.id,
1442 count: 0,
1443 }
1444 }
1445 fn rebuild(
1446 &self,
1447 _prev: &Self,
1448 element: &mut CounterWidget,
1449 _ctx: &mut BuildCtx<'_>,
1450 ) -> ChangeFlags {
1451 element.id = self.id;
1454 ChangeFlags::NONE
1455 }
1456 }
1457
1458 impl Widget for CounterWidget {
1459 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1460 bc.constrain(Size::new(ROW_W, ROW_H))
1461 }
1462 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1463 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1464 let InputEvent::Pointer(p) = event else {
1465 return EventResult::Ignored;
1466 };
1467 if p.phase == PointerPhase::Down {
1468 self.count += 1;
1469 ctx.state_mut::<Vec<u32>>().push(self.count);
1470 EventResult::Handled
1471 } else {
1472 EventResult::Ignored
1473 }
1474 }
1475 }
1476
1477 fn kcounter(key: u64, id: u32) -> FlexChild<Vec<u32>> {
1479 keyed(key, Counter { id })
1480 }
1481
1482 fn keyed_column(children: Vec<FlexChild<Vec<u32>>>) -> FlexView<Vec<u32>> {
1484 FlexView::new(Axis::Vertical, children)
1485 }
1486
1487 #[test]
1488 fn keyed_reorder_preserves_widget_state() {
1489 let mut counter = 0u64;
1494 let prev = keyed_column(vec![kcounter(1, 1), kcounter(2, 2)]);
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(0)));
1501 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1502 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1503 assert_eq!(log, vec![1, 2, 3], "count accumulates on the original row");
1504 log.clear();
1505
1506 let reordered = keyed_column(vec![kcounter(2, 2), kcounter(1, 1)]);
1508 reordered.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1509 assert_eq!(w.children.len(), 2);
1510 layout_column(&mut w);
1511
1512 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1515 assert_eq!(
1516 log,
1517 vec![4],
1518 "reordered row kept its internal state (4, not a reset 1)"
1519 );
1520 }
1521
1522 #[test]
1523 fn keyed_insert_above_preserves_existing_widget_state() {
1524 let mut counter = 0u64;
1527 let prev = keyed_column(vec![kcounter(1, 1), kcounter(2, 2)]);
1528 let mut w = prev.build(&mut ctx(&mut counter));
1529 layout_column(&mut w);
1530
1531 let mut log: Vec<u32> = Vec::new();
1532 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1534 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1535 assert_eq!(log, vec![1, 2]);
1536 log.clear();
1537
1538 let inserted = keyed_column(vec![kcounter(9, 9), kcounter(1, 1), kcounter(2, 2)]);
1540 inserted.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1541 assert_eq!(w.children.len(), 3);
1542 layout_column(&mut w);
1543
1544 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(2)));
1546 assert_eq!(log, vec![3], "shifted row preserved its state");
1547 log.clear();
1548
1549 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1551 assert_eq!(log, vec![1], "newly-inserted key builds a fresh widget");
1552 }
1553
1554 #[test]
1555 fn keyed_swap_preserves_both_widgets() {
1556 let mut counter = 0u64;
1558 let prev = keyed_column(vec![kcounter(1, 1), kcounter(2, 2)]);
1559 let mut w = prev.build(&mut ctx(&mut counter));
1560 layout_column(&mut w);
1561
1562 let mut log: Vec<u32> = Vec::new();
1563 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1565 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1566 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1567 assert_eq!(log, vec![1, 1, 2]);
1568 log.clear();
1569
1570 let swapped = keyed_column(vec![kcounter(2, 2), kcounter(1, 1)]);
1572 swapped.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1573 layout_column(&mut w);
1574
1575 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1577 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1578 assert_eq!(log, vec![3, 2], "both swapped rows kept their state");
1579 }
1580
1581 #[test]
1582 fn keyed_same_order_rebuild_is_not_structural() {
1583 let mut counter = 0u64;
1586 let prev: FlexView<Vec<u32>> = FlexView::new(
1587 Axis::Vertical,
1588 vec![keyed(1u64, Captor { id: 0 }), keyed(2u64, Captor { id: 1 })],
1589 );
1590 let mut w = prev.build(&mut ctx(&mut counter));
1591 layout_column(&mut w);
1592
1593 let mut log: Vec<u32> = Vec::new();
1594 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1595 assert!(w.children[1].is_active());
1596
1597 let same: FlexView<Vec<u32>> = FlexView::new(
1599 Axis::Vertical,
1600 vec![keyed(1u64, Captor { id: 0 }), keyed(2u64, Captor { id: 1 })],
1601 );
1602 same.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1603 assert!(
1604 w.children[1].is_active(),
1605 "same-order keyed rebuild must not clear an in-flight capture"
1606 );
1607
1608 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1609 assert_eq!(log, vec![1], "captured row fires on Up as normal");
1610 }
1611
1612 #[test]
1613 fn keyed_reorder_preserves_captured_drag() {
1614 let mut counter = 0u64;
1618 let prev: FlexView<Vec<u32>> = FlexView::new(
1619 Axis::Vertical,
1620 vec![keyed(1u64, Captor { id: 0 }), keyed(2u64, Captor { id: 1 })],
1621 );
1622 let mut w = prev.build(&mut ctx(&mut counter));
1623 layout_column(&mut w);
1624
1625 let mut log: Vec<u32> = Vec::new();
1626 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1628 assert!(w.children[0].is_active());
1629
1630 let reordered: FlexView<Vec<u32>> = FlexView::new(
1633 Axis::Vertical,
1634 vec![keyed(2u64, Captor { id: 1 }), keyed(1u64, Captor { id: 0 })],
1635 );
1636 reordered.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1637 assert!(
1638 w.children[1].is_active(),
1639 "keyed reorder carries the captured row's active path to its new index"
1640 );
1641
1642 layout_column(&mut w);
1644 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1645 assert_eq!(log, vec![0], "the relocated captured row fires on Up");
1646 }
1647
1648 #[test]
1649 fn keyed_removed_active_key_unwinds_without_fire() {
1650 let mut counter = 0u64;
1653 let prev: FlexView<Vec<u32>> = FlexView::new(
1654 Axis::Vertical,
1655 vec![
1656 keyed(1u64, Captor { id: 0 }),
1657 keyed(2u64, Captor { id: 1 }),
1658 keyed(3u64, Captor { id: 2 }),
1659 ],
1660 );
1661 let mut w = prev.build(&mut ctx(&mut counter));
1662 layout_column(&mut w);
1663
1664 let mut log: Vec<u32> = Vec::new();
1665 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1667 assert!(w.children[1].is_active());
1668
1669 let removed: FlexView<Vec<u32>> = FlexView::new(
1671 Axis::Vertical,
1672 vec![keyed(1u64, Captor { id: 0 }), keyed(3u64, Captor { id: 2 })],
1673 );
1674 removed.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1675 assert_eq!(w.children.len(), 2);
1676 assert_eq!(w.flex.len(), 2);
1677 assert!(w.children.iter().all(|p| !p.is_active()));
1678
1679 layout_column(&mut w);
1680 dispatch(&mut w, &mut log, &ev(PointerPhase::Up, 10.0, row_y(1)));
1681 assert!(log.is_empty(), "removed active row does not fire on Up");
1682 }
1683
1684 #[test]
1685 fn keyed_reorder_preserves_focus_and_key_routing() {
1686 let mut counter = 0u64;
1690 let prev: FlexView<Vec<u32>> = FlexView::new(
1691 Axis::Vertical,
1692 vec![
1693 keyed(1u64, FocusRow { id: 1 }),
1694 keyed(2u64, FocusRow { id: 2 }),
1695 ],
1696 );
1697 let mut w = prev.build(&mut ctx(&mut counter));
1698 layout_column(&mut w);
1699
1700 let mut log: Vec<u32> = Vec::new();
1701 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(0)));
1703 assert!(w.children[0].is_focused());
1704
1705 let reordered: FlexView<Vec<u32>> = FlexView::new(
1708 Axis::Vertical,
1709 vec![
1710 keyed(2u64, FocusRow { id: 2 }),
1711 keyed(1u64, FocusRow { id: 1 }),
1712 ],
1713 );
1714 reordered.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1715 assert!(
1716 w.children[1].is_focused(),
1717 "the focused key-1 row keeps focus at its new index"
1718 );
1719 assert!(!w.children[0].is_focused());
1720
1721 layout_column(&mut w);
1723 dispatch(&mut w, &mut log, &key_event());
1724 assert_eq!(log, vec![1], "Key routes to the relocated focused row");
1725 }
1726
1727 #[test]
1728 fn keyed_removed_focused_key_clears_focus() {
1729 let mut counter = 0u64;
1732 let prev: FlexView<Vec<u32>> = FlexView::new(
1733 Axis::Vertical,
1734 vec![
1735 keyed(1u64, FocusRow { id: 1 }),
1736 keyed(2u64, FocusRow { id: 2 }),
1737 ],
1738 );
1739 let mut w = prev.build(&mut ctx(&mut counter));
1740 layout_column(&mut w);
1741
1742 let mut log: Vec<u32> = Vec::new();
1743 dispatch(&mut w, &mut log, &ev(PointerPhase::Down, 10.0, row_y(1)));
1745 assert!(w.children[1].is_focused());
1746
1747 let removed: FlexView<Vec<u32>> =
1749 FlexView::new(Axis::Vertical, vec![keyed(1u64, FocusRow { id: 1 })]);
1750 removed.rebuild(&prev, &mut w, &mut ctx(&mut counter));
1751 assert_eq!(w.children.len(), 1);
1752 assert!(
1753 w.children.iter().all(|p| !p.is_focused()),
1754 "the removed focused row leaves no focus path behind"
1755 );
1756
1757 layout_column(&mut w);
1758 dispatch(&mut w, &mut log, &key_event());
1759 assert!(
1760 log.is_empty(),
1761 "Key reaches nobody after the focused key is removed"
1762 );
1763 }
1764
1765 struct FocusRow {
1768 id: u32,
1769 }
1770 struct FocusRowWidget {
1772 id: u32,
1773 }
1774
1775 impl View<Vec<u32>> for FocusRow {
1776 type Element = FocusRowWidget;
1777 fn build(&self, _ctx: &mut BuildCtx<'_>) -> FocusRowWidget {
1778 FocusRowWidget { id: self.id }
1779 }
1780 fn rebuild(
1781 &self,
1782 _prev: &Self,
1783 element: &mut FocusRowWidget,
1784 _ctx: &mut BuildCtx<'_>,
1785 ) -> ChangeFlags {
1786 element.id = self.id;
1787 ChangeFlags::NONE
1788 }
1789 }
1790
1791 impl Widget for FocusRowWidget {
1792 fn layout(&mut self, _ctx: &mut LayoutCtx, bc: &BoxConstraints) -> Size {
1793 bc.constrain(Size::new(ROW_W, ROW_H))
1794 }
1795 fn paint(&mut self, _ctx: &mut PaintCtx, _scene: &mut dyn PaintScene) {}
1796 fn event(&mut self, ctx: &mut EventCtx, event: &InputEvent) -> EventResult {
1797 match event {
1798 InputEvent::Pointer(p) if p.phase == PointerPhase::Down => {
1799 ctx.request_focus();
1800 EventResult::Handled
1801 }
1802 InputEvent::Key(_) => {
1805 ctx.state_mut::<Vec<u32>>().push(self.id);
1806 EventResult::Handled
1807 }
1808 _ => EventResult::Ignored,
1809 }
1810 }
1811 }
1812
1813 fn key_event() -> InputEvent {
1815 InputEvent::Key(KeyEvent {
1816 key: Key::Character("a".to_string()),
1817 modifiers: Modifiers::default(),
1818 repeat: false,
1819 })
1820 }
1821}