Skip to main content

hyperchad_transformer/layout/
mod.rs

1//! Layout calculation engine for UI containers.
2//!
3//! This module provides layout calculation functionality including flexbox layout, positioning,
4//! and size calculations. It includes utilities for floating-point comparisons, rectangle operations,
5//! and the [`Calc`](crate::layout::Calc) trait for implementing custom layout algorithms. Requires the `layout` feature.
6
7use std::sync::atomic::AtomicU16;
8
9use crate::Container;
10
11/// Layout calculation implementation with the `Calculator` type and layout algorithms.
12pub mod calc;
13/// Font metrics traits and types for text measurement during layout.
14pub mod font;
15
16/// Epsilon value for floating-point comparisons in layout calculations.
17///
18/// Used by float comparison macros to determine equality within a tolerance of 0.001.
19pub static EPSILON: f32 = 0.001;
20static SCROLLBAR_SIZE: AtomicU16 = AtomicU16::new(16);
21
22/// Compares two floats for approximate equality within epsilon tolerance.
23///
24/// Returns `true` if the absolute difference between `$a` and `$b` is less than
25/// [`EPSILON`](crate::layout::EPSILON) (0.001).
26///
27/// # Examples
28///
29/// ```rust
30/// # use hyperchad_transformer::float_eq;
31/// assert!(float_eq!(1.0_f32, 1.0005_f32));
32/// assert!(!float_eq!(1.0_f32, 1.01_f32));
33/// ```
34#[macro_export]
35macro_rules! float_eq {
36    ($a:expr, $b:expr $(,)?) => {{ ($a - $b).abs() < $crate::layout::EPSILON }};
37}
38
39/// Compares if float `$a` is less than `$b` with epsilon tolerance.
40///
41/// Returns `true` if `$a` is less than `$b` by at least [`EPSILON`](crate::layout::EPSILON).
42///
43/// # Examples
44///
45/// ```rust
46/// # use hyperchad_transformer::float_lt;
47/// assert!(float_lt!(1.0, 2.0));
48/// assert!(!float_lt!(2.0, 1.0));
49/// ```
50#[macro_export]
51macro_rules! float_lt {
52    ($a:expr, $b:expr $(,)?) => {{ $a - $b <= -$crate::layout::EPSILON }};
53}
54
55/// Compares if float `$a` is less than or approximately equal to `$b` with epsilon tolerance.
56///
57/// Returns `true` if `$a` is less than `$b` or within [`EPSILON`](crate::layout::EPSILON) of `$b`.
58///
59/// # Examples
60///
61/// ```rust
62/// # use hyperchad_transformer::float_lte;
63/// assert!(float_lte!(1.0, 2.0));
64/// assert!(float_lte!(1.0, 1.0001));
65/// ```
66#[macro_export]
67macro_rules! float_lte {
68    ($a:expr, $b:expr $(,)?) => {{ $a - $b < $crate::layout::EPSILON }};
69}
70
71/// Compares if float `$a` is greater than `$b` with epsilon tolerance.
72///
73/// Returns `true` if `$a` is greater than `$b` by at least [`EPSILON`](crate::layout::EPSILON).
74///
75/// # Examples
76///
77/// ```rust
78/// # use hyperchad_transformer::float_gt;
79/// assert!(float_gt!(2.0, 1.0));
80/// assert!(!float_gt!(1.0, 2.0));
81/// ```
82#[macro_export]
83macro_rules! float_gt {
84    ($a:expr, $b:expr $(,)?) => {{ $a - $b >= $crate::layout::EPSILON }};
85}
86
87/// Compares if float `$a` is greater than or approximately equal to `$b` with epsilon tolerance.
88///
89/// Returns `true` if `$a` is greater than `$b` or within [`EPSILON`](crate::layout::EPSILON) of `$b`.
90///
91/// # Examples
92///
93/// ```rust
94/// # use hyperchad_transformer::float_gte;
95/// assert!(float_gte!(2.0, 1.0));
96/// assert!(float_gte!(1.0, 1.0001));
97/// ```
98#[macro_export]
99macro_rules! float_gte {
100    ($a:expr, $b:expr $(,)?) => {{ $a - $b > -$crate::layout::EPSILON }};
101}
102
103/// Returns the minimum of two float values.
104///
105/// # Examples
106///
107/// ```rust
108/// # use hyperchad_transformer::min_float;
109/// assert_eq!(min_float!(1.0, 2.0), 1.0);
110/// assert_eq!(min_float!(3.0, 1.5), 1.5);
111/// ```
112#[macro_export]
113macro_rules! min_float {
114    ($a:expr, $b:expr $(,)?) => {{ if $a <= $b { $a } else { $b } }};
115}
116
117/// Returns the maximum of two float values.
118///
119/// # Examples
120///
121/// ```rust
122/// # use hyperchad_transformer::max_float;
123/// assert_eq!(max_float!(1.0, 2.0), 2.0);
124/// assert_eq!(max_float!(3.0, 1.5), 3.0);
125/// ```
126#[macro_export]
127macro_rules! max_float {
128    ($a:expr, $b:expr $(,)?) => {{ if $a > $b { $a } else { $b } }};
129}
130
131/// Gets the current scrollbar size in pixels.
132#[must_use]
133pub fn get_scrollbar_size() -> u16 {
134    SCROLLBAR_SIZE.load(std::sync::atomic::Ordering::SeqCst)
135}
136
137/// Sets the scrollbar size in pixels for layout calculations.
138pub fn set_scrollbar_size(size: u16) {
139    SCROLLBAR_SIZE.store(size, std::sync::atomic::Ordering::SeqCst);
140}
141
142/// Trait for types that can perform layout calculations on containers.
143pub trait Calc {
144    /// Performs layout calculation on the given container.
145    ///
146    /// Returns `true` if the layout changed, `false` otherwise.
147    fn calc(&self, container: &mut Container) -> bool;
148}
149
150/// Represents a rectangular region with position and dimensions.
151#[derive(Clone, Copy, Default)]
152pub struct Rect {
153    /// X coordinate.
154    pub x: f32,
155    /// Y coordinate.
156    pub y: f32,
157    /// Width of the rectangle.
158    pub width: f32,
159    /// Height of the rectangle.
160    pub height: f32,
161}
162
163#[allow(clippy::trivially_copy_pass_by_ref)]
164#[inline]
165#[must_use]
166pub(crate) fn order_float(a: &f32, b: &f32) -> std::cmp::Ordering {
167    if a > b {
168        std::cmp::Ordering::Greater
169    } else if a < b {
170        std::cmp::Ordering::Less
171    } else {
172        std::cmp::Ordering::Equal
173    }
174}
175
176/// Increases an optional float value by the given amount.
177///
178/// If the option is `None`, sets it to `value`. Returns the new value.
179pub fn increase_opt(opt: &mut Option<f32>, value: f32) -> f32 {
180    if let Some(existing) = *opt {
181        opt.replace(existing + value);
182        existing + value
183    } else {
184        opt.replace(value);
185        value
186    }
187}
188
189/// Sets an optional value if it differs from the current value.
190///
191/// Returns `Some(value)` if changed, `None` if unchanged.
192pub fn set_value<T: PartialEq + Copy>(opt: &mut Option<T>, value: T) -> Option<T> {
193    if let Some(existing) = *opt {
194        if existing != value {
195            *opt = Some(value);
196            return *opt;
197        }
198    } else {
199        *opt = Some(value);
200        return *opt;
201    }
202
203    None
204}
205
206/// Sets an optional float value if it differs significantly from the current value.
207///
208/// Uses epsilon comparison to avoid float precision issues.
209/// Returns `Some(value)` if changed, `None` if unchanged.
210pub fn set_float(opt: &mut Option<f32>, value: f32) -> Option<f32> {
211    if let Some(existing) = *opt {
212        if !float_eq!(existing, value) {
213            *opt = Some(value);
214            return *opt;
215        }
216    } else {
217        *opt = Some(value);
218        return *opt;
219    }
220
221    None
222}
223
224#[cfg(test)]
225mod tests {
226    use serial_test::serial;
227
228    use super::*;
229
230    #[test_log::test]
231    #[serial(scrollbar_size)]
232    fn get_and_set_scrollbar_size_updates_global_value() {
233        let original = get_scrollbar_size();
234
235        set_scrollbar_size(42);
236        assert_eq!(get_scrollbar_size(), 42);
237
238        set_scrollbar_size(100);
239        assert_eq!(get_scrollbar_size(), 100);
240
241        // Restore original value
242        set_scrollbar_size(original);
243    }
244
245    #[test_log::test]
246    fn order_float_returns_greater_when_a_greater_than_b() {
247        assert_eq!(order_float(&2.0, &1.0), std::cmp::Ordering::Greater);
248        assert_eq!(order_float(&100.5, &100.4), std::cmp::Ordering::Greater);
249    }
250
251    #[test_log::test]
252    fn order_float_returns_less_when_a_less_than_b() {
253        assert_eq!(order_float(&1.0, &2.0), std::cmp::Ordering::Less);
254        assert_eq!(order_float(&-5.0, &0.0), std::cmp::Ordering::Less);
255    }
256
257    #[test_log::test]
258    fn order_float_returns_equal_when_values_equal() {
259        assert_eq!(order_float(&1.0, &1.0), std::cmp::Ordering::Equal);
260        assert_eq!(order_float(&0.0, &0.0), std::cmp::Ordering::Equal);
261    }
262
263    #[test_log::test]
264    fn increase_opt_sets_value_when_none() {
265        let mut opt: Option<f32> = None;
266        let result = increase_opt(&mut opt, 10.0);
267
268        assert!((result - 10.0).abs() < f32::EPSILON);
269        assert!((opt.unwrap() - 10.0).abs() < f32::EPSILON);
270    }
271
272    #[test_log::test]
273    fn increase_opt_adds_to_existing_value() {
274        let mut opt: Option<f32> = Some(5.0);
275        let result = increase_opt(&mut opt, 10.0);
276
277        assert!((result - 15.0).abs() < f32::EPSILON);
278        assert!((opt.unwrap() - 15.0).abs() < f32::EPSILON);
279    }
280
281    #[test_log::test]
282    fn increase_opt_handles_negative_values() {
283        let mut opt: Option<f32> = Some(10.0);
284        let result = increase_opt(&mut opt, -3.0);
285
286        assert!((result - 7.0).abs() < f32::EPSILON);
287        assert!((opt.unwrap() - 7.0).abs() < f32::EPSILON);
288    }
289
290    #[test_log::test]
291    fn increase_opt_accumulates_multiple_calls() {
292        let mut opt: Option<f32> = None;
293
294        increase_opt(&mut opt, 5.0);
295        increase_opt(&mut opt, 3.0);
296        let result = increase_opt(&mut opt, 2.0);
297
298        assert!((result - 10.0).abs() < f32::EPSILON);
299        assert!((opt.unwrap() - 10.0).abs() < f32::EPSILON);
300    }
301
302    #[test_log::test]
303    fn set_value_sets_when_none_and_returns_new_value() {
304        let mut opt: Option<i32> = None;
305        let result = set_value(&mut opt, 42);
306
307        assert_eq!(result, Some(42));
308        assert_eq!(opt, Some(42));
309    }
310
311    #[test_log::test]
312    fn set_value_updates_when_different_value() {
313        let mut opt: Option<i32> = Some(10);
314        let result = set_value(&mut opt, 42);
315
316        assert_eq!(result, Some(42));
317        assert_eq!(opt, Some(42));
318    }
319
320    #[test_log::test]
321    fn set_value_returns_none_when_same_value() {
322        let mut opt: Option<i32> = Some(42);
323        let result = set_value(&mut opt, 42);
324
325        assert_eq!(result, None);
326        assert_eq!(opt, Some(42));
327    }
328
329    #[test_log::test]
330    fn set_float_sets_when_none_and_returns_new_value() {
331        let mut opt: Option<f32> = None;
332        let result = set_float(&mut opt, 7.25);
333
334        assert!(result.is_some());
335        assert!((result.unwrap() - 7.25).abs() < f32::EPSILON);
336        assert!((opt.unwrap() - 7.25).abs() < f32::EPSILON);
337    }
338
339    #[test_log::test]
340    fn set_float_updates_when_significantly_different() {
341        let mut opt: Option<f32> = Some(1.0);
342        let result = set_float(&mut opt, 2.0);
343
344        assert!(result.is_some());
345        assert!((result.unwrap() - 2.0).abs() < f32::EPSILON);
346        assert!((opt.unwrap() - 2.0).abs() < f32::EPSILON);
347    }
348
349    #[test_log::test]
350    fn set_float_returns_none_when_within_epsilon() {
351        let mut opt: Option<f32> = Some(1.0);
352        // Value within epsilon (0.001) should not trigger an update
353        let result = set_float(&mut opt, 1.0005);
354
355        assert!(result.is_none());
356        // Original value should be preserved
357        assert!((opt.unwrap() - 1.0).abs() < f32::EPSILON);
358    }
359
360    #[test_log::test]
361    fn set_float_updates_when_just_outside_epsilon() {
362        let mut opt: Option<f32> = Some(1.0);
363        // Value just outside epsilon should trigger an update
364        let result = set_float(&mut opt, 1.002);
365
366        assert!(result.is_some());
367        assert!((opt.unwrap() - 1.002).abs() < f32::EPSILON);
368    }
369}