T11c motion: transitions, stagger, presence that folds, flip that glides, drag to reorder; motion keeps each row's identity

This commit is contained in:
ZacharyZhang-NY committed 2026-09-25 18:01:29 -04:00
1 parent ce31c7fb06
commit 8e7e7ccace
10 files changed
+1156 -10

No files matched your search

+189
View File
@@ -150,3 +150,192 @@ fn a_sweeping_bar_keeps_its_thickness_in_a_tall_column(cx: &mut TestAppContext)
});
assert_eq!(after.get(), thickness, "the bar took only its thickness");
}
/// A transition, a presence list and a reorderable list, recording what they report.
struct Stage {
shown: bool,
rows: Vec<(&'static str, bool)>,
order: Vec<&'static str>,
/// The top of what sits below the transition.
drawn: Rc<Cell<usize>>,
/// Each reorder row's keyed state, by row.
seen: Rc<std::cell::RefCell<std::collections::HashMap<&'static str, gpui::EntityId>>>,
log: Vec<String>,
}
/// A 20px row that keeps keyed state and says which entity holds it.
#[derive(IntoElement)]
struct Marker {
key: &'static str,
seen: Rc<std::cell::RefCell<std::collections::HashMap<&'static str, gpui::EntityId>>>,
}
impl gpui::RenderOnce for Marker {
fn render(self, window: &mut Window, cx: &mut gpui::App) -> impl IntoElement {
let state = window.use_keyed_state(self.key, cx, |_, _| ());
self.seen.borrow_mut().insert(self.key, state.entity_id());
div().h(px(20.0)).child(self.key)
}
}
impl Render for Stage {
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let drawn = self.drawn.clone();
let (gone, moved) = (cx.entity(), cx.entity());
let presence = self.rows.iter().fold(
super::AnimatePresence::new("presence").on_gone(move |key, _, cx| {
gone.update(cx, |stage, _| stage.log.push(format!("gone {key}")))
}),
|list, (key, shown)| list.row(*key, *shown, div().h(px(20.0)).child(*key)),
);
let reorder = self.order.iter().fold(
super::Reorder::new("reorder").on_reorder(move |from, to, _, cx| {
moved.update(cx, |stage, cx| {
let row = stage.order.remove(from);
stage.order.insert(to, row);
stage.log.push(format!("moved {from} to {to}"));
cx.notify();
})
}),
|list, key| {
list.row(
*key,
Marker {
key,
seen: self.seen.clone(),
},
)
},
);
div()
.w(px(300.0))
.child(reorder)
.child(super::Transition::new("fade", self.shown).child(div().h(px(10.0))))
.child(
crate::primitives::Measure::new("below", move |bounds, _, _| {
drawn.set(f32::from(bounds.origin.y) as usize)
})
.h(px(1.0)),
)
.child(presence)
}
}
fn stage(cx: &mut TestAppContext) -> (gpui::Entity<Stage>, &mut VisualTestContext) {
cx.update(|cx| {
Theme::init(cx);
Theme::update(cx, |theme| theme.reduced_motion = true);
});
let (view, cx) = cx.add_window_view(|_, _| Stage {
shown: true,
rows: vec![("a", true), ("b", true)],
order: vec!["one", "two", "three", "four", "five"],
drawn: Rc::new(Cell::new(0)),
seen: Rc::default(),
log: Vec::new(),
});
settle(cx);
(view, cx)
}
fn settle(cx: &mut VisualTestContext) {
for _ in 0..3 {
std::thread::sleep(std::time::Duration::from_millis(2));
cx.update(|window, _| window.refresh());
cx.run_until_parked();
}
}
#[gpui::test]
fn a_hidden_transition_leaves_then_stops_drawing(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let below = view.read_with(cx, |stage, _| stage.drawn.clone());
assert_eq!(below.get(), 110, "the reorder list, then the shown content");
view.update(cx, |stage, cx| {
stage.shown = false;
cx.notify();
});
settle(cx);
assert_eq!(below.get(), 100, "the content left the layout");
}
#[gpui::test]
fn a_row_marked_gone_is_reported_once(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
view.update(cx, |stage, cx| {
stage.rows[1].1 = false;
cx.notify();
});
settle(cx);
settle(cx);
let log = view.read_with(cx, |stage, _| stage.log.clone());
assert_eq!(log, ["gone b"]);
}
#[gpui::test]
fn dragging_a_row_down_moves_it_past_the_rows_it_passes(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let at = |y: f32| point(px(100.0), px(y));
cx.simulate_mouse_move(at(10.0), None, Modifiers::none());
cx.simulate_mouse_down(at(10.0), gpui::MouseButton::Left, Modifiers::none());
for y in [16.0, 30.0, 45.0, 50.0] {
cx.simulate_mouse_move(at(y), Some(gpui::MouseButton::Left), Modifiers::none());
cx.run_until_parked();
}
cx.simulate_mouse_up(at(50.0), gpui::MouseButton::Left, Modifiers::none());
settle(cx);
let (order, log) = view.read_with(cx, |stage, _| (stage.order.clone(), stage.log.clone()));
assert_eq!(log, ["moved 0 to 2"]);
assert_eq!(order, ["two", "three", "one", "four", "five"]);
}
#[gpui::test]
fn a_moved_row_keeps_its_state(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let seen = view.read_with(cx, |stage, _| stage.seen.clone());
let before = seen.borrow()["one"];
view.update(cx, |stage, cx| {
stage.order.reverse();
cx.notify();
});
settle(cx);
assert_eq!(
seen.borrow()["one"],
before,
"the same state after the move"
);
}
#[gpui::test]
fn a_list_that_shrinks_under_a_drag_lets_it_go(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
let at = |y: f32| point(px(100.0), px(y));
cx.simulate_mouse_move(at(90.0), None, Modifiers::none());
cx.simulate_mouse_down(at(90.0), gpui::MouseButton::Left, Modifiers::none());
for y in [84.0, 60.0] {
cx.simulate_mouse_move(at(y), Some(gpui::MouseButton::Left), Modifiers::none());
cx.run_until_parked();
}
view.update(cx, |stage, cx| {
stage.order.truncate(2);
cx.notify();
});
settle(cx);
cx.simulate_mouse_up(at(20.0), gpui::MouseButton::Left, Modifiers::none());
settle(cx);
let log = view.read_with(cx, |stage, _| stage.log.clone());
assert!(log.is_empty(), "no move for a row that left: {log:?}");
}
#[gpui::test]
fn a_row_hidden_from_the_start_is_gone_at_once(cx: &mut TestAppContext) {
let (view, cx) = stage(cx);
cx.update(|_, cx| Theme::update(cx, |theme| theme.reduced_motion = false));
view.update(cx, |stage, cx| {
stage.rows.push(("c", false));
cx.notify();
});
settle(cx);
let log = view.read_with(cx, |stage, _| stage.log.clone());
assert_eq!(log, ["gone c"], "no fold to wait for");
}