1use crate::{Px, PxPosition, PxRect, PxSize};
9
10#[derive(Clone, Copy, Debug, Default, PartialEq)]
12pub struct FloatRect {
13 pub x: f32,
15 pub y: f32,
17 pub width: f32,
19 pub height: f32,
21}
22
23impl FloatRect {
24 pub const ZERO: Self = Self {
26 x: 0.0,
27 y: 0.0,
28 width: 0.0,
29 height: 0.0,
30 };
31
32 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 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 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 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 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#[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 pub const IDENTITY: Self = Self {
117 scale: 1.0,
118 translation: PxPosition::ZERO,
119 };
120
121 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 pub const fn scale(self) -> f32 {
133 self.scale
134 }
135
136 pub const fn translation(self) -> PxPosition {
138 self.translation
139 }
140
141 pub fn map_point(self, point: PxPosition) -> PxPosition {
143 self.translation + self.map_vector(point)
144 }
145
146 pub fn map_vector(self, vector: PxPosition) -> PxPosition {
148 PxPosition::from_f32_xy(vector.x * self.scale, vector.y * self.scale)
149 }
150
151 pub fn inverse_point(self, point: PxPosition) -> PxPosition {
153 self.inverse_vector(point - self.translation)
154 }
155
156 pub fn inverse_vector(self, vector: PxPosition) -> PxPosition {
158 PxPosition::from_f32_xy(vector.x / self.scale, vector.y / self.scale)
159 }
160
161 pub fn compose(self, child: Self) -> Option<Self> {
163 Self::new(self.scale * child.scale, self.map_point(child.translation))
164 }
165
166 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 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 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 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#[derive(Clone, Copy, Debug, Default, PartialEq)]
207pub struct LayoutCoordinates {
208 pub local_size: PxSize,
210 pub local_to_window: LocalToWindow,
212}
213
214impl LayoutCoordinates {
215 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#[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;