Every page and scripted state was shot with gpui 0.2.2 and with the port, light and dark, full width and at 280px, judged pair by pair by agents with skeptics, and every page's log diffed. What differed is fixed, each with a test that fails without it where the test platform can show it: - The gallery's harness takes posted mouse moves (gpui_macos listens to a tracking area), posts each drag move twice (it no longer replays a held drag), and waits three frames before each shot. - Rolling digits: a block column, so taffy 0.13 lends the cell no baseline, and a line of whole device pixels, since gpui snaps each length on its own. DescriptionList's row aligns ends. - Margins that taffy 0.13 collapses through a plain block: the drawer's pull and snap, the stagger and transition rise, the dialog and palette rises, the fullscreen dialog's slide. - An Input keeps room to type in a squeezed row; the field rows drop w_full, whose reason was taffy 0.9's. AGENTS.md's rules that named gpui 0.2.2 or taffy 0.9 were re-tested against both sources and rewritten, and new ones say what the port changed. TASKS.md's tags follow: what the new gpui makes possible is prove. Captions, doc comments and README say what holds now.
171 lines
5.0 KiB
Rust
171 lines
5.0 KiB
Rust
use std::time::{Duration, Instant};
|
|
|
|
use gpui::{
|
|
Animation, AnimationExt, AnyElement, App, ElementId, IntoElement, ParentElement, RenderOnce,
|
|
Styled, Window, div,
|
|
};
|
|
use smallvec::SmallVec;
|
|
|
|
use super::{BASE, NUDGE, duration, ease_out_cubic};
|
|
use crate::theme::ActiveTheme;
|
|
|
|
/// How content comes and goes.
|
|
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
|
pub enum Entrance {
|
|
#[default]
|
|
Fade,
|
|
/// Fades while rising from a little below.
|
|
Rise,
|
|
/// Fades while sliding in from the left, and out the same way.
|
|
Slide,
|
|
}
|
|
|
|
/// Where a transition stands: shown or not, since when, and how many times it turned.
|
|
struct Turns {
|
|
shown: bool,
|
|
since: Instant,
|
|
turn: usize,
|
|
}
|
|
|
|
/// Shows or hides its content with a fade, a rise or a slide. It keeps drawing the content while it leaves, so pass the content either way.
|
|
#[derive(IntoElement)]
|
|
pub struct Transition {
|
|
id: ElementId,
|
|
shown: bool,
|
|
entrance: Entrance,
|
|
body: SmallVec<[AnyElement; 2]>,
|
|
}
|
|
|
|
impl Transition {
|
|
pub fn new(id: impl Into<ElementId>, shown: bool) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
shown,
|
|
entrance: Entrance::default(),
|
|
body: SmallVec::new(),
|
|
}
|
|
}
|
|
|
|
pub fn entrance(mut self, entrance: Entrance) -> Self {
|
|
self.entrance = entrance;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Transition {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.body.extend(elements);
|
|
}
|
|
}
|
|
|
|
/// Moves `element` a share `t` of the way in: 0 is gone, 1 is in place.
|
|
pub(super) fn arrive<E: Styled>(element: E, entrance: Entrance, t: f32) -> E {
|
|
let t = ease_out_cubic(t);
|
|
match entrance {
|
|
Entrance::Fade => element.opacity(t),
|
|
// Padding, as taffy's size cache drops a margin that collapses through a block.
|
|
Entrance::Rise => element.opacity(t).pt(NUDGE * 2.0 * (1.0 - t)),
|
|
Entrance::Slide => element.opacity(t).ml(NUDGE * -4.0 * (1.0 - t)),
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Transition {
|
|
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let shown = self.shown;
|
|
let state = window.use_keyed_state((self.id.clone(), "turns"), cx, |_, _| Turns {
|
|
shown,
|
|
since: Instant::now(),
|
|
turn: 0,
|
|
});
|
|
if state.read(cx).shown != shown {
|
|
state.update(cx, |turns, _| {
|
|
turns.shown = shown;
|
|
turns.since = Instant::now();
|
|
turns.turn += 1;
|
|
});
|
|
}
|
|
let (since, turn) = (state.read(cx).since, state.read(cx).turn);
|
|
let length = duration(BASE, cx);
|
|
let leaving = !shown && turn > 0 && since.elapsed() < length;
|
|
if !shown && !leaving {
|
|
return div().into_any_element();
|
|
}
|
|
let share =
|
|
(since.elapsed().as_secs_f32() / length.as_secs_f32().max(f32::EPSILON)).min(1.0);
|
|
if turn > 0 && share < 1.0 {
|
|
window.request_animation_frame();
|
|
}
|
|
let content = div().children(self.body);
|
|
match turn {
|
|
0 => content.into_any_element(),
|
|
_ => arrive(
|
|
content,
|
|
self.entrance,
|
|
if shown { share } else { 1.0 - share },
|
|
)
|
|
.into_any_element(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Children that enter one after another, `step` apart, each arriving once.
|
|
#[derive(IntoElement)]
|
|
pub struct Stagger {
|
|
id: ElementId,
|
|
step: Duration,
|
|
entrance: Entrance,
|
|
body: SmallVec<[AnyElement; 4]>,
|
|
}
|
|
|
|
impl Stagger {
|
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
step: Duration::from_millis(60),
|
|
entrance: Entrance::Rise,
|
|
body: SmallVec::new(),
|
|
}
|
|
}
|
|
|
|
/// The gap between two children's starts.
|
|
pub fn step(mut self, step: Duration) -> Self {
|
|
self.step = step;
|
|
self
|
|
}
|
|
|
|
pub fn entrance(mut self, entrance: Entrance) -> Self {
|
|
self.entrance = entrance;
|
|
self
|
|
}
|
|
}
|
|
|
|
impl ParentElement for Stagger {
|
|
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
|
self.body.extend(elements);
|
|
}
|
|
}
|
|
|
|
impl RenderOnce for Stagger {
|
|
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
|
let length = duration(BASE, cx);
|
|
let step = if cx.theme().reduced_motion {
|
|
Duration::ZERO
|
|
} else {
|
|
self.step
|
|
};
|
|
let (id, entrance) = (self.id, self.entrance);
|
|
div().children(self.body.into_iter().enumerate().map(move |(ix, child)| {
|
|
let lead = step * ix as u32;
|
|
let total = lead + length;
|
|
div().child(child).with_animation(
|
|
(id.clone(), format!("child-{ix}")),
|
|
Animation::new(total),
|
|
move |child, t| {
|
|
let at = (t * total.as_secs_f32() - lead.as_secs_f32()) / length.as_secs_f32();
|
|
arrive(child, entrance, at.clamp(0.0, 1.0))
|
|
},
|
|
)
|
|
}))
|
|
}
|
|
}
|