use gpui::{ App, Bounds, ElementId, Entity, IntoElement, Pixels, SharedString, Styled, Window, canvas, }; use super::spring; /// Start and length of an item along the axis a marker slides on. pub(crate) type Span = (Pixels, Pixels); #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub(crate) enum Axis { Horizontal, Vertical, } impl Axis { fn span(self, bounds: Bounds, origin: Pixels) -> Span { match self { Axis::Horizontal => (bounds.left() - origin, bounds.size.width), Axis::Vertical => (bounds.top() - origin, bounds.size.height), } } fn start(self, bounds: Bounds) -> Pixels { match self { Axis::Horizontal => bounds.left(), Axis::Vertical => bounds.top(), } } } /// Where a sliding marker's items sit, and where it last stood. #[derive(Default)] pub(crate) struct Slide { origin: Pixels, spans: Vec>, from: Option, last: Option, generation: u64, } /// A marker's trip: from the last choice to this one, keyed by `generation`. pub(crate) struct Marker { pub from: Span, pub to: Span, pub generation: u64, } /// The slide over `values` with `selected` chosen; no marker until that item is measured. pub(crate) fn slide( id: impl Into, values: &[SharedString], selected: &SharedString, window: &mut Window, cx: &mut App, ) -> (Entity, Option) { let chosen = values .iter() .position(|value| value == selected) .unwrap_or_else(|| panic!("no item {selected} to slide to")); let state = window.use_keyed_state(id, cx, |_, _| Slide::default()); if state.read(cx).spans.len() != values.len() { state.update(cx, |slide, _| slide.spans = vec![None; values.len()]); } if state.read(cx).last.as_ref() != Some(selected) { state.update(cx, |slide, _| { let previous = slide .last .as_ref() .and_then(|last| values.iter().position(|value| value == last)); slide.from = previous.and_then(|ix| slide.spans[ix]); slide.last = Some(selected.clone()); slide.generation += 1; }); } let marker = { let slide = state.read(cx); slide.spans[chosen].map(|to| Marker { from: slide.from.unwrap_or(to), to, generation: slide.generation, }) }; (state, marker) } /// Records item `ix`'s span; place it as an absolute child that fills the item. pub(crate) fn measure_item(state: Entity, ix: usize, axis: Axis) -> impl IntoElement { canvas( move |bounds: Bounds, window, cx| { let span = Some(axis.span(bounds, state.read(cx).origin)); if state.read(cx).spans.get(ix) != Some(&span) { state.update(cx, |slide, cx| { slide.spans[ix] = span; cx.notify(); window.request_animation_frame(); }); } }, |_, _, _, _| {}, ) .absolute() .top_0() .left_0() .size_full() } /// Records where the container starts; place it as an absolute child that fills it. pub(crate) fn measure_origin(state: Entity, axis: Axis) -> impl IntoElement { canvas( move |bounds: Bounds, window, cx| { let start = axis.start(bounds); if state.read(cx).origin != start { state.update(cx, |slide, cx| { slide.origin = start; cx.notify(); window.request_animation_frame(); }); } }, |_, _, _, _| {}, ) .absolute() .top_0() .left_0() .size_full() } /// A marker's span at `t` of its trip, overshooting a little. pub(crate) fn glide(from: Span, to: Span, t: f32) -> Span { let s = spring(t); let lerp = |a: Pixels, b: Pixels| a + (b - a) * s; (lerp(from.0, to.0), lerp(from.1, to.1)) } #[cfg(test)] mod tests { use gpui::px; use super::glide; #[test] fn glide_starts_at_from_and_settles_on_to() { let (from, to) = ((px(0.0), px(40.0)), (px(100.0), px(60.0))); assert_eq!(glide(from, to, 0.0), from); let (left, width) = glide(from, to, 1.0); assert!((f32::from(left) - 100.0).abs() < 0.5 && (f32::from(width) - 60.0).abs() < 0.5); } }