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 { left: 0.0, right: 0.0, top: 0.0, bottom: 0.0 };
53
54    /// All four sides the same value.
55    pub fn all(v: f64) -> Self {
56        Self { left: v, right: v, top: v, bottom: v }
57    }
58
59    /// Horizontal sides (`left` / `right`) = `h`, vertical (`top` / `bottom`) = `v`.
60    pub fn symmetric(h: f64, v: f64) -> Self {
61        Self { left: h, right: h, top: v, bottom: v }
62    }
63
64    /// Explicit per-side constructor.
65    pub fn from_sides(left: f64, right: f64, top: f64, bottom: f64) -> Self {
66        Self { left, right, top, bottom }
67    }
68
69    /// Sum of `left + right`.
70    #[inline] pub fn horizontal(&self) -> f64 { self.left + self.right }
71
72    /// Sum of `top + bottom`.
73    #[inline] pub fn vertical(&self)   -> f64 { self.top  + self.bottom }
74
75    /// Return a new `Insets` with all sides multiplied by `factor`.
76    #[inline]
77    pub fn scale(self, factor: f64) -> Self {
78        Self {
79            left:   self.left   * factor,
80            right:  self.right  * factor,
81            top:    self.top    * factor,
82            bottom: self.bottom * factor,
83        }
84    }
85}
86
87// ---------------------------------------------------------------------------
88// HAnchor
89// ---------------------------------------------------------------------------
90
91/// Horizontal anchor flags — how a widget sizes and positions itself
92/// horizontally within the slot assigned by its parent.
93///
94/// | Constant | Meaning |
95/// |---|---|
96/// | `ABSOLUTE` | No automatic sizing or positioning (manual bounds). |
97/// | `LEFT` | Align to the left edge of the slot (respecting margin). |
98/// | `CENTER` | Center horizontally in the slot (respecting margin). |
99/// | `RIGHT` | Align to the right edge of the slot (respecting margin). |
100/// | `FIT` | Width encloses natural content (default). |
101/// | `STRETCH` | Fill the slot width (`LEFT \| RIGHT`). |
102/// | `MAX_FIT_OR_STRETCH` | Take the larger of Fit or Stretch. |
103/// | `MIN_FIT_OR_STRETCH` | Take the smaller of Fit or Stretch. |
104///
105/// At most one of `LEFT`, `CENTER`, `RIGHT` may be set for position anchoring;
106/// combining `LEFT | RIGHT` means "stretch", not "anchor to both edges".
107#[derive(Copy, Clone, Debug, PartialEq, Eq)]
108pub struct HAnchor(u8);
109
110impl HAnchor {
111    pub const ABSOLUTE:           Self = HAnchor(0);
112    pub const LEFT:               Self = HAnchor(1);
113    pub const CENTER:             Self = HAnchor(2);
114    pub const RIGHT:              Self = HAnchor(4);
115    /// Width fits natural content size (default).
116    pub const FIT:                Self = HAnchor(8);
117    /// Fill parent slot width (`LEFT | RIGHT`).
118    pub const STRETCH:            Self = HAnchor(5);  // 1 | 4
119    /// Take the larger of Fit or Stretch.
120    pub const MAX_FIT_OR_STRETCH: Self = HAnchor(13); // 8 | 5
121    /// Take the smaller of Fit or Stretch.
122    pub const MIN_FIT_OR_STRETCH: Self = HAnchor(16);
123
124    /// Returns `true` if all bits in `flags` are set in `self`.
125    #[inline]
126    pub fn contains(self, flags: Self) -> bool {
127        flags.0 != 0 && (self.0 & flags.0) == flags.0
128    }
129
130    /// Returns `true` if this anchor causes horizontal stretching
131    /// (both LEFT and RIGHT are set, or MIN/MAX_FIT_OR_STRETCH resolves to stretch).
132    #[inline]
133    pub fn is_stretch(self) -> bool {
134        self.contains(Self::LEFT) && self.contains(Self::RIGHT)
135    }
136}
137
138impl Default for HAnchor {
139    /// Default is [`FIT`](HAnchor::FIT): take natural content width.
140    fn default() -> Self { Self::FIT }
141}
142
143impl std::ops::BitOr for HAnchor {
144    type Output = Self;
145    fn bitor(self, rhs: Self) -> Self { HAnchor(self.0 | rhs.0) }
146}
147
148impl std::ops::BitAnd for HAnchor {
149    type Output = Self;
150    fn bitand(self, rhs: Self) -> Self { HAnchor(self.0 & rhs.0) }
151}
152
153// ---------------------------------------------------------------------------
154// VAnchor
155// ---------------------------------------------------------------------------
156
157/// Vertical anchor flags — how a widget sizes and positions itself vertically
158/// within the slot assigned by its parent.
159///
160/// Mirrors [`HAnchor`] with `BOTTOM` / `TOP` instead of `LEFT` / `RIGHT`.
161/// Y-up convention: `BOTTOM` is the visually lower edge (small Y), `TOP` is
162/// the visually upper edge (large Y).
163#[derive(Copy, Clone, Debug, PartialEq, Eq)]
164pub struct VAnchor(u8);
165
166impl VAnchor {
167    pub const ABSOLUTE:           Self = VAnchor(0);
168    pub const BOTTOM:             Self = VAnchor(1);
169    pub const CENTER:             Self = VAnchor(2);
170    pub const TOP:                Self = VAnchor(4);
171    /// Height fits natural content size (default).
172    pub const FIT:                Self = VAnchor(8);
173    /// Fill parent slot height (`BOTTOM | TOP`).
174    pub const STRETCH:            Self = VAnchor(5);  // 1 | 4
175    /// Take the larger of Fit or Stretch.
176    pub const MAX_FIT_OR_STRETCH: Self = VAnchor(13); // 8 | 5
177    /// Take the smaller of Fit or Stretch.
178    pub const MIN_FIT_OR_STRETCH: Self = VAnchor(16);
179
180    /// Returns `true` if all bits in `flags` are set in `self`.
181    #[inline]
182    pub fn contains(self, flags: Self) -> bool {
183        flags.0 != 0 && (self.0 & flags.0) == flags.0
184    }
185
186    /// Returns `true` if this anchor causes vertical stretching.
187    #[inline]
188    pub fn is_stretch(self) -> bool {
189        self.contains(Self::BOTTOM) && self.contains(Self::TOP)
190    }
191}
192
193impl Default for VAnchor {
194    /// Default is [`FIT`](VAnchor::FIT): take natural content height.
195    fn default() -> Self { Self::FIT }
196}
197
198impl std::ops::BitOr for VAnchor {
199    type Output = Self;
200    fn bitor(self, rhs: Self) -> Self { VAnchor(self.0 | rhs.0) }
201}
202
203impl std::ops::BitAnd for VAnchor {
204    type Output = Self;
205    fn bitand(self, rhs: Self) -> Self { VAnchor(self.0 & rhs.0) }
206}
207
208// ---------------------------------------------------------------------------
209// WidgetBase
210// ---------------------------------------------------------------------------
211
212/// Stores the five universal layout properties that every widget carries.
213///
214/// Embed in every concrete widget and delegate the five
215/// [`Widget`](crate::widget::Widget) layout-property getters to the
216/// corresponding fields.  The builder methods return `Self` so they can be
217/// chained on the concrete type.
218///
219/// ```rust,ignore
220/// pub struct MyWidget {
221///     bounds:   Rect,
222///     children: Vec<Box<dyn Widget>>,
223///     base:     WidgetBase,
224///     // ...widget-specific fields...
225/// }
226///
227/// impl Widget for MyWidget {
228///     fn margin(&self)   -> Insets  { self.base.margin }
229///     fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
230///     fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
231///     fn min_size(&self) -> Size    { self.base.min_size }
232///     fn max_size(&self) -> Size    { self.base.max_size }
233///     // ...
234/// }
235///
236/// impl MyWidget {
237///     pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
238///     pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
239///     pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
240///     pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
241///     pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
242/// }
243/// ```
244#[derive(Copy, Clone, Debug)]
245pub struct WidgetBase {
246    /// Space outside this widget's bounds (read by the parent during layout).
247    pub margin:   Insets,
248    /// Horizontal anchor — how this widget positions/sizes itself horizontally.
249    pub h_anchor: HAnchor,
250    /// Vertical anchor — how this widget positions/sizes itself vertically.
251    pub v_anchor: VAnchor,
252    /// Minimum size constraint (logical units).  The parent will never assign
253    /// a slot smaller than this in either axis.
254    pub min_size: Size,
255    /// Maximum size constraint (logical units).  The parent will never assign
256    /// a slot larger than this in either axis.
257    pub max_size: Size,
258    /// Per-widget override of the global pixel-alignment policy.  When
259    /// `true` (the common default) `paint_subtree` rounds the child
260    /// translation to the physical pixel grid before painting, so crisp text
261    /// and strokes land on whole pixels regardless of fractional Label
262    /// heights (`font_size × 1.5`) accumulating through a flex stack.
263    /// Disable for widgets that deliberately want sub-pixel positioning
264    /// (smooth-scrolling markers, zoomed canvases).
265    ///
266    /// Mirrors MatterCAD's `GuiWidget.EnforceIntegerBounds`.  Captured from
267    /// [`pixel_bounds::default_enforce_integer_bounds`] at construction;
268    /// later global changes do NOT retroactively alter existing widgets.
269    pub enforce_integer_bounds: bool,
270}
271
272impl WidgetBase {
273    /// Construct a `WidgetBase` with all defaults:
274    /// zero margin, `FIT` anchors, `ZERO` min size, `Size::MAX` max size.
275    /// `enforce_integer_bounds` captures the current process-wide default.
276    pub fn new() -> Self {
277        Self {
278            margin:   Insets::ZERO,
279            h_anchor: HAnchor::FIT,
280            v_anchor: VAnchor::FIT,
281            min_size: Size::ZERO,
282            max_size: Size::MAX,
283            enforce_integer_bounds: crate::pixel_bounds::default_enforce_integer_bounds(),
284        }
285    }
286
287    // ----- consuming builder methods ----------------------------------------
288
289    pub fn with_margin(mut self, m: Insets)    -> Self { self.margin   = m; self }
290    pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.h_anchor = h; self }
291    pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.v_anchor = v; self }
292    pub fn with_min_size(mut self, s: Size)    -> Self { self.min_size = s; self }
293    pub fn with_max_size(mut self, s: Size)    -> Self { self.max_size = s; self }
294
295    // ----- helpers ----------------------------------------------------------
296
297    /// Clamp `proposed` to `[min_size, max_size]`.
298    #[inline]
299    pub fn clamp_size(&self, proposed: Size) -> Size {
300        Size::new(
301            proposed.width .clamp(self.min_size.width,  self.max_size.width),
302            proposed.height.clamp(self.min_size.height, self.max_size.height),
303        )
304    }
305
306    /// Return [`margin`](Self::margin) in logical units.
307    ///
308    /// Previously multiplied by [`device_scale`](crate::device_scale::device_scale)
309    /// when margin handling was spread across widgets.  DPI scaling is now
310    /// applied once at the [`App`](crate::widget::App) boundary via a paint-
311    /// ctx transform, so widgets work in logical units end-to-end and this
312    /// helper is a simple passthrough kept for call-site readability.
313    pub fn scaled_margin(&self) -> Insets {
314        self.margin
315    }
316}
317
318impl Default for WidgetBase {
319    fn default() -> Self { Self::new() }
320}
321
322// ---------------------------------------------------------------------------
323// Helper: resolve MIN/MAX_FIT_OR_STRETCH
324// ---------------------------------------------------------------------------
325
326/// Given a natural (fit) size and a stretch (fill) size for one axis, resolve
327/// the `MIN_FIT_OR_STRETCH` or `MAX_FIT_OR_STRETCH` anchor to a concrete size.
328///
329/// Used by layout containers when a child has one of the composite anchors.
330#[inline]
331pub fn resolve_fit_or_stretch(fit_size: f64, stretch_size: f64, max_mode: bool) -> f64 {
332    if max_mode {
333        fit_size.max(stretch_size)
334    } else {
335        fit_size.min(stretch_size)
336    }
337}