Files
Ely-GPUI-Components/src/motion/list.rs
T

284 lines
9.4 KiB
Rust

use std::{collections::HashMap, rc::Rc};
use gpui::{
AnyElement, App, ElementId, Entity, IntoElement, ParentElement, Pixels, RenderOnce,
SharedString, Styled, Window, canvas, div,
};
use web_time::Instant;
use super::{BASE, duration, ease_in_out_cubic, ease_out_cubic};
use crate::theme::{ActiveTheme, Elevation};
/// Where a row sits, where it glides from, and since when.
#[derive(Clone, Copy)]
struct Place {
height: Pixels,
top: Pixels,
from: Pixels,
since: Instant,
turn: usize,
}
impl Place {
fn now(&self, length: std::time::Duration) -> Pixels {
let share =
(self.since.elapsed().as_secs_f32() / length.as_secs_f32().max(f32::EPSILON)).min(1.0);
self.from + (self.top - self.from) * ease_in_out_cubic(share)
}
}
/// Measures a row's height as it draws; a change asks for the next frame.
fn measure<T: 'static>(
state: &Entity<HashMap<SharedString, T>>,
key: SharedString,
height: impl Fn(&mut T) -> &mut Pixels + 'static,
) -> impl IntoElement {
let state = state.clone();
canvas(
move |bounds, window, cx| {
let changed = state.update(cx, |rows, _| {
let Some(row) = rows.get_mut(&key) else {
return false;
};
let slot = height(row);
let changed = *slot != bounds.size.height;
*slot = bounds.size.height;
changed
});
if changed {
state.update(cx, |_, cx| cx.notify());
window.request_animation_frame();
}
},
|_, _, _, _| {},
)
.absolute()
.top_0()
.left_0()
.size_full()
}
/// A column whose rows glide to their new places when the order changes: first, last, invert, play. Rows are keyed so each is followed.
#[derive(IntoElement)]
pub struct Flip {
id: ElementId,
rows: Vec<(SharedString, AnyElement)>,
lifted: Option<(SharedString, Pixels)>,
}
impl Flip {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
rows: Vec::new(),
lifted: None,
}
}
/// A row in display order; its key stays with it as the order changes.
pub fn row(mut self, key: impl Into<SharedString>, row: impl IntoElement) -> Self {
self.rows.push((key.into(), row.into_any_element()));
self
}
/// Draws one row raised at `top`, free of its slot, as a drag holds it.
pub(super) fn lifted(mut self, key: SharedString, top: Pixels) -> Self {
self.lifted = Some((key, top));
self
}
}
impl RenderOnce for Flip {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let state = window.use_keyed_state((self.id.clone(), "places"), cx, |_, _| {
HashMap::<SharedString, Place>::new()
});
let length = duration(BASE, cx);
let keys: Vec<SharedString> = self.rows.iter().map(|(key, _)| key.clone()).collect();
let places = state.update(cx, |places, _| {
places.retain(|key, _| keys.contains(key));
let measured = keys.iter().all(|key| {
places
.get(key)
.is_some_and(|place| place.height > Pixels::ZERO)
});
let mut top = Pixels::ZERO;
for key in &keys {
let place = places.entry(key.clone()).or_insert(Place {
height: Pixels::ZERO,
top,
from: top,
since: Instant::now(),
turn: 0,
});
if place.top != top {
place.from = if measured { place.now(length) } else { top };
place.top = top;
place.since = Instant::now();
place.turn += 1;
}
top += place.height;
}
(places.clone(), top)
});
let (places, total) = places;
if places
.values()
.any(|place| place.turn > 0 && place.since.elapsed() < length)
{
window.request_animation_frame();
}
let shadow = cx.theme().elevation(Elevation::Floating);
let rows = self.rows.into_iter().map(|(key, row)| {
let place = places[&key];
let slot = div()
.absolute()
.left_0()
.right_0()
.child(row)
.child(measure(&state, key.clone(), |place: &mut Place| {
&mut place.height
}));
match &self.lifted {
Some((lifted, top)) if *lifted == key => (
true,
slot.top(*top).shadow(shadow.clone()).into_any_element(),
),
_ => (false, slot.top(place.now(length)).into_any_element()),
}
});
let (mut rest, mut raised): (Vec<_>, Vec<_>) = (Vec::new(), Vec::new());
for (lifted, row) in rows {
if lifted {
raised.push(row)
} else {
rest.push(row)
}
}
rest.append(&mut raised);
div().relative().h(total).children(rest)
}
}
/// Whether a row shows, how tall it is, and when it last turned.
#[derive(Clone, Copy)]
struct Presence {
height: Pixels,
shown: bool,
since: Instant,
turn: usize,
told: bool,
}
type OnGone = Rc<dyn Fn(&SharedString, &mut Window, &mut App)>;
/// A hidden row is gone once its fold ends, or at once if it was never shown here.
fn gone(presence: &Presence, length: std::time::Duration) -> bool {
presence.turn == 0 || presence.since.elapsed() >= length
}
/// Rows that fold open and fade in when they arrive, and fold away when the owner marks them gone. `on_gone` says when a row may be dropped. Rows there at first stay still.
#[derive(IntoElement)]
pub struct AnimatePresence {
id: ElementId,
rows: Vec<(SharedString, bool, AnyElement)>,
on_gone: Option<OnGone>,
}
impl AnimatePresence {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
rows: Vec::new(),
on_gone: None,
}
}
/// A row, and whether it should show; keep passing it while it leaves.
pub fn row(mut self, key: impl Into<SharedString>, shown: bool, row: impl IntoElement) -> Self {
self.rows.push((key.into(), shown, row.into_any_element()));
self
}
pub fn on_gone(
mut self,
handler: impl Fn(&SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_gone = Some(Rc::new(handler));
self
}
}
impl RenderOnce for AnimatePresence {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let first = window.use_keyed_state((self.id.clone(), "first"), cx, |_, _| true);
let state = window.use_keyed_state((self.id.clone(), "rows"), cx, |_, _| {
HashMap::<SharedString, Presence>::new()
});
let still = *first.read(cx);
first.update(cx, |first, _| *first = false);
let length = duration(BASE, cx);
let on_gone = self.on_gone;
let mut rows = Vec::new();
for (key, shown, row) in self.rows {
let presence = state.update(cx, |rows, _| {
let presence = rows.entry(key.clone()).or_insert(Presence {
height: Pixels::ZERO,
shown,
since: Instant::now(),
turn: usize::from(!still && shown),
told: false,
});
if presence.shown != shown {
presence.shown = shown;
presence.since = Instant::now();
presence.turn += 1;
presence.told = false;
}
let copy = *presence;
if !shown && gone(presence, length) {
presence.told = true;
}
copy
});
let settled = presence.since.elapsed() >= length;
if !shown && gone(&presence, length) {
if let Some(on_gone) = on_gone.clone().filter(|_| !presence.told) {
let key = key.clone();
window.defer(cx, move |window, cx| on_gone(&key, window, cx));
}
continue;
}
if !settled && presence.turn > 0 {
window.request_animation_frame();
}
let frame = div().relative().overflow_hidden().child(
div()
.absolute()
.top_0()
.left_0()
.right_0()
.child(row)
.child(measure(&state, key.clone(), |row: &mut Presence| {
&mut row.height
})),
);
let share = (presence.since.elapsed().as_secs_f32()
/ length.as_secs_f32().max(f32::EPSILON))
.min(1.0);
let open = if presence.turn == 0 {
1.0
} else {
ease_out_cubic(if shown { share } else { 1.0 - share })
};
rows.push(
frame
.h(presence.height * open)
.opacity(open)
.into_any_element(),
);
}
div().flex().flex_col().children(rows)
}
}