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}