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