Skip to main content

gpui_motion_kit/
scroll_native.rs

1//! Native measured scroll-to-target mapping.
2use 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/// Native scroll axis.
13#[derive(Clone, Copy, Debug)]
14pub enum Axis {
15    /// Horizontal.
16    X,
17    /// Vertical.
18    Y,
19}
20
21/// Reads native scroll offsets and extents; unscrollable content returns zero.
22/// Handles Kit's negative scroll offsets and positive maximum extents.
23pub 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
41/// Binds native scroll progress to partial numeric targets. Defaults to instant
42/// mapping; choose a spring for smoothing. Layout extent changes trigger a
43/// follow-up frame, so the previous measurement cannot remain stale after resize.
44/// Preserve its Kit-scoped ID and configure styles before wrapping.
45pub 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    /// Creates a measured scroll binding.
57    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    /// Selects optional smoothing.
77    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
188// Map renderer-measured progress to the engine's public numeric target API.
189fn 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}