matte/
lib.rs

1#![no_std]
2#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/readme.md"))]
3
4mod num;
5pub use num::*;
6
7/// Shortens signature for a mutable frame reference
8macro_rules! child {
9    () => {
10        impl FnMut(&mut Frame<T>)
11    };
12}
13
14pub trait Child<T>: FnMut(&mut Frame<T>) {}
15
16/// A layout frame that manages rectangular areas with margins and scaling.
17/// A frame consists of an outer rectangle, an inner cursor rectangle (available space),
18/// and properties that control how child frames are created and positioned.
19#[derive(Debug, Clone)]
20pub struct Frame<T> {
21    /// The outer rectangle defining the frame boundaries
22    rect: Rect<T>,
23    /// Inner rectangle representing available space
24    cursor: Rect<T>,
25    /// Scaling factor for dimensions
26    scale: f32,
27    /// Margin size between frames
28    margin: T,
29    /// Gap between each child frame
30    gap: T,
31    /// Controls how children rects are culled when they exceed available space
32    pub fitting: Fitting,
33}
34
35/// Represents a generic rectangle with position and dimensions that
36/// implement [Num] trait, i.e. u16, f32, etc.
37///
38/// A rectangle is defined by its top-left corner coordinates (x, y)
39/// and its width and height.
40#[derive(Debug, Clone, Copy)]
41pub struct Rect<T> {
42    /// X-coordinate of the top-left corner
43    pub x: T,
44    /// Y-coordinate of the top-left corner
45    pub y: T,
46    /// Width of the rectangle
47    pub w: T,
48    /// Height of the rectangle
49    pub h: T,
50}
51
52/// Represents the side of a frame where a child frame can be added.
53#[derive(Debug, Clone, Copy, Default)]
54pub enum Edge {
55    #[default]
56    /// Left side of the frame
57    Left,
58    /// Right side of the frame
59    Right,
60    /// Top side of the frame
61    Top,
62    /// Bottom side of the frame
63    Bottom,
64}
65
66/// Represents the alignment of a child frame that is sized (width, height).
67/// Notice that LeftTop is *not* the same as TopLeft! LeftTop means "push_edge from the left,
68/// align to Top" and TopLeft means "push_edge from Top, align to Left". The result may look the same,
69/// but the available space will shrink from the left in the former, from the top in the latter.
70#[derive(Debug, Clone, Copy, Default, PartialEq)]
71pub enum Align {
72    #[default]
73    LeftTop,
74    LeftCenter,
75    LeftBottom,
76    RightTop,
77    RightCenter,
78    RightBottom,
79    TopLeft,
80    TopCenter,
81    TopRight,
82    BottomLeft,
83    BottomCenter,
84    BottomRight,
85    /// Only option that does not shrink the available space (the "cursor" rect).
86    Center,
87}
88
89/// Clipping strategy
90#[derive(Debug, Clone, Copy, PartialEq)]
91pub enum Fitting {
92    /// Allows child frame even if it goes over the available space.
93    /// Also useful for debugging, since Frame is less likely to disappear when space is too small.
94    Relaxed,
95    /// Removes child frames that touch the margin.
96    Aggressive,
97    /// Clamps child frame's edges to available space.
98    Clamp,
99    /// Scales the child frame to fit available space while preserving aspect ratio.
100    Scale,
101}
102
103impl<T> Frame<T>
104where
105    T: Num,
106{
107    /// Creates a new frame with the specified outer rectangle.
108    /// Initializes with default values for scale (1.0) and margin (4 units).
109    pub fn new(rect: Rect<T>) -> Self {
110        let scale = 1.0;
111        let margin = T::four();
112        let cursor = rect_shrink(rect, margin);
113        Self {
114            rect,
115            cursor,
116            margin,
117            gap: margin,
118            scale,
119            fitting: Fitting::Aggressive,
120        }
121    }
122
123    /// The rect that represents this Frame's position and size.
124    /// Does not change when adding child frames.
125    pub fn rect(&self) -> Rect<T> {
126        self.rect
127    }
128
129    /// The available space to add more child frames.
130    /// Shrinks every time a child frame is added.
131    pub fn cursor(&self) -> Rect<T> {
132        self.cursor
133    }
134
135    /// Returns the current margin value.
136    pub fn get_margin(&self) -> T {
137        self.margin
138    }
139
140    /// Sets a new margin value and recalculates the cursor rectangle.
141    pub fn set_margin(&mut self, margin: T) {
142        // Remove old margin
143        self.cursor = rect_expand(self.cursor, self.margin);
144        // Apply new margin
145        self.margin = margin;
146        self.cursor = rect_shrink(self.rect, self.margin);
147    }
148
149    /// Returns the current gap value.
150    pub fn get_gap(&self) -> T {
151        self.gap
152    }
153
154    /// Sets a new gap value.
155    pub fn set_gap(&mut self, gap: T) {
156        self.gap = gap
157    }
158
159    /// Returns the current scale factor.
160    pub fn get_scale(&self) -> f32 {
161        self.scale
162    }
163
164    /// Sets a new scale factor for the frame.
165    pub fn set_scale(&mut self, scale: f32) {
166        self.scale = scale;
167        // self.set_margin(self.margin);
168    }
169
170    /// Calculates the size if you divide the available space's width by "columns",
171    /// taking into account the size of the gaps between each column.
172    pub fn divide_width(&self, columns: u32) -> T {
173        let gaps = self.gap * T::from_f32((columns - 1) as f32 * self.scale);
174        (self.cursor.w - gaps) / T::from_f32(columns as f32)
175    }
176
177    /// Calculates the size if you divide the available space's height by "rows",
178    /// taking into account the size of the gaps between each row.
179    pub fn divide_height(&self, rows: u32) -> T {
180        let gaps = self.gap * T::from_f32((rows - 1) as f32 * self.scale);
181        (self.cursor.h - gaps) / T::from_f32(rows as f32)
182    }
183
184    /// Determines the edge associated with an alignment.
185    fn alignment_to_edge(align: Align) -> Edge {
186        match align {
187            Align::LeftTop | Align::LeftCenter | Align::LeftBottom => Edge::Left,
188            Align::RightTop | Align::RightCenter | Align::RightBottom => Edge::Right,
189            Align::TopLeft | Align::TopCenter | Align::TopRight => Edge::Top,
190            Align::BottomLeft | Align::BottomCenter | Align::BottomRight => Edge::Bottom,
191            Align::Center => Edge::Left, // Center uses left as base for positioning
192        }
193    }
194
195    /// Converts an edge to its default alignment.
196    fn edge_to_alignment(edge: Edge) -> Align {
197        match edge {
198            Edge::Left => Align::LeftTop,
199            Edge::Right => Align::RightTop,
200            Edge::Top => Align::TopLeft,
201            Edge::Bottom => Align::BottomLeft,
202        }
203    }
204
205    /// Calculates offsets based on alignment for positioned frames.
206    fn calculate_align_offsets(&self, align: Align, w: T, h: T) -> (T, T) {
207        let (offset_x, offset_y) = match align {
208            // Left edge alignments
209            Align::LeftTop => (T::zero(), T::zero()),
210            Align::LeftCenter => (T::zero(), (self.cursor.h - h) / T::two()),
211            Align::LeftBottom => (T::zero(), self.cursor.h.saturating_sub(h)),
212
213            // Right edge alignments
214            Align::RightTop => (T::zero(), T::zero()),
215            Align::RightCenter => (T::zero(), (self.cursor.h - h) / T::two()),
216            Align::RightBottom => (T::zero(), self.cursor.h.saturating_sub(h)),
217
218            // Top edge alignments
219            Align::TopLeft => (T::zero(), T::zero()),
220            Align::TopCenter => ((self.cursor.w - w) / T::two(), T::zero()),
221            Align::TopRight => (self.cursor.w.saturating_sub(w), T::zero()),
222
223            // Bottom edge alignments
224            Align::BottomLeft => (T::zero(), T::zero()),
225            Align::BottomCenter => ((self.cursor.w - w) / T::two(), T::zero()),
226            Align::BottomRight => (self.cursor.w.saturating_sub(w), T::zero()),
227
228            // Center alignment
229            Align::Center => (
230                (self.cursor.w - w) / T::two(),
231                (self.cursor.h - h) / T::two(),
232            ),
233        };
234
235        // Ensure offsets are non-negative
236        let x = offset_x.get_max(T::zero());
237        let y = offset_y.get_max(T::zero());
238
239        (x, y)
240    }
241
242    /// Calculates the scale needed to fit a rectangle of given dimensions
243    /// into the available space, preserving aspect ratio.
244    /// Takes into account the offsets where the rectangle will be placed.
245    fn calculate_fit_scale(&self, w: T, h: T, offset_x: T, offset_y: T) -> f32 {
246        match self.fitting {
247            Fitting::Relaxed | Fitting::Aggressive | Fitting::Clamp => self.scale,
248            Fitting::Scale => {
249                let original_w = w.to_f32();
250                let original_h = h.to_f32();
251
252                if original_w <= 0.0 || original_h <= 0.0 {
253                    return self.scale;
254                }
255
256                // Calculate available space considering offsets
257                let available_w = self.cursor.w.to_f32() - offset_x.to_f32();
258                let available_h = self.cursor.h.to_f32() - offset_y.to_f32();
259
260                if available_w <= 0.0 || available_h <= 0.0 {
261                    return self.scale;
262                }
263
264                // Calculate scale ratios for width and height
265                let scale_w = available_w / original_w;
266                let scale_h = available_h / original_h;
267
268                // Use the smaller ratio to maintain aspect ratio
269                let fit_scale = scale_w.min(scale_h);
270
271                // Apply base scale but cap it to fit in available space
272                if self.scale >= 1.0 {
273                    self.scale.min(fit_scale)
274                } else {
275                    self.scale.min(fit_scale) // May need further investigation
276                }
277            }
278        }
279    }
280
281    /// Attempts to add a frame with the specified size (w,h).
282    /// Does not modify the available space if Align is Center.
283    /// # Parameters
284    /// * `align` - Alignment that determines positioning and cursor updating
285    /// * `w` - Width of the new frame
286    /// * `h` - Height of the new frame
287    /// * `func` - Closure to execute with the new child frame
288    #[inline(always)]
289    pub fn push_size(&mut self, align: Align, w: T, h: T, func: child!()) {
290        let (offset_x, offset_y) = self.calculate_align_offsets(align, w, h);
291        let edge = Self::alignment_to_edge(align);
292
293        // Calculate actual scale for Fitting::Scale
294        let actual_scale = self.calculate_fit_scale(w, h, offset_x, offset_y);
295
296        // Final offsets with actual size
297        let (offset_x, offset_y) = self.calculate_align_offsets(
298            align,
299            w * T::from_f32(actual_scale),
300            h * T::from_f32(actual_scale),
301        );
302
303        let update_cursor = align != Align::Center;
304
305        self.add_scope(
306            edge,
307            offset_x,
308            offset_y,
309            w,
310            h,
311            actual_scale,
312            update_cursor,
313            self.fitting,
314            func,
315        );
316    }
317
318    /// Adds a new frame on the specified edge with specified length.
319    /// # Parameters
320    /// * `edge` - Which edge to add the child frame to
321    /// * `len` - Length of the new frame
322    /// * `func` - Closure to execute with the new child frame
323    #[inline(always)]
324    pub fn push_edge(&mut self, edge: Edge, len: T, func: child!()) {
325        // Default width and height based on the edge
326        let is_horizontal = matches!(edge, Edge::Left | Edge::Right);
327        let (w, h) = if is_horizontal {
328            (len, T::from_f32(self.cursor.h.to_f32() / self.scale))
329        } else {
330            (T::from_f32(self.cursor.w.to_f32() / self.scale), len)
331        };
332
333        let align = Self::edge_to_alignment(edge);
334        let (offset_x, offset_y) = self.calculate_align_offsets(align, w, h);
335        let actual_scale = self.calculate_fit_scale(w, h, offset_x, offset_y);
336        let update_cursor = align != Align::Center;
337
338        self.add_scope(
339            edge,
340            offset_x,
341            offset_y,
342            w,
343            h,
344            actual_scale,
345            update_cursor,
346            self.fitting,
347            func,
348        );
349    }
350
351    /// Fills the entire available cursor area with a new Frame.
352    /// # Parameters
353    /// * `func` - Closure to execute with the new child frame
354    pub fn fill(&mut self, func: child!()) {
355        self.add_scope(
356            Edge::Top,
357            T::zero(),
358            T::zero(),
359            self.cursor.w,
360            self.cursor.h,
361            1.0,
362            true,
363            self.fitting,
364            func,
365        );
366    }
367
368    /// Allows arbitrary placement of the new frame in relation to the current frame.
369    /// Does not modify the available space if Align is Center.
370    /// Scales the frame if necessary to fit.
371    /// # Parameters
372    /// * `align` - Alignment that determines cursor updating
373    /// * `x` - X position of the new frame in relation to this frame
374    /// * `y` - Y position of the new frame in relation to this frame
375    /// * `w` - Width of the new frame
376    /// * `h` - Height of the new frame
377    /// * `func` - Closure to execute with the new child frame
378    pub fn place(&mut self, align: Align, x: T, y: T, w: T, h: T, func: child!()) {
379        let edge = Self::alignment_to_edge(align);
380        let update_cursor = align != Align::Center;
381
382        // Calculate actual scale and apply it to dimensions, taking offsets into account
383        let actual_scale = self.calculate_fit_scale(w, h, x, y);
384
385        // Ensures "1.0" is used as scale since we've already applied scaling to dimensions
386        self.add_scope(
387            edge,
388            x,
389            y,
390            w,
391            h,
392            actual_scale,
393            update_cursor,
394            self.fitting,
395            func,
396        );
397    }
398
399    /// Internal multi-purpose function called by the mode-specialized public functions.
400    fn add_scope(
401        &mut self,
402        edge: Edge,
403        extra_x: T,
404        extra_y: T,
405        w: T,
406        h: T,
407        scale: f32,
408        update_cursor: bool,
409        fitting: Fitting,
410        mut func: child!(),
411    ) {
412        let scaled_w = T::from_f32(w.to_f32() * scale);
413        let scaled_h = T::from_f32(h.to_f32() * scale);
414        let margin = T::from_f32(self.gap.to_f32() * self.scale);
415        let gap = T::from_f32(self.gap.to_f32() * self.scale);
416
417        if scaled_w < T::one() || scaled_h < T::one() {
418            return;
419        }
420
421        // Calculate the child rectangle based on the edge
422        let mut child_rect = match edge {
423            Edge::Left => {
424                if self.cursor.x > self.rect.x + self.rect.w {
425                    return;
426                }
427                Rect {
428                    x: self.cursor.x + extra_x,
429                    y: self.cursor.y + extra_y,
430                    w: scaled_w,
431                    h: scaled_h,
432                }
433            }
434            Edge::Right => Rect {
435                x: (self.cursor.x + self.cursor.w).saturating_sub(scaled_w) - extra_x,
436                y: self.cursor.y + extra_y,
437                w: scaled_w,
438                h: scaled_h,
439            },
440            Edge::Top => {
441                if self.cursor.y > self.rect.y + self.rect.h {
442                    return;
443                }
444                Rect {
445                    x: self.cursor.x + extra_x,
446                    y: self.cursor.y + extra_y,
447                    w: scaled_w,
448                    h: scaled_h,
449                }
450            }
451            Edge::Bottom => Rect {
452                x: self.cursor.x + extra_x,
453                y: (self.cursor.y + self.cursor.h).saturating_sub(scaled_h) - extra_y,
454                w: scaled_w,
455                h: scaled_h,
456            },
457        };
458
459        if child_rect.x > self.cursor.x + self.cursor.w - self.margin {
460            return;
461        }
462
463        if child_rect.y > self.cursor.y + self.cursor.h - self.margin {
464            return;
465        }
466
467        match fitting {
468            Fitting::Relaxed => {}
469            Fitting::Aggressive => {
470                if (child_rect.x + child_rect.w).round_down()
471                    > (self.cursor.x + self.cursor.w).round_up()
472                {
473                    return;
474                }
475                if (child_rect.y + child_rect.h).round_down()
476                    > (self.cursor.y + self.cursor.h).round_up()
477                {
478                    return;
479                }
480            }
481            Fitting::Clamp => {
482                // Clamp to ensure the rect stays within cursor boundaries
483                // Clamp x position
484                if child_rect.x < self.cursor.x {
485                    let diff = self.cursor.x - child_rect.x;
486                    child_rect.x = self.cursor.x;
487                    child_rect.w = child_rect.w.saturating_sub(diff);
488                }
489
490                // Clamp y position
491                if child_rect.y < self.cursor.y {
492                    let diff = self.cursor.y - child_rect.y;
493                    child_rect.y = self.cursor.y;
494                    child_rect.h = child_rect.h.saturating_sub(diff);
495                }
496
497                // Clamp width
498                if child_rect.x + child_rect.w > self.cursor.x + self.cursor.w {
499                    child_rect.w = self.cursor.x + self.cursor.w - child_rect.x;
500                }
501
502                // Clamp height
503                if child_rect.y + child_rect.h > self.cursor.y + self.cursor.h {
504                    child_rect.h = self.cursor.y + self.cursor.h - child_rect.y;
505                }
506            }
507            Fitting::Scale => {
508                // // The scaling is now handled prior to this function in the calling methods
509                // // We still need to check if the frame is within bounds
510                // if child_rect.x < self.cursor.x
511                //    || child_rect.y < self.cursor.y
512                //    || child_rect.x + child_rect.w > self.cursor.x + self.cursor.w
513                //    || child_rect.y + child_rect.h > self.cursor.y + self.cursor.h {
514                //     if !matches!(edge, Edge::Left | Edge::Top) {
515                //         // Readjust position for right and bottom edges since they're calculated with subtraction
516                //         if matches!(edge, Edge::Right) {
517                //             child_rect.x = (self.cursor.x + self.cursor.w).saturating_sub(child_rect.w) - extra_x;
518                //         }
519                //         if matches!(edge, Edge::Bottom) {
520                //             child_rect.y = (self.cursor.y + self.cursor.h).saturating_sub(child_rect.h) - extra_y;
521                //         }
522                //     }
523                // }
524            }
525        }
526
527        if child_rect.w < T::one() || child_rect.h < T::one() {
528            return;
529        }
530
531        // Update parent cursor
532        if update_cursor {
533            match edge {
534                Edge::Left => {
535                    // Add extra_x to the cursor movement
536                    self.cursor.x += scaled_w + gap + extra_x;
537                    self.cursor.w = self.cursor.w.saturating_sub(scaled_w + gap + extra_x);
538                }
539                Edge::Right => {
540                    // Subtract extra_x in width reduction
541                    self.cursor.w = self.cursor.w.saturating_sub(scaled_w + gap + extra_x);
542                }
543                Edge::Top => {
544                    // Add extra_y to the cursor movement
545                    self.cursor.y += scaled_h + gap + extra_y;
546                    self.cursor.h = self.cursor.h.saturating_sub(scaled_h + gap + extra_y);
547                }
548                Edge::Bottom => {
549                    // Subtract extra_y in height reduction
550                    self.cursor.h = self.cursor.h.saturating_sub(scaled_h + gap + extra_y);
551                }
552            }
553        }
554
555        // Call the function with the new frame
556        func(&mut Frame {
557            cursor: rect_shrink(child_rect, margin),
558            rect: child_rect,
559            margin: self.margin,
560            gap: self.gap,
561            scale: self.scale,
562            fitting,
563        })
564    }
565}
566
567/// Shrinks a rectangle by applying a margin on all edges.
568#[inline(always)]
569fn rect_shrink<T>(rect: Rect<T>, margin: T) -> Rect<T>
570where
571    T: Num,
572{
573    Rect {
574        x: rect.x + margin,
575        y: rect.y + margin,
576        w: rect.w.saturating_sub(margin * T::two()),
577        h: rect.h.saturating_sub(margin * T::two()),
578    }
579}
580
581/// Expands a rectangle by removing a margin from all sides.
582#[inline(always)]
583fn rect_expand<T>(rect: Rect<T>, margin: T) -> Rect<T>
584where
585    T: Num,
586{
587    Rect {
588        x: rect.x - margin,
589        y: rect.y - margin,
590        w: rect.w.saturating_add(margin * T::two()),
591        h: rect.h.saturating_add(margin * T::two()),
592    }
593}