use std::cell::RefCell;
use std::panic::Location;
use std::rc::Rc;
use std::time::Duration;
use gpui::{
App, AvailableSpace, Bounds, Element, ElementId, GlobalElementId, InspectorElementId,
IntoElement, LayoutId, Pixels, Point, SharedString, Size, Style, Window, px, size,
};
use gpui_kit_theme::{ActiveTheme, SpringPreset};
use web_time::Instant;
use super::keyed;
use super::{Interpolate, MotionSpec, Spring, Transition};
const EPSILON: f32 = 0.5;
const MEMORY: Duration = Duration::from_millis(500);
const HANDOFF_GRACE: u64 = 30;
#[derive(Default)]
struct FlipState {
origin: Option<Point<Pixels>>,
from: Point<Pixels>,
current: Point<Pixels>,
elapsed: Duration,
last_frame: Option<Instant>,
size: Option<Transition<Size<Pixels>>>,
natural: Option<Size<Pixels>>,
available: Option<Size<AvailableSpace>>,
offered: bool,
recorded_at: Option<Instant>,
size_frame: Option<Instant>,
seen_frame: Option<u64>,
contested_through: Option<u64>,
}
impl FlipState {
fn advance(&mut self, now: Instant) {
if let Some(last) = self.last_frame {
self.elapsed += now.saturating_duration_since(last);
}
self.last_frame = Some(now);
}
fn sample(&self, spring: Spring, settle: Duration) -> Point<Pixels> {
if self.elapsed >= settle {
return Point::default();
}
self.from.lerp(Point::default(), spring.value(self.elapsed))
}
fn record(&mut self, origin: Point<Pixels>, residual: Point<Pixels>) {
if let Some(previous) = self.origin
&& (moved(previous.x, origin.x) || moved(previous.y, origin.y))
{
self.from = previous - origin + residual;
self.elapsed = Duration::ZERO;
}
self.origin = Some(origin);
}
fn record_size(
&mut self,
natural: Size<Pixels>,
spec: MotionSpec,
now: Instant,
) -> Size<Pixels> {
self.natural = Some(natural);
let mut transition = self
.size
.unwrap_or_else(|| Transition::new(natural, spec))
.spec(spec);
if let Some(last) = self.size_frame {
transition.advance(now.saturating_duration_since(last));
}
self.size_frame = Some(now);
let target = transition.target();
if moved(target.width, natural.width) || moved(target.height, natural.height) {
transition.set(natural);
}
self.size = Some(transition);
transition.value()
}
fn settle_size(&mut self, natural: Size<Pixels>, spec: MotionSpec, now: Instant) {
self.natural = Some(natural);
let mut transition = self
.size
.unwrap_or_else(|| Transition::new(natural, spec))
.spec(spec);
transition.snap(natural);
self.size = Some(transition);
self.size_frame = Some(now);
}
fn settle(&mut self, origin: Point<Pixels>, settle: Duration) {
self.origin = Some(origin);
self.from = Point::default();
self.current = Point::default();
self.elapsed = settle;
self.last_frame = None;
}
fn forget_if_stale(&mut self, now: Instant) {
let stale = self
.recorded_at
.is_some_and(|at| now.saturating_duration_since(at) > MEMORY);
if stale {
self.origin = None;
self.from = Point::default();
self.current = Point::default();
self.elapsed = Duration::ZERO;
self.last_frame = None;
self.size = None;
self.natural = None;
self.size_frame = None;
}
self.recorded_at = Some(now);
}
fn claim(&mut self, frame: Option<u64>) -> bool {
let Some(frame) = frame else {
return false;
};
if self.seen_frame == Some(frame) {
self.contested_through = Some(frame + 1);
}
self.seen_frame = Some(frame);
self.contested_through
.is_some_and(|through| frame <= through)
}
}
fn moved(a: Pixels, b: Pixels) -> bool {
(f32::from(a) - f32::from(b)).abs() > EPSILON
}
#[derive(Clone)]
pub struct Flip {
id: SharedString,
state: Rc<RefCell<FlipState>>,
}
impl std::fmt::Debug for Flip {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Flip")
.field("id", &self.id)
.field("offset", &self.offset())
.field("size", &self.size())
.finish()
}
}
impl Flip {
pub fn id(&self) -> &SharedString {
&self.id
}
pub fn offset(&self) -> Point<Pixels> {
self.state.borrow().current
}
pub fn size(&self) -> Option<Size<Pixels>> {
self.state.borrow().size.map(|size| size.value())
}
pub fn target_size(&self) -> Option<Size<Pixels>> {
self.state.borrow().natural
}
pub fn is_contended(&self) -> bool {
let state = self.state.borrow();
match (state.seen_frame, state.contested_through) {
(Some(frame), Some(through)) => frame <= through,
_ => false,
}
}
pub fn is_animating(&self) -> bool {
let offset = self.offset();
let sliding = offset.x.abs() > px(EPSILON) || offset.y.abs() > px(EPSILON);
sliding || self.state.borrow().size.is_some_and(|s| s.is_animating())
}
}
pub fn flip(id: impl Into<SharedString>, cx: &mut App) -> Flip {
let id = id.into();
let state = keyed::slot::<FlipState>(&id, cx);
Flip { id, state }
}
pub fn shared_flip(id: impl Into<SharedString>, cx: &mut App) -> Flip {
let id = id.into();
let state = keyed::slot_retained::<FlipState>(&id, HANDOFF_GRACE, cx);
Flip { id, state }
}
pub fn tracked_ids(cx: &App) -> Vec<SharedString> {
keyed::ids::<FlipState>(cx)
}
pub trait Flipping: IntoElement + Sized {
fn flip(self, flip: &Flip, window: &mut Window, cx: &mut App) -> Flipped {
flipped(self, flip, false, window, cx)
}
fn flip_size(self, flip: &Flip, window: &mut Window, cx: &mut App) -> Flipped {
flipped(self, flip, true, window, cx)
}
}
fn flipped<E: IntoElement>(
element: E,
flip: &Flip,
sized: bool,
window: &mut Window,
cx: &mut App,
) -> Flipped {
let spring = Spring::preset(cx.theme(), SpringPreset::Grab);
let element = Flipped {
element: element.into_any_element(),
state: Rc::clone(&flip.state),
spring,
settle: spring.settle_time(),
sized,
measuring: false,
measured_against: None,
reduce_motion: cx.reduce_motion(),
frame: keyed::frame_counter(cx),
};
if flip.is_animating() {
window.request_animation_frame();
}
element
}
impl<E: IntoElement> Flipping for E {}
pub struct Flipped {
element: gpui::AnyElement,
state: Rc<RefCell<FlipState>>,
spring: Spring,
settle: Duration,
sized: bool,
measuring: bool,
measured_against: Option<Size<AvailableSpace>>,
reduce_motion: bool,
frame: Option<u64>,
}
impl std::fmt::Debug for Flipped {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("Flipped")
.field("offset", &self.state.borrow().current)
.field("sized", &self.sized)
.field("reduce_motion", &self.reduce_motion)
.finish()
}
}
impl Flipped {
fn spec(&self) -> MotionSpec {
MotionSpec::sprung(self.spring)
}
}
impl IntoElement for Flipped {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
impl Element for Flipped {
type RequestLayoutState = ();
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn source_location(&self) -> Option<&'static Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (LayoutId, ()) {
let now = cx.background_executor().now();
self.state.borrow_mut().forget_if_stale(now);
if !self.sized {
return (self.element.request_layout(window, cx), ());
}
let Some(available) = self.state.borrow().available else {
self.measuring = false;
return (self.element.request_layout(window, cx), ());
};
self.measuring = true;
self.measured_against = Some(available);
let natural = self.element.layout_as_root(available, window, cx);
let spec = self.spec();
let drawn = {
let mut state = self.state.borrow_mut();
if self.reduce_motion {
state.settle_size(natural, spec, now);
natural
} else {
state.record_size(natural, spec, now)
}
};
self.state.borrow_mut().offered = false;
let state = Rc::clone(&self.state);
let layout_id = window.request_measured_layout(
Style::default(),
move |known, available, _window, _cx| {
let mut state = state.borrow_mut();
if !state.offered {
state.offered = true;
state.available = Some(available);
}
drop(state);
size(
known.width.unwrap_or(drawn.width),
known.height.unwrap_or(drawn.height),
)
},
);
(layout_id, ())
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
bounds: Bounds<Pixels>,
_request_layout: &mut (),
window: &mut Window,
cx: &mut App,
) {
let origin = bounds.origin - window.element_offset();
let now = cx.background_executor().now();
let offset = {
let mut state = self.state.borrow_mut();
let contested = state.claim(self.frame);
if self.sized && !self.measuring {
state.available = Some(size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(bounds.size.height),
));
state.settle_size(bounds.size, self.spec(), now);
}
if self.reduce_motion || contested {
state.settle(origin, self.settle);
if self.sized
&& let Some(natural) = state.natural
{
state.settle_size(natural, self.spec(), now);
}
} else {
state.advance(now);
let residual = state.sample(self.spring, self.settle);
state.record(origin, residual);
state.current = state.sample(self.spring, self.settle);
if state.elapsed >= self.settle {
state.last_frame = None;
}
}
state.current
};
if self.measuring {
self.element.prepaint_as_root(
bounds.origin + offset,
size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(bounds.size.height),
),
window,
cx,
);
} else {
window.with_element_offset(offset, |window| {
self.element.prepaint(window, cx);
});
}
let state = self.state.borrow();
let growing = state.size.is_some_and(|size| size.is_animating());
let told_something_new = self.measuring && state.available != self.measured_against;
drop(state);
if offset != Point::default() || growing || told_something_new {
window.request_animation_frame();
}
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_inspector_id: Option<&InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut (),
_prepaint: &mut (),
window: &mut Window,
cx: &mut App,
) {
self.element.paint(window, cx);
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{point, size};
use gpui_kit_theme::Theme;
fn grab() -> Spring {
Spring::preset(&Theme::studio_dark(), SpringPreset::Grab)
}
fn spec() -> MotionSpec {
MotionSpec::sprung(grab())
}
struct Frames(Instant);
impl Frames {
fn new() -> Self {
Self(Instant::now())
}
fn step(&mut self) -> Instant {
self.0 += Duration::from_millis(8);
self.0
}
}
fn run_to_rest(
state: &mut FlipState,
natural: Size<Pixels>,
frames: &mut Frames,
) -> Size<Pixels> {
let mut drawn = natural;
for _ in 0..600 {
if !state.size.is_some_and(|size| size.is_animating()) {
break;
}
drawn = state.record_size(natural, spec(), frames.step());
}
drawn
}
#[test]
fn the_grab_spring_settles_sooner_than_the_snappy_one() {
let snappy = Spring::preset(&Theme::studio_dark(), SpringPreset::Snappy);
assert!(grab().settle_time() < snappy.settle_time());
}
#[test]
fn a_first_measurement_produces_no_offset() {
let mut state = FlipState::default();
state.record(point(px(10.0), px(20.0)), Point::default());
assert_eq!(state.sample(grab(), grab().settle_time()), Point::default());
}
#[test]
fn a_move_inverts_into_the_distance_travelled() {
let spring = grab();
let settle = spring.settle_time();
let mut state = FlipState::default();
state.record(point(px(0.0), px(0.0)), Point::default());
state.record(point(px(0.0), px(40.0)), Point::default());
assert_eq!(state.sample(spring, settle), point(px(0.0), px(-40.0)));
state.elapsed = settle;
assert_eq!(state.sample(spring, settle), Point::default());
}
#[test]
fn a_move_mid_slide_continues_from_what_is_on_screen() {
let spring = grab();
let settle = spring.settle_time();
let mut state = FlipState::default();
state.record(point(px(0.0), px(0.0)), Point::default());
state.record(point(px(0.0), px(40.0)), Point::default());
state.elapsed = settle / 2;
let residual = state.sample(spring, settle);
assert!(residual.y > px(-40.0) && residual.y < px(0.0));
state.record(point(px(0.0), px(60.0)), residual);
assert_eq!(
state.sample(spring, settle),
residual - point(px(0.0), px(20.0))
);
}
#[test]
fn sub_pixel_drift_does_not_start_a_slide() {
let spring = grab();
let settle = spring.settle_time();
let mut state = FlipState::default();
state.record(point(px(0.0), px(0.0)), Point::default());
state.record(point(px(0.2), px(0.3)), Point::default());
assert_eq!(state.sample(spring, settle), Point::default());
}
#[test]
fn a_first_size_is_drawn_at_once() {
let mut frames = Frames::new();
let mut state = FlipState::default();
let first = size(px(100.0), px(40.0));
assert_eq!(state.record_size(first, spec(), frames.step()), first);
assert!(!state.size.expect("recorded").is_animating());
}
#[test]
fn a_size_change_starts_at_the_old_size_and_lands_on_the_new_one() {
let mut frames = Frames::new();
let mut state = FlipState::default();
state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
let grown = size(px(200.0), px(80.0));
let drawn = state.record_size(grown, spec(), frames.step());
assert_eq!(
drawn,
size(px(100.0), px(40.0)),
"the first frame of a resize is the size it had"
);
assert_eq!(run_to_rest(&mut state, grown, &mut frames), grown);
}
#[test]
fn a_size_change_mid_animation_continues_from_the_size_on_screen() {
let mut frames = Frames::new();
let mut state = FlipState::default();
state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
let wider = size(px(200.0), px(40.0));
state.record_size(wider, spec(), frames.step());
let mut interrupted = size(px(100.0), px(40.0));
let mut caught = frames.step();
for _ in 0..6 {
caught = frames.step();
interrupted = state.record_size(wider, spec(), caught);
}
assert!(
interrupted.width > px(100.0) && interrupted.width < px(200.0),
"the animation has to be in flight for the claim to mean anything: {interrupted:?}"
);
let widest = size(px(300.0), px(40.0));
let drawn = state.record_size(widest, spec(), caught);
assert_eq!(
drawn, interrupted,
"a retarget starts from what is on screen rather than from the old size"
);
assert_eq!(run_to_rest(&mut state, widest, &mut frames), widest);
}
#[test]
fn sub_pixel_size_churn_starts_nothing() {
let mut frames = Frames::new();
let mut state = FlipState::default();
state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
let drawn = state.record_size(size(px(100.3), px(40.2)), spec(), frames.step());
assert_eq!(drawn, size(px(100.0), px(40.0)));
assert!(!state.size.expect("recorded").is_animating());
}
#[test]
fn a_settled_size_is_the_new_size_with_nothing_in_flight() {
let mut frames = Frames::new();
let mut state = FlipState::default();
state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
state.settle_size(size(px(200.0), px(80.0)), spec(), frames.step());
let transition = state.size.expect("recorded");
assert_eq!(transition.value(), size(px(200.0), px(80.0)));
assert!(!transition.is_animating());
}
#[test]
fn position_and_size_run_independently() {
let mut frames = Frames::new();
let spring = grab();
let settle = spring.settle_time();
let mut state = FlipState::default();
state.record(point(px(0.0), px(0.0)), Point::default());
state.record_size(size(px(100.0), px(40.0)), spec(), frames.step());
state.record(point(px(0.0), px(40.0)), Point::default());
let taller = size(px(100.0), px(90.0));
let drawn = state.record_size(taller, spec(), frames.step());
assert_eq!(state.sample(spring, settle), point(px(0.0), px(-40.0)));
assert_eq!(drawn, size(px(100.0), px(40.0)));
assert_eq!(run_to_rest(&mut state, taller, &mut frames), taller);
}
#[test]
fn a_rectangle_older_than_the_handoff_window_is_not_inverted_from() {
let start = Instant::now();
let mut state = FlipState::default();
state.forget_if_stale(start);
state.record(point(px(0.0), px(0.0)), Point::default());
state.record_size(size(px(100.0), px(40.0)), spec(), start);
state.forget_if_stale(start + MEMORY / 2);
state.record(point(px(0.0), px(300.0)), Point::default());
assert_ne!(
state.sample(grab(), grab().settle_time()),
Point::default(),
"a gap inside the window is a handoff and travels"
);
state.forget_if_stale(start + MEMORY / 2 + MEMORY * 2);
assert_eq!(state.origin, None, "a stale rectangle is forgotten");
assert_eq!(state.size, None);
state.record(point(px(0.0), px(600.0)), Point::default());
assert_eq!(
state.sample(grab(), grab().settle_time()),
Point::default(),
"an element with no recent rectangle is simply already in place"
);
}
#[test]
fn two_elements_sharing_an_id_in_one_frame_contest_it() {
let mut state = FlipState::default();
assert!(
!state.claim(Some(7)),
"one element per frame is no collision"
);
assert!(
state.claim(Some(7)),
"the second element in a frame collides"
);
assert!(
state.claim(Some(8)),
"the frame after a collision is still refused"
);
assert!(
!state.claim(Some(9)),
"a single renderer resumes once it has a rectangle of its own"
);
}
#[test]
fn a_host_without_a_frame_counter_never_reports_a_collision() {
let mut state = FlipState::default();
assert!(!state.claim(None));
assert!(!state.claim(None));
}
}