Skip to main content

agg_gui/
layout_props.rs

1//! Layout property types: [`Insets`], [`HAnchor`], [`VAnchor`], [`WidgetBase`].
2//!
3//! These types mirror the C# agg-sharp `BorderDouble`, `HAnchor`, `VAnchor`,
4//! and the per-widget layout fields that every `GuiWidget` carried.
5//!
6//! # Design
7//!
8//! Every concrete widget embeds a [`WidgetBase`] and delegates the five
9//! layout-property getters on the [`Widget`](crate::widget::Widget) trait to
10//! it.  The parent layout container reads those getters when placing children.
11//!
12//! All values are stored in **logical (device-independent) units**.
13//! [`WidgetBase::scaled_margin`] multiplies by the global
14//! [`device_scale`](crate::device_scale::device_scale) factor to produce
15//! physical pixel values for use inside layout algorithms.
16//!
17//! # Margin vs padding
18//!
19//! - **Margin** lives on the child and is read by the parent during layout.
20//!   It is space *outside* the widget's bounds.
21//! - **Padding** is the parent container's internal inset — space between its
22//!   own border and its children.  Containers store padding directly (e.g.
23//!   `FlexColumn::inner_padding`); individual leaf widgets do not have padding.
24//!
25//! # Margin semantics
26//!
27//! Margins are **additive**, not collapsed.  When child A has
28//! `margin.bottom = 4` and child B has `margin.top = 6`, the gap between them
29//! is `gap + 4 + 6 = 10 + gap`, not `max(4, 6) = 6`.  This matches the
30//! original C# agg-sharp behaviour.
31
32use crate::geometry::Size;
33
34// ---------------------------------------------------------------------------
35// Insets
36// ---------------------------------------------------------------------------
37
38/// Per-side inset values (logical units).
39///
40/// Used for both widget **margin** (space outside the widget) and container
41/// **padding** (space inside the container around its children).
42#[derive(Copy, Clone, Debug, PartialEq, Default)]
43pub struct Insets {
44    pub left: f64,
45    pub right: f64,
46    pub top: f64,
47    pub bottom: f64,
48}
49
50impl Insets {
51    /// All sides zero.
52    pub const ZERO: Self = Self {
53        left: 0.0,
54        right: 0.0,
55        top: 0.0,
56        bottom: 0.0,
57    };
58
59    /// All four sides the same value.
60    pub fn all(v: f64) -> Self {
61        Self {
62            left: v,
63            right: v,
64            top: v,
65            bottom: v,
66        }
67    }
68
69    /// Horizontal sides (`left` / `right`) = `h`, vertical (`top` / `bottom`) = `v`.
70    pub fn symmetric(h: f64, v: f64) -> Self {
71        Self {
72            left: h,
73            right: h,
74            top: v,
75            bottom: v,
76        }
77    }
78
79    /// Explicit per-side constructor.
80    pub fn from_sides(left: f64, right: f64, top: f64, bottom: f64) -> Self {
81        Self {
82            left,
83            right,
84            top,
85            bottom,
86        }
87    }
88
89    /// Sum of `left + right`.
90    #[inline]
91    pub fn horizontal(&self) -> f64 {
92        self.left + self.right
93    }
94
95    /// Sum of `top + bottom`.
96    #[inline]
97    pub fn vertical(&self) -> f64 {
98        self.top + self.bottom
99    }
100
101    /// Return a new `Insets` with all sides multiplied by `factor`.
102    #[inline]
103    pub fn scale(self, factor: f64) -> Self {
104        Self {
105            left: self.left * factor,
106            right: self.right * factor,
107            top: self.top * factor,
108            bottom: self.bottom * factor,
109        }
110    }
111}
112
113// ---------------------------------------------------------------------------
114// HAnchor
115// ---------------------------------------------------------------------------
116
117/// Horizontal anchor flags — how a widget sizes and positions itself
118/// horizontally within the slot assigned by its parent.
119///
120/// | Constant | Meaning |
121/// |---|---|
122/// | `ABSOLUTE` | No automatic sizing or positioning (manual bounds). |
123/// | `LEFT` | Align to the left edge of the slot (respecting margin). |
124/// | `CENTER` | Center horizontally in the slot (respecting margin). |
125/// | `RIGHT` | Align to the right edge of the slot (respecting margin). |
126/// | `FIT` | Width encloses natural content (default). |
127/// | `STRETCH` | Fill the slot width (`LEFT \| RIGHT`). |
128/// | `MAX_FIT_OR_STRETCH` | Take the larger of Fit or Stretch. |
129/// | `MIN_FIT_OR_STRETCH` | Take the smaller of Fit or Stretch. |
130///
131/// At most one of `LEFT`, `CENTER`, `RIGHT` may be set for position anchoring;
132/// combining `LEFT | RIGHT` means "stretch", not "anchor to both edges".
133#[derive(Copy, Clone, Debug, PartialEq, Eq)]
134pub struct HAnchor(u8);
135
136impl HAnchor {
137    pub const ABSOLUTE: Self = HAnchor(0);
138    pub const LEFT: Self = HAnchor(1);
139    pub const CENTER: Self = HAnchor(2);
140    pub const RIGHT: Self = HAnchor(4);
141    /// Width fits natural content size (default).
142    pub const FIT: Self = HAnchor(8);
143    /// Fill parent slot width (`LEFT | RIGHT`).
144    pub const STRETCH: Self = HAnchor(5); // 1 | 4
145    /// Take the larger of Fit or Stretch.
146    pub const MAX_FIT_OR_STRETCH: Self = HAnchor(13); // 8 | 5
147    /// Take the smaller of Fit or Stretch.
148    pub const MIN_FIT_OR_STRETCH: Self = HAnchor(16);
149
150    /// Returns `true` if all bits in `flags` are set in `self`.
151    #[inline]
152    pub fn contains(self, flags: Self) -> bool {
153        flags.0 != 0 && (self.0 & flags.0) == flags.0
154    }
155
156    /// Returns `true` if this anchor causes horizontal stretching
157    /// (both LEFT and RIGHT are set, or MIN/MAX_FIT_OR_STRETCH resolves to stretch).
158    #[inline]
159    pub fn is_stretch(self) -> bool {
160        self.contains(Self::LEFT) && self.contains(Self::RIGHT)
161    }
162}
163
164impl Default for HAnchor {
165    /// Default is [`FIT`](HAnchor::FIT): take natural content width.
166    fn default() -> Self {
167        Self::FIT
168    }
169}
170
171impl std::ops::BitOr for HAnchor {
172    type Output = Self;
173    fn bitor(self, rhs: Self) -> Self {
174        HAnchor(self.0 | rhs.0)
175    }
176}
177
178impl std::ops::BitAnd for HAnchor {
179    type Output = Self;
180    fn bitand(self, rhs: Self) -> Self {
181        HAnchor(self.0 & rhs.0)
182    }
183}
184
185// ---------------------------------------------------------------------------
186// VAnchor
187// ---------------------------------------------------------------------------
188
189/// Vertical anchor flags — how a widget sizes and positions itself vertically
190/// within the slot assigned by its parent.
191///
192/// Mirrors [`HAnchor`] with `BOTTOM` / `TOP` instead of `LEFT` / `RIGHT`.
193/// Y-up convention: `BOTTOM` is the visually lower edge (small Y), `TOP` is
194/// the visually upper edge (large Y).
195#[derive(Copy, Clone, Debug, PartialEq, Eq)]
196pub struct VAnchor(u8);
197
198impl VAnchor {
199    pub const ABSOLUTE: Self = VAnchor(0);
200    pub const BOTTOM: Self = VAnchor(1);
201    pub const CENTER: Self = VAnchor(2);
202    pub const TOP: Self = VAnchor(4);
203    /// Height fits natural content size (default).
204    pub const FIT: Self = VAnchor(8);
205    /// Fill parent slot height (`BOTTOM | TOP`).
206    pub const STRETCH: Self = VAnchor(5); // 1 | 4
207    /// Take the larger of Fit or Stretch.
208    pub const MAX_FIT_OR_STRETCH: Self = VAnchor(13); // 8 | 5
209    /// Take the smaller of Fit or Stretch.
210    pub const MIN_FIT_OR_STRETCH: Self = VAnchor(16);
211
212    /// Returns `true` if all bits in `flags` are set in `self`.
213    #[inline]
214    pub fn contains(self, flags: Self) -> bool {
215        flags.0 != 0 && (self.0 & flags.0) == flags.0
216    }
217
218    /// Returns `true` if this anchor causes vertical stretching.
219    #[inline]
220    pub fn is_stretch(self) -> bool {
221        self.contains(Self::BOTTOM) && self.contains(Self::TOP)
222    }
223}
224
225impl Default for VAnchor {
226    /// Default is [`FIT`](VAnchor::FIT): take natural content height.
227    fn default() -> Self {
228        Self::FIT
229    }
230}
231
232impl std::ops::BitOr for VAnchor {
233    type Output = Self;
234    fn bitor(self, rhs: Self) -> Self {
235        VAnchor(self.0 | rhs.0)
236    }
237}
238
239impl std::ops::BitAnd for VAnchor {
240    type Output = Self;
241    fn bitand(self, rhs: Self) -> Self {
242        VAnchor(self.0 & rhs.0)
243    }
244}
245
246// ---------------------------------------------------------------------------
247// WidgetBase
248// ---------------------------------------------------------------------------
249
250/// Stores the five universal layout properties that every widget carries.
251///
252/// Embed in every concrete widget and delegate the five
253/// [`Widget`](crate::widget::Widget) layout-property getters to the
254/// corresponding fields.  The builder methods return `Self` so they can be
255/// chained on the concrete type.
256///
257/// ```rust,ignore
258/// pub struct MyWidget {
259///     bounds:   Rect,
260///     children: Vec<Box<dyn Widget>>,
261///     base:     WidgetBase,
262///     // ...widget-specific fields...
263/// }
264///
265/// impl Widget for MyWidget {
266///     fn margin(&self)   -> Insets  { self.base.margin }
267///     fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
268///     fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
269///     fn min_size(&self) -> Size    { self.base.min_size }
270///     fn max_size(&self) -> Size    { self.base.max_size }
271///     // ...
272/// }
273///
274/// impl MyWidget {
275///     pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
276///     pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
277///     pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
278///     pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
279///     pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
280/// }
281/// ```
282#[derive(Copy, Clone, Debug)]
283pub struct WidgetBase {
284    /// Space outside this widget's bounds (read by the parent during layout).
285    pub margin: Insets,
286    /// Horizontal anchor — how this widget positions/sizes itself horizontally.
287    pub h_anchor: HAnchor,
288    /// Vertical anchor — how this widget positions/sizes itself vertically.
289    pub v_anchor: VAnchor,
290    /// Minimum size constraint (logical units).  The parent will never assign
291    /// a slot smaller than this in either axis.
292    pub min_size: Size,
293    /// Maximum size constraint (logical units).  The parent will never assign
294    /// a slot larger than this in either axis.
295    pub max_size: Size,
296    /// Per-widget override of the global pixel-alignment policy.  When
297    /// `true` (the common default) `paint_subtree` rounds the child
298    /// translation to the physical pixel grid before painting, so crisp text
299    /// and strokes land on whole pixels regardless of fractional Label
300    /// heights (`font_size × 1.5`) accumulating through a flex stack.
301    /// Disable for widgets that deliberately want sub-pixel positioning
302    /// (smooth-scrolling markers, zoomed canvases).
303    ///
304    /// Mirrors MatterCAD's `GuiWidget.EnforceIntegerBounds`.  Captured from
305    /// [`pixel_bounds::default_enforce_integer_bounds`] at construction;
306    /// later global changes do NOT retroactively alter existing widgets.
307    pub enforce_integer_bounds: bool,
308}
309
310impl WidgetBase {
311    /// Construct a `WidgetBase` with all defaults:
312    /// zero margin, `FIT` anchors, `ZERO` min size, `Size::MAX` max size.
313    /// `enforce_integer_bounds` captures the current process-wide default.
314    pub fn new() -> Self {
315        Self {
316            margin: Insets::ZERO,
317            h_anchor: HAnchor::FIT,
318            v_anchor: VAnchor::FIT,
319            min_size: Size::ZERO,
320            max_size: Size::MAX,
321            enforce_integer_bounds: crate::pixel_bounds::default_enforce_integer_bounds(),
322        }
323    }
324
325    // ----- consuming builder methods ----------------------------------------
326
327    pub fn with_margin(mut self, m: Insets) -> Self {
328        self.margin = m;
329        self
330    }
331    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
332        self.h_anchor = h;
333        self
334    }
335    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
336        self.v_anchor = v;
337        self
338    }
339    pub fn with_min_size(mut self, s: Size) -> Self {
340        self.min_size = s;
341        self
342    }
343    pub fn with_max_size(mut self, s: Size) -> Self {
344        self.max_size = s;
345        self
346    }
347
348    // ----- helpers ----------------------------------------------------------
349
350    /// Clamp `proposed` to `[min_size, max_size]`.
351    #[inline]
352    pub fn clamp_size(&self, proposed: Size) -> Size {
353        Size::new(
354            proposed
355                .width
356                .clamp(self.min_size.width, self.max_size.width),
357            proposed
358                .height
359                .clamp(self.min_size.height, self.max_size.height),
360        )
361    }
362
363    /// Return [`margin`](Self::margin) in logical units.
364    ///
365    /// Previously multiplied by [`device_scale`](crate::device_scale::device_scale)
366    /// when margin handling was spread across widgets.  DPI scaling is now
367    /// applied once at the [`App`](crate::widget::App) boundary via a paint-
368    /// ctx transform, so widgets work in logical units end-to-end and this
369    /// helper is a simple passthrough kept for call-site readability.
370    pub fn scaled_margin(&self) -> Insets {
371        self.margin
372    }
373}
374
375impl Default for WidgetBase {
376    fn default() -> Self {
377        Self::new()
378    }
379}
380
381// ---------------------------------------------------------------------------
382// Helper: resolve MIN/MAX_FIT_OR_STRETCH
383// ---------------------------------------------------------------------------
384
385/// Given a natural (fit) size and a stretch (fill) size for one axis, resolve
386/// the `MIN_FIT_OR_STRETCH` or `MAX_FIT_OR_STRETCH` anchor to a concrete size.
387///
388/// Used by layout containers when a child has one of the composite anchors.
389#[inline]
390pub fn resolve_fit_or_stretch(fit_size: f64, stretch_size: f64, max_mode: bool) -> f64 {
391    if max_mode {
392        fit_size.max(stretch_size)
393    } else {
394        fit_size.min(stretch_size)
395    }
396}