Skip to main content

gpui_ce_motion/
geometry.rs

1//! Portable measured constraints, scroll intersections, and spatial reorder math.
2use std::fmt;
3
4/// Invalid finite geometry, scroll range, or edge-scroll configuration.
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct GeometryError;
7
8impl fmt::Display for GeometryError {
9    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
10        f.write_str("invalid motion geometry")
11    }
12}
13
14impl std::error::Error for GeometryError {}
15
16/// Window-space logical-pixel rectangle. Dimensions are finite and non-negative.
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub struct LayoutRect {
19    origin: [f64; 2],
20    size: [f64; 2],
21}
22
23impl LayoutRect {
24    /// Validates a measured rectangle.
25    pub fn new(origin: [f64; 2], size: [f64; 2]) -> Result<Self, GeometryError> {
26        if origin.iter().any(|v| !v.is_finite() || v.abs() > 1e12)
27            || size.iter().any(|v| !v.is_finite() || *v < 0.0 || *v > 1e12)
28        {
29            return Err(GeometryError);
30        }
31
32        Ok(Self { origin, size })
33    }
34
35    /// Top-left window coordinates.
36    pub fn origin(self) -> [f64; 2] {
37        self.origin
38    }
39
40    /// Width and height.
41    pub fn size(self) -> [f64; 2] {
42        self.size
43    }
44
45    /// Center in window coordinates.
46    pub fn center(self) -> [f64; 2] {
47        std::array::from_fn(|i| self.origin[i] + self.size[i] * 0.5)
48    }
49
50    /// Intersection with another rectangle in the same window coordinates.
51    pub fn intersection(self, other: Self) -> Self {
52        let origin = std::array::from_fn(|i| self.origin[i].max(other.origin[i]));
53
54        Self {
55            origin,
56            size: std::array::from_fn(|i| {
57                ((self.origin[i] + self.size[i]).min(other.origin[i] + other.size[i]) - origin[i])
58                    .max(0.0)
59            }),
60        }
61    }
62
63    /// Insets top/right/bottom/left; negative margins expand the rectangle.
64    /// Panics for non-finite margins. Over-insetting collapses at the leading edge.
65    pub fn inset(self, margins: [f64; 4]) -> Self {
66        assert!(
67            margins.iter().all(|v| v.is_finite() && v.abs() <= 1e12),
68            "invalid viewport margin"
69        );
70
71        Self {
72            origin: [self.origin[0] + margins[3], self.origin[1] + margins[0]],
73            size: [
74                (self.size[0] - margins[1] - margins[3]).max(0.0),
75                (self.size[1] - margins[0] - margins[2]).max(0.0),
76            ],
77        }
78    }
79}
80
81/// Allowed drag offsets relative to an element's untranslated layout rectangle.
82#[derive(Clone, Copy, Debug, PartialEq)]
83pub struct DragBounds {
84    min: [f64; 2],
85    max: [f64; 2],
86}
87
88impl DragBounds {
89    /// Keeps an element within its measured container. An oversized element is
90    /// pinned to the container's leading edge on that axis.
91    pub fn within(container: LayoutRect, element: LayoutRect) -> Self {
92        let min = std::array::from_fn(|i| container.origin[i] - element.origin[i]);
93
94        Self {
95            min,
96            max: std::array::from_fn(|i| min[i] + (container.size[i] - element.size[i]).max(0.0)),
97        }
98    }
99
100    /// Minimum x/y offsets.
101    pub fn min(self) -> [f64; 2] {
102        self.min
103    }
104
105    /// Maximum x/y offsets.
106    pub fn max(self) -> [f64; 2] {
107        self.max
108    }
109}
110
111/// Fractional intersection of a target and its container, with a pixel adjustment.
112/// Fractions may be outside `0..=1`, permitting anticipation and overscroll ranges.
113#[derive(Clone, Copy, Debug, PartialEq)]
114pub struct ScrollEdge {
115    target: f64,
116    container: f64,
117    pixels: f64,
118}
119
120impl ScrollEdge {
121    /// Creates an intersection. Zero means start, 0.5 center, and one end.
122    /// Panics for non-finite values or geometry beyond ±1e12.
123    pub fn new(target: f64, container: f64, pixels: f64) -> Self {
124        assert!(
125            [target, container, pixels]
126                .iter()
127                .all(|v| v.is_finite() && v.abs() <= 1e12),
128            "invalid scroll intersection"
129        );
130
131        Self {
132            target,
133            container,
134            pixels,
135        }
136    }
137
138    fn distance(self, container: LayoutRect, target: LayoutRect, axis: usize) -> f64 {
139        target.origin[axis] + target.size[axis] * self.target
140            - container.origin[axis]
141            - container.size[axis] * self.container
142            + self.pixels
143    }
144}
145
146/// Two measured scroll intersections, independent of browser string parsing.
147#[derive(Clone, Copy, Debug, PartialEq)]
148pub struct ScrollOffsets {
149    start: ScrollEdge,
150    end: ScrollEdge,
151    clamp: bool,
152}
153
154impl Default for ScrollOffsets {
155    fn default() -> Self {
156        Self::new(
157            ScrollEdge::new(0.0, 1.0, 0.0),
158            ScrollEdge::new(1.0, 0.0, 0.0),
159        )
160    }
161}
162
163impl ScrollOffsets {
164    /// Chooses start/end intersections; default progress is clamped.
165    pub fn new(start: ScrollEdge, end: ScrollEdge) -> Self {
166        Self {
167            start,
168            end,
169            clamp: true,
170        }
171    }
172
173    /// Allows progress outside zero through one for custom mappings.
174    pub fn unclamped(mut self) -> Self {
175        self.clamp = false;
176
177        self
178    }
179
180    /// Calculates progress from current window-space target/container measurements.
181    /// Axis zero is horizontal and one vertical. A collapsed range returns zero.
182    /// Panics for another axis.
183    pub fn progress(self, container: LayoutRect, target: LayoutRect, axis: usize) -> f64 {
184        assert!(axis < 2, "scroll axis must be zero or one");
185        let start = self.start.distance(container, target, axis);
186        let end = self.end.distance(container, target, axis);
187
188        let progress = if (start - end).abs() <= f64::EPSILON {
189            0.0
190        } else {
191            start / (start - end)
192        };
193
194        if self.clamp {
195            progress.clamp(0.0, 1.0)
196        } else {
197            progress
198        }
199    }
200}
201
202/// Edge autoscroll velocity policy. Use real elapsed time and the native scroll
203/// handle's current bounds; do not integrate a fixed distance per rendered frame.
204#[derive(Clone, Copy, Debug)]
205pub struct AutoScroll {
206    edge: f64,
207    speed: f64,
208}
209
210impl Default for AutoScroll {
211    fn default() -> Self {
212        Self {
213            edge: 48.0,
214            speed: 600.0,
215        }
216    }
217}
218
219impl AutoScroll {
220    /// Validates edge size in pixels and maximum speed in pixels/second.
221    pub fn new(edge: f64, speed: f64) -> Result<Self, GeometryError> {
222        if !edge.is_finite() || edge <= 0.0 || !speed.is_finite() || speed < 0.0 {
223            return Err(GeometryError);
224        }
225
226        Ok(Self { edge, speed })
227    }
228
229    /// Signed content-scroll velocity. Opposite edges cancel in a tiny viewport;
230    /// a pointer outside the viewport keeps the nearest edge at maximum speed.
231    pub fn velocity(self, pointer: f64, start: f64, length: f64) -> f64 {
232        assert!(
233            [pointer, start, length].iter().all(|v| v.is_finite()) && length >= 0.0,
234            "invalid scroll metrics"
235        );
236
237        if length == 0.0 {
238            return 0.0;
239        }
240
241        let edge = self.edge.min(length * 0.5);
242        let leading = ((start + edge - pointer) / edge).clamp(0.0, 1.0);
243        let trailing = ((pointer - (start + length - edge)) / edge).clamp(0.0, 1.0);
244
245        (trailing * trailing - leading * leading) * self.speed
246    }
247}
248
249/// Reorder detection mode, independent of native scroll axes.
250#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
251pub enum ReorderAxis {
252    /// Infer a row, column, or wrapped/grid layout from measured centers.
253    #[default]
254    Auto,
255    /// Horizontal list.
256    X,
257    /// Vertical list.
258    Y,
259    /// Two-dimensional nearest-center detection.
260    XY,
261}
262
263impl ReorderAxis {
264    /// Detects a row/column using overlapping cross-axis extents; a wrapped layout
265    /// uses both axes. Empty and single-item collections default to vertical.
266    pub fn resolve(self, rects: &[LayoutRect]) -> Self {
267        if self != Self::Auto {
268            return self;
269        }
270
271        if rects.len() < 2 {
272            return Self::Y;
273        }
274
275        let overlap = |axis: usize| {
276            let leading = rects
277                .iter()
278                .map(|r| r.origin[axis])
279                .fold(f64::NEG_INFINITY, f64::max);
280
281            let trailing = rects
282                .iter()
283                .map(|r| r.origin[axis] + r.size[axis])
284                .fold(f64::INFINITY, f64::min);
285
286            leading < trailing
287        };
288
289        if overlap(1) {
290            Self::X
291        } else if overlap(0) {
292            Self::Y
293        } else {
294            Self::XY
295        }
296    }
297
298    /// Finds the closest measured destination. Ties preserve the first index.
299    pub fn destination(self, position: [f64; 2], rects: &[LayoutRect]) -> Option<usize> {
300        assert!(
301            position.iter().all(|v| v.is_finite()),
302            "invalid reorder position"
303        );
304        let axis = self.resolve(rects);
305
306        rects
307            .iter()
308            .enumerate()
309            .min_by(|(_, a), (_, b)| {
310                let distance = |rect: LayoutRect| {
311                    let c = rect.center();
312
313                    let x = if axis == Self::Y {
314                        0.0
315                    } else {
316                        c[0] - position[0]
317                    };
318
319                    let y = if axis == Self::X {
320                        0.0
321                    } else {
322                        c[1] - position[1]
323                    };
324
325                    x.hypot(y)
326                };
327
328                distance(**a).total_cmp(&distance(**b))
329            })
330            .map(|(i, _)| i)
331    }
332}
333
334/// Native scroll sample, also suitable for feeding a portable graph or typed track.
335#[derive(Clone, Copy, Debug)]
336pub struct ScrollFrame {
337    /// Intersection progress, optionally smoothed/unclamped.
338    pub progress: f64,
339    /// Progress units per second.
340    pub velocity: f64,
341    /// Positive native content offsets in pixels.
342    pub offset: [f64; 2],
343    /// Both target and container have measured geometry.
344    pub ready: bool,
345    /// A smoothing trajectory remains active.
346    pub active: bool,
347}
348
349impl ScrollFrame {
350    /// Maps progress to partial numeric targets; drawable opacity/dimensions clamp
351    /// at native application time. Omitted dimensions inherit the other endpoint.
352    pub fn target(self, from: crate::Target, to: crate::Target) -> crate::Target {
353        let from = from.inherit(to);
354        let to = to.inherit(from);
355        let a = from.values();
356        let b = to.values();
357
358        crate::Target::from_values(
359            std::array::from_fn(|i| crate::value::finite(a[i] + (b[i] - a[i]) * self.progress)),
360            from.mask() | to.mask(),
361        )
362    }
363}
364
365/// Native pointer/pinch lifecycle phase.
366#[derive(Clone, Copy, Debug, PartialEq, Eq)]
367pub enum GesturePhase {
368    /// Gesture has acquired ownership.
369    Started,
370    /// Pointer/scale changed.
371    Moved,
372    /// Gesture committed.
373    Ended,
374    /// Escape, lost ownership, blur or platform cancellation.
375    Cancelled,
376}
377
378/// Structured pointer information in logical window pixels.
379#[derive(Clone, Copy, Debug)]
380pub struct DragEvent {
381    /// Gesture phase.
382    pub phase: GesturePhase,
383    /// Current pointer location.
384    pub point: [f64; 2],
385    /// Movement since the preceding event.
386    pub delta: [f64; 2],
387    /// Movement since acquisition.
388    pub offset: [f64; 2],
389    /// Pointer units/second, independent of elastic element displacement.
390    pub velocity: [f64; 2],
391    /// Automatically locked axis, if configured: zero horizontal, one vertical.
392    pub locked_axis: Option<usize>,
393}
394
395/// Native pinch event. Scale is relative to the beginning of this gesture;
396/// translating a normal subtree or scaling an SVG remains an application mapping.
397#[derive(Clone, Copy, Debug)]
398pub struct PinchGesture {
399    /// Gesture phase.
400    pub phase: GesturePhase,
401    /// Native pinch center in window coordinates.
402    pub center: [f64; 2],
403    /// Multiplicative scale accumulated within this gesture.
404    pub scale: f64,
405    /// Native percentage delta for this event.
406    pub delta: f64,
407}