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}