1use crate::MotionState;
3use crate::core::{Motion, Target, Transition};
4
5use gpui_kit::{
6 AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
7 LayoutId, Pixels, ScrollHandle, Styled, Window,
8};
9
10use std::time::Instant;
11
12#[derive(Clone, Copy, Debug)]
14pub enum Axis {
15 X,
17 Y,
19}
20
21pub fn native_scroll_progress(handle: &ScrollHandle, axis: Axis) -> f64 {
24 let offset = handle.offset();
25 let max = handle.max_offset();
26
27 let (offset, max) = match axis {
28 Axis::X => (offset.x, max.x),
29 Axis::Y => (offset.y, max.y),
30 };
31
32 let max = f64::from(max).abs();
33
34 if max == 0. {
35 0.
36 } else {
37 (-f64::from(offset) / max).clamp(0., 1.)
38 }
39}
40
41pub struct ScrollMotion<E> {
46 id: ElementId,
47 child: Option<E>,
48 handle: ScrollHandle,
49 axis: Axis,
50 from: Target,
51 to: Target,
52 transition: Transition,
53}
54
55impl<E> ScrollMotion<E> {
56 pub fn new(
58 id: impl Into<ElementId>,
59 handle: ScrollHandle,
60 axis: Axis,
61 from: Target,
62 to: Target,
63 child: E,
64 ) -> Self {
65 Self {
66 id: id.into(),
67 child: Some(child),
68 handle,
69 axis,
70 from,
71 to,
72 transition: Transition::Instant,
73 }
74 }
75
76 pub fn transition(mut self, transition: Transition) -> Self {
78 self.transition = transition;
79
80 self
81 }
82}
83
84struct History {
85 state: MotionState,
86 epoch: Instant,
87 progress: f64,
88}
89
90impl<E: IntoElement + Styled + 'static> IntoElement for ScrollMotion<E> {
91 type Element = Self;
92
93 fn into_element(self) -> Self {
94 self
95 }
96}
97
98impl<E: IntoElement + Styled + 'static> Element for ScrollMotion<E> {
99 type RequestLayoutState = AnyElement;
100 type PrepaintState = ();
101
102 fn id(&self) -> Option<ElementId> {
103 Some(self.id.clone())
104 }
105
106 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
107 None
108 }
109
110 fn request_layout(
111 &mut self,
112 id: Option<&GlobalElementId>,
113 _: Option<&InspectorElementId>,
114 w: &mut Window,
115 cx: &mut App,
116 ) -> (LayoutId, AnyElement) {
117 let progress = native_scroll_progress(&self.handle, self.axis);
118 let now = cx.background_executor().now();
119 let target = interpolate(self.from, self.to, progress);
120
121 let frame = w.with_element_state(id.expect("scroll ID"), |state: Option<History>, w| {
122 let mut state = state.unwrap_or_else(|| History {
123 state: MotionState::default(),
124 epoch: now,
125 progress,
126 });
127
128 state.progress = progress;
129 let frame = state.state.sample(
130 Motion::new(target).transition(self.transition),
131 now.saturating_duration_since(state.epoch),
132 cx.reduce_motion(),
133 );
134
135 if frame.is_animating() {
136 w.request_animation_frame();
137 }
138
139 (frame, state)
140 });
141
142 let mut child = frame
143 .apply(self.child.take().expect("one layout"))
144 .into_any_element();
145
146 let layout = child.request_layout(w, cx);
147
148 (layout, child)
149 }
150
151 fn prepaint(
152 &mut self,
153 id: Option<&GlobalElementId>,
154 _: Option<&InspectorElementId>,
155 _: Bounds<Pixels>,
156 child: &mut AnyElement,
157 w: &mut Window,
158 cx: &mut App,
159 ) {
160 child.prepaint(w, cx);
161 let progress = native_scroll_progress(&self.handle, self.axis);
162
163 w.with_element_state(id.expect("scroll ID"), |state: Option<History>, w| {
164 let state = state.expect("layout first");
165
166 if (state.progress - progress).abs() > 1e-9 {
167 w.request_animation_frame();
168 }
169
170 ((), state)
171 });
172 }
173
174 fn paint(
175 &mut self,
176 _: Option<&GlobalElementId>,
177 _: Option<&InspectorElementId>,
178 _: Bounds<Pixels>,
179 child: &mut AnyElement,
180 _: &mut (),
181 w: &mut Window,
182 cx: &mut App,
183 ) {
184 child.paint(w, cx);
185 }
186}
187
188fn interpolate(from: Target, to: Target, progress: f64) -> Target {
190 let blend = |a: f32, b: f32| (f64::from(a) + (f64::from(b) - f64::from(a)) * progress) as f32;
191
192 let mut target = Target::default()
193 .x(blend(from.x_px(), to.x_px()))
194 .y(blend(from.y_px(), to.y_px()))
195 .opacity(blend(from.alpha(), to.alpha()));
196
197 for (a, b, property) in [
198 (from.width_px(), to.width_px(), 0),
199 (from.height_px(), to.height_px(), 1),
200 (from.border_radius_px(), to.border_radius_px(), 2),
201 ] {
202 if let Some(value) = a.or(b) {
203 let value = blend(a.unwrap_or(value), b.unwrap_or(value));
204
205 target = match property {
206 0 => target.width(value),
207 1 => target.height(value),
208 _ => target.border_radius(value),
209 };
210 }
211 }
212
213 target
214}