Skip to main content

gpui_ce_motion/
scroll_native.rs

1//! Native measured scroll-to-target mapping.
2use crate::{Motion, MotionState, Target, Transition};
3
4use gpui::{
5    AnyElement, App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement,
6    LayoutId, Pixels, ScrollHandle, Styled, Window,
7};
8
9use std::time::Instant;
10
11/// Native scroll axis.
12#[derive(Clone, Copy, Debug)]
13pub enum Axis {
14    /// Horizontal.
15    X,
16    /// Vertical.
17    Y,
18}
19
20/// Reads native scroll offsets and extents; unscrollable content returns zero.
21/// Handles CE's negative scroll offsets and positive maximum extents.
22pub fn native_scroll_progress(handle: &ScrollHandle, axis: Axis) -> f64 {
23    let offset = handle.offset();
24    let max = handle.max_offset();
25
26    let (offset, max) = match axis {
27        Axis::X => (offset.x, max.x),
28        Axis::Y => (offset.y, max.y),
29    };
30
31    let max = f64::from(max).abs();
32
33    if max == 0. {
34        0.
35    } else {
36        (-f64::from(offset) / max).clamp(0., 1.)
37    }
38}
39
40/// Binds native scroll progress to partial numeric targets. Defaults to instant
41/// mapping; choose a spring for smoothing. Layout extent changes trigger a
42/// follow-up frame, so the previous measurement cannot remain stale after resize.
43/// Preserve its CE-scoped ID and configure styles before wrapping.
44pub struct ScrollMotion<E> {
45    id: ElementId,
46    child: Option<E>,
47    handle: ScrollHandle,
48    axis: Axis,
49    from: Target,
50    to: Target,
51    transition: Transition,
52}
53
54impl<E> ScrollMotion<E> {
55    /// Creates a measured scroll binding.
56    pub fn new(
57        id: impl Into<ElementId>,
58        handle: ScrollHandle,
59        axis: Axis,
60        from: Target,
61        to: Target,
62        child: E,
63    ) -> Self {
64        Self {
65            id: id.into(),
66            child: Some(child),
67            handle,
68            axis,
69            from,
70            to,
71            transition: Transition::Instant,
72        }
73    }
74
75    /// Selects optional smoothing.
76    pub fn transition(mut self, transition: Transition) -> Self {
77        self.transition = transition;
78
79        self
80    }
81}
82
83struct History {
84    state: MotionState,
85    epoch: Instant,
86    progress: f64,
87}
88
89impl<E: IntoElement + Styled + 'static> IntoElement for ScrollMotion<E> {
90    type Element = Self;
91
92    fn into_element(self) -> Self {
93        self
94    }
95}
96
97impl<E: IntoElement + Styled + 'static> Element for ScrollMotion<E> {
98    type RequestLayoutState = AnyElement;
99    type PrepaintState = ();
100
101    fn id(&self) -> Option<ElementId> {
102        Some(self.id.clone())
103    }
104
105    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
106        None
107    }
108
109    fn request_layout(
110        &mut self,
111        id: Option<&GlobalElementId>,
112        _: Option<&InspectorElementId>,
113        w: &mut Window,
114        cx: &mut App,
115    ) -> (LayoutId, AnyElement) {
116        let progress = native_scroll_progress(&self.handle, self.axis);
117        let now = cx.background_executor().now();
118        let from = self.from.inherit(self.to);
119        let to = self.to.inherit(self.from);
120
121        let target = Target::from_values(
122            std::array::from_fn(|i| {
123                from.values()[i] + (to.values()[i] - from.values()[i]) * progress
124            }),
125            from.mask() | to.mask(),
126        );
127
128        let frame = w.with_element_state(id.expect("scroll ID"), |state: Option<History>, w| {
129            let mut state = state.unwrap_or_else(|| History {
130                state: MotionState::default(),
131                epoch: now,
132                progress,
133            });
134
135            state.progress = progress;
136            let frame = state.state.sample(
137                Motion::new(target).transition(self.transition),
138                now.saturating_duration_since(state.epoch),
139                cx.reduce_motion(),
140            );
141
142            if frame.is_animating() {
143                w.request_animation_frame();
144            }
145
146            (frame, state)
147        });
148
149        let mut child = frame
150            .apply(self.child.take().expect("one layout"))
151            .into_any_element();
152
153        let layout = child.request_layout(w, cx);
154
155        (layout, child)
156    }
157
158    fn prepaint(
159        &mut self,
160        id: Option<&GlobalElementId>,
161        _: Option<&InspectorElementId>,
162        _: Bounds<Pixels>,
163        child: &mut AnyElement,
164        w: &mut Window,
165        cx: &mut App,
166    ) {
167        child.prepaint(w, cx);
168        let progress = native_scroll_progress(&self.handle, self.axis);
169
170        w.with_element_state(id.expect("scroll ID"), |state: Option<History>, w| {
171            let state = state.expect("layout first");
172
173            if (state.progress - progress).abs() > 1e-9 {
174                w.request_animation_frame();
175            }
176
177            ((), state)
178        });
179    }
180
181    fn paint(
182        &mut self,
183        _: Option<&GlobalElementId>,
184        _: Option<&InspectorElementId>,
185        _: Bounds<Pixels>,
186        child: &mut AnyElement,
187        _: &mut (),
188        w: &mut Window,
189        cx: &mut App,
190    ) {
191        child.paint(w, cx);
192    }
193}