Skip to main content

tessera_ui/
geometry.rs

1//! Fractional geometry and uniform coordinate transformations.
2//!
3//! ## Usage
4//!
5//! Map local control geometry into window coordinates without changing layout
6//! units.
7
8use crate::{Px, PxPosition, PxRect, PxSize};
9
10/// A rectangle in fractional physical pixels.
11#[derive(Clone, Copy, Debug, Default, PartialEq)]
12pub struct FloatRect {
13    /// Left edge.
14    pub x: f32,
15    /// Top edge.
16    pub y: f32,
17    /// Horizontal extent.
18    pub width: f32,
19    /// Vertical extent.
20    pub height: f32,
21}
22
23impl FloatRect {
24    /// An empty rectangle at the origin.
25    pub const ZERO: Self = Self {
26        x: 0.0,
27        y: 0.0,
28        width: 0.0,
29        height: 0.0,
30    };
31
32    /// Creates a rectangle from a fractional origin and integer layout size.
33    pub fn from_position_size(position: PxPosition, size: PxSize) -> Self {
34        Self {
35            x: position.x,
36            y: position.y,
37            width: size.width.0 as f32,
38            height: size.height.0 as f32,
39        }
40    }
41
42    /// Returns the smallest integer rectangle containing this rectangle.
43    pub fn outward(self) -> PxRect {
44        let x = self.x.floor() as i32;
45        let y = self.y.floor() as i32;
46        let right = (self.x + self.width).ceil() as i32;
47        let bottom = (self.y + self.height).ceil() as i32;
48        PxRect {
49            x: Px(x),
50            y: Px(y),
51            width: Px(right.saturating_sub(x).max(0)),
52            height: Px(bottom.saturating_sub(y).max(0)),
53        }
54    }
55
56    /// Rounds an origin and layout extents to the nearest integer pixels.
57    pub fn rounded(self) -> PxRect {
58        PxRect {
59            x: Px(self.x.round() as i32),
60            y: Px(self.y.round() as i32),
61            width: Px(self.width.round() as i32),
62            height: Px(self.height.round() as i32),
63        }
64    }
65
66    /// Returns whether a point is inside the rectangle, excluding its far
67    /// edges.
68    pub fn contains(self, point: PxPosition) -> bool {
69        point.x >= self.x
70            && point.y >= self.y
71            && point.x < self.x + self.width
72            && point.y < self.y + self.height
73    }
74
75    /// Returns the positive-area intersection of two rectangles.
76    pub fn intersection(self, other: Self) -> Option<Self> {
77        let x = self.x.max(other.x);
78        let y = self.y.max(other.y);
79        let right = (self.x + self.width).min(other.x + other.width);
80        let bottom = (self.y + self.height).min(other.y + other.height);
81        (right > x && bottom > y).then_some(Self {
82            x,
83            y,
84            width: right - x,
85            height: bottom - y,
86        })
87    }
88}
89
90impl From<PxRect> for FloatRect {
91    fn from(rect: PxRect) -> Self {
92        Self {
93            x: rect.x.0 as f32,
94            y: rect.y.0 as f32,
95            width: rect.width.0 as f32,
96            height: rect.height.0 as f32,
97        }
98    }
99}
100
101/// A positive uniform scale followed by a translation in physical pixels.
102#[derive(Clone, Copy, Debug, PartialEq)]
103pub struct LocalToWindow {
104    scale: f32,
105    translation: PxPosition,
106}
107
108impl Default for LocalToWindow {
109    fn default() -> Self {
110        Self::IDENTITY
111    }
112}
113
114impl LocalToWindow {
115    /// The identity coordinate transformation.
116    pub const IDENTITY: Self = Self {
117        scale: 1.0,
118        translation: PxPosition::ZERO,
119    };
120
121    /// Creates a finite, invertible uniform coordinate transformation.
122    pub fn new(scale: f32, translation: PxPosition) -> Option<Self> {
123        (scale.is_finite()
124            && scale > 0.0
125            && scale.recip().is_finite()
126            && translation.x.is_finite()
127            && translation.y.is_finite())
128        .then_some(Self { scale, translation })
129    }
130
131    /// Returns the uniform scale.
132    pub const fn scale(self) -> f32 {
133        self.scale
134    }
135
136    /// Returns the mapped local origin.
137    pub const fn translation(self) -> PxPosition {
138        self.translation
139    }
140
141    /// Maps a local point into window coordinates.
142    pub fn map_point(self, point: PxPosition) -> PxPosition {
143        self.translation + self.map_vector(point)
144    }
145
146    /// Maps a local displacement into window coordinates.
147    pub fn map_vector(self, vector: PxPosition) -> PxPosition {
148        PxPosition::from_f32_xy(vector.x * self.scale, vector.y * self.scale)
149    }
150
151    /// Maps a window point into local coordinates.
152    pub fn inverse_point(self, point: PxPosition) -> PxPosition {
153        self.inverse_vector(point - self.translation)
154    }
155
156    /// Maps a window displacement into local coordinates.
157    pub fn inverse_vector(self, vector: PxPosition) -> PxPosition {
158        PxPosition::from_f32_xy(vector.x / self.scale, vector.y / self.scale)
159    }
160
161    /// Composes a child transformation followed by this transformation.
162    pub fn compose(self, child: Self) -> Option<Self> {
163        Self::new(self.scale * child.scale, self.map_point(child.translation))
164    }
165
166    /// Moves the local origin without changing the scale.
167    pub fn with_local_origin(self, origin: PxPosition) -> Self {
168        Self {
169            scale: self.scale,
170            translation: self.map_point(origin),
171        }
172    }
173
174    /// Maps an integer layout size into fractional window extents.
175    pub fn map_size(self, size: PxSize) -> [f32; 2] {
176        [
177            size.width.0 as f32 * self.scale,
178            size.height.0 as f32 * self.scale,
179        ]
180    }
181
182    /// Maps a local rectangle into window coordinates.
183    pub fn map_rect(self, rect: FloatRect) -> FloatRect {
184        let origin = self.map_point(PxPosition::from_f32_xy(rect.x, rect.y));
185        FloatRect {
186            x: origin.x,
187            y: origin.y,
188            width: rect.width * self.scale,
189            height: rect.height * self.scale,
190        }
191    }
192
193    /// Maps a window rectangle into local coordinates.
194    pub fn inverse_rect(self, rect: FloatRect) -> FloatRect {
195        let origin = self.inverse_point(PxPosition::from_f32_xy(rect.x, rect.y));
196        FloatRect {
197            x: origin.x,
198            y: origin.y,
199            width: rect.width / self.scale,
200            height: rect.height / self.scale,
201        }
202    }
203}
204
205/// Local layout dimensions and their exact placement in the window.
206#[derive(Clone, Copy, Debug, Default, PartialEq)]
207pub struct LayoutCoordinates {
208    /// Unscaled physical layout dimensions.
209    pub local_size: PxSize,
210    /// Coordinate transformation for this layout node.
211    pub local_to_window: LocalToWindow,
212}
213
214impl LayoutCoordinates {
215    /// Returns the exact bounds of this layout node in the window.
216    pub fn window_bounds(self) -> FloatRect {
217        self.local_to_window.map_rect(FloatRect::from_position_size(
218            PxPosition::ZERO,
219            self.local_size,
220        ))
221    }
222}
223
224#[derive(Clone, Copy, PartialEq)]
225struct ScaleChildren(f32);
226
227impl crate::BuildModifierNode for ScaleChildren {
228    fn apply(&self, runtime: &mut crate::runtime::TesseraRuntime) {
229        runtime.set_current_children_scale(self.0);
230    }
231}
232
233#[derive(Clone, Copy, PartialEq)]
234struct ScaleLayout(f32);
235
236impl crate::LayoutModifierNode for ScaleLayout {
237    fn measure(
238        &self,
239        scope: &mut crate::layout::MeasureScope<'_>,
240    ) -> Result<crate::LayoutResult, crate::MeasurementError> {
241        let children = scope.children();
242        if children.is_empty() {
243            return Ok(crate::LayoutResult::new(crate::ComputedData {
244                width: scope.parent_constraint().width().min,
245                height: scope.parent_constraint().height().min,
246            }));
247        }
248        if children.len() != 1 {
249            return Err(crate::MeasurementError::MeasureFnFailed(
250                "A scale boundary requires exactly one layout child".to_owned(),
251            ));
252        }
253        let parent = *scope.parent_constraint().as_ref();
254        let inverse_axis = |axis: crate::AxisConstraint| {
255            let max = axis
256                .max
257                .map(|value| Px((value.0 as f32 / self.0).floor() as i32));
258            let min = Px((axis.min.0 as f32 / self.0).ceil() as i32);
259            crate::AxisConstraint::new(max.map_or(min, |max| min.min(max)), max)
260        };
261        let constraint = crate::Constraint {
262            width: inverse_axis(parent.width),
263            height: inverse_axis(parent.height),
264        };
265        let child = children[0].measure(scope, &constraint)?;
266        let size = crate::ComputedData {
267            width: parent
268                .width
269                .clamp(Px((child.width.0 as f32 * self.0).ceil() as i32)),
270            height: parent
271                .height
272                .clamp(Px((child.height.0 as f32 * self.0).ceil() as i32)),
273        };
274        let mut result = crate::LayoutResult::new(size);
275        result.place_child(child, PxPosition::ZERO);
276        Ok(result)
277    }
278}
279
280/// # scaled_box
281///
282/// Scale a single native child uniformly for responsive illustrations and
283/// previews.
284///
285/// ## Usage
286///
287/// Fit an interactive control or fixed-size scene into a smaller display
288/// region.
289///
290/// ## Parameters
291///
292/// - `scale` — finite, positive, invertible scale relative to the child's
293///   layout units.
294/// - `child` — optional content producing one direct layout child; empty when
295///   omitted.
296///
297/// ## Examples
298///
299/// ```
300/// use tessera_ui::{LocalToWindow, Px, PxPosition, PxSize, layout::layout, scaled_box};
301///
302/// #[tessera_ui::tessera]
303/// fn preview() {
304///     scaled_box(0.625).child(|| {
305///         layout().child(|| {});
306///     });
307/// }
308///
309/// let transform = LocalToWindow::new(0.625, PxPosition::ZERO).unwrap();
310/// assert_eq!(
311///     transform.map_size(PxSize::new(Px(240), Px(56))),
312///     [150.0, 35.0]
313/// );
314/// ```
315#[crate::tessera(crate)]
316pub fn scaled_box(scale: f32, child: Option<crate::RenderSlot>) {
317    let transform = LocalToWindow::new(scale, PxPosition::ZERO)
318        .expect("Scale must be finite, positive, and invertible");
319    let child = child.unwrap_or_else(crate::RenderSlot::empty);
320    crate::layout::layout()
321        .policy(ScaleLayout(transform.scale()))
322        .modifier(crate::Modifier::new().push_build(ScaleChildren(transform.scale())))
323        .child(move || child.render());
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329
330    #[test]
331    fn fractional_scale_preserves_exact_control_geometry_and_outward_clip() {
332        let _runtime_session = crate::RuntimeSession::new();
333        let _runtime_scope = _runtime_session.enter();
334        let transform = LocalToWindow::new(0.625, PxPosition::from_f32_xy(10.25, 20.5)).unwrap();
335        let bounds = transform.map_rect(FloatRect::from_position_size(
336            PxPosition::ZERO,
337            PxSize::new(Px(240), Px(56)),
338        ));
339        assert_eq!(
340            bounds,
341            FloatRect {
342                x: 10.25,
343                y: 20.5,
344                width: 150.0,
345                height: 35.0
346            }
347        );
348        assert_eq!(
349            bounds.outward(),
350            PxRect {
351                x: Px(10),
352                y: Px(20),
353                width: Px(151),
354                height: Px(36)
355            }
356        );
357        assert_eq!(
358            transform.map_point(PxPosition::from_f32_xy(52.0, 0.0)).x,
359            42.75
360        );
361        assert_eq!(
362            transform.inverse_point(PxPosition::from_f32_xy(40.25, 35.5)),
363            PxPosition::from_f32_xy(48.0, 24.0)
364        );
365    }
366
367    #[test]
368    fn nested_uniform_transforms_compose_once_and_reject_noninvertible_values() {
369        let _runtime_session = crate::RuntimeSession::new();
370        let _runtime_scope = _runtime_session.enter();
371        let parent = LocalToWindow::new(0.5, PxPosition::from_f32_xy(10.25, 20.5)).unwrap();
372        let child = LocalToWindow::new(1.25, PxPosition::from_f32_xy(8.0, 12.0)).unwrap();
373        let combined = parent.compose(child).unwrap();
374        assert_eq!(
375            combined.map_point(PxPosition::from_f32_xy(16.0, 24.0)),
376            PxPosition::from_f32_xy(24.25, 41.5)
377        );
378        for scale in [0.0, -1.0, f32::NAN, f32::INFINITY, f32::from_bits(1)] {
379            assert!(LocalToWindow::new(scale, PxPosition::ZERO).is_none());
380        }
381    }
382}
383
384#[cfg(all(test, feature = "testing"))]
385#[path = "geometry_tests.rs"]
386mod layout_tests;