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}