use crate::MotionState;
use crate::core::{Motion, Target, Transition};
use gpui_kit::{
AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
LayoutId, Pixels, ScrollHandle, Styled, Window,
};
use std::time::Instant;
#[derive(Clone, Copy, Debug)]
pub enum Axis {
X,
Y,
}
pub fn native_scroll_progress(handle: &ScrollHandle, axis: Axis) -> f64 {
let offset = handle.offset();
let max = handle.max_offset();
let (offset, max) = match axis {
Axis::X => (offset.x, max.x),
Axis::Y => (offset.y, max.y),
};
let max = f64::from(max).abs();
if max == 0. {
0.
} else {
(-f64::from(offset) / max).clamp(0., 1.)
}
}
pub struct ScrollMotion<E> {
id: ElementId,
child: Option<E>,
handle: ScrollHandle,
axis: Axis,
from: Target,
to: Target,
transition: Transition,
}
impl<E> ScrollMotion<E> {
pub fn new(
id: impl Into<ElementId>,
handle: ScrollHandle,
axis: Axis,
from: Target,
to: Target,
child: E,
) -> Self {
Self {
id: id.into(),
child: Some(child),
handle,
axis,
from,
to,
transition: Transition::Instant,
}
}
pub fn transition(mut self, transition: Transition) -> Self {
self.transition = transition;
self
}
}
struct History {
state: MotionState,
epoch: Instant,
progress: f64,
}
impl<E: IntoElement + Styled + 'static> IntoElement for ScrollMotion<E> {
type Element = Self;
fn into_element(self) -> Self {
self
}
}
impl<E: IntoElement + Styled + 'static> Element for ScrollMotion<E> {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
Some(self.id.clone())
}
fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
w: &mut Window,
cx: &mut App,
) -> (LayoutId, AnyElement) {
let progress = native_scroll_progress(&self.handle, self.axis);
let now = cx.background_executor().now();
let target = interpolate(self.from, self.to, progress);
let frame = w.with_element_state(id.expect("scroll ID"), |state: Option<History>, w| {
let mut state = state.unwrap_or_else(|| History {
state: MotionState::default(),
epoch: now,
progress,
});
state.progress = progress;
let frame = state.state.sample(
Motion::new(target).transition(self.transition),
now.saturating_duration_since(state.epoch),
cx.reduce_motion(),
);
if frame.is_animating() {
w.request_animation_frame();
}
(frame, state)
});
let mut child = frame
.apply(self.child.take().expect("one layout"))
.into_any_element();
let layout = child.request_layout(w, cx);
(layout, child)
}
fn prepaint(
&mut self,
id: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
child: &mut AnyElement,
w: &mut Window,
cx: &mut App,
) {
child.prepaint(w, cx);
let progress = native_scroll_progress(&self.handle, self.axis);
w.with_element_state(id.expect("scroll ID"), |state: Option<History>, w| {
let state = state.expect("layout first");
if (state.progress - progress).abs() > 1e-9 {
w.request_animation_frame();
}
((), state)
});
}
fn paint(
&mut self,
_: Option<&GlobalElementId>,
_: Option<&InspectorElementId>,
_: Bounds<Pixels>,
child: &mut AnyElement,
_: &mut (),
w: &mut Window,
cx: &mut App,
) {
child.paint(w, cx);
}
}
fn interpolate(from: Target, to: Target, progress: f64) -> Target {
let blend = |a: f32, b: f32| (f64::from(a) + (f64::from(b) - f64::from(a)) * progress) as f32;
let mut target = Target::default()
.x(blend(from.x_px(), to.x_px()))
.y(blend(from.y_px(), to.y_px()))
.opacity(blend(from.alpha(), to.alpha()));
for (a, b, property) in [
(from.width_px(), to.width_px(), 0),
(from.height_px(), to.height_px(), 1),
(from.border_radius_px(), to.border_radius_px(), 2),
] {
if let Some(value) = a.or(b) {
let value = blend(a.unwrap_or(value), b.unwrap_or(value));
target = match property {
0 => target.width(value),
1 => target.height(value),
_ => target.border_radius(value),
};
}
}
target
}