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//! # Coordinate system
18//!
19//! Layout produces positions in **logical Y-up** space, origin at the
20//! bottom-left. [`VAnchor::TOP`] anchors to the high-Y edge of the parent
21//! slot, [`VAnchor::BOTTOM`] to Y=0. The App handles the Y-flip against
22//! platform input coordinates at the event boundary (see `App::flip_y`),
23//! so anchor/margin math here is straight arithmetic in Y-up space.
24//!
25//! # Margin vs padding
26//!
27//! - **Margin** lives on the child and is read by the parent during layout.
28//!   It is space *outside* the widget's bounds.
29//! - **Padding** is the parent container's internal inset — space between its
30//!   own border and its children.  Containers store padding directly (e.g.
31//!   `FlexColumn::inner_padding`); individual leaf widgets do not have padding.
32//!
33//! # Margin semantics
34//!
35//! Margins are **additive**, not collapsed.  When child A has
36//! `margin.bottom = 4` and child B has `margin.top = 6`, the gap between them
37//! is `gap + 4 + 6 = 10 + gap`, not `max(4, 6) = 6`.  This matches the
38//! original C# agg-sharp behaviour.
39
40use crate::geometry::Size;
41
42// ---------------------------------------------------------------------------
43// Insets
44// ---------------------------------------------------------------------------
45
46/// Per-side inset values (logical units).
47///
48/// Used for both widget **margin** (space outside the widget) and container
49/// **padding** (space inside the container around its children).
50#[derive(Copy, Clone, Debug, PartialEq, Default)]
51#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
52pub struct Insets {
53    pub left: f64,
54    pub right: f64,
55    pub top: f64,
56    pub bottom: f64,
57}
58
59impl Insets {
60    /// All sides zero.
61    pub const ZERO: Self = Self {
62        left: 0.0,
63        right: 0.0,
64        top: 0.0,
65        bottom: 0.0,
66    };
67
68    /// All four sides the same value.
69    pub fn all(v: f64) -> Self {
70        Self {
71            left: v,
72            right: v,
73            top: v,
74            bottom: v,
75        }
76    }
77
78    /// Horizontal sides (`left` / `right`) = `h`, vertical (`top` / `bottom`) = `v`.
79    pub fn symmetric(h: f64, v: f64) -> Self {
80        Self {
81            left: h,
82            right: h,
83            top: v,
84            bottom: v,
85        }
86    }
87
88    /// Explicit per-side constructor.
89    pub fn from_sides(left: f64, right: f64, top: f64, bottom: f64) -> Self {
90        Self {
91            left,
92            right,
93            top,
94            bottom,
95        }
96    }
97
98    /// Sum of `left + right`.
99    #[inline]
100    pub fn horizontal(&self) -> f64 {
101        self.left + self.right
102    }
103
104    /// Sum of `top + bottom`.
105    #[inline]
106    pub fn vertical(&self) -> f64 {
107        self.top + self.bottom
108    }
109
110    /// Return a new `Insets` with all sides multiplied by `factor`.
111    #[inline]
112    pub fn scale(self, factor: f64) -> Self {
113        Self {
114            left: self.left * factor,
115            right: self.right * factor,
116            top: self.top * factor,
117            bottom: self.bottom * factor,
118        }
119    }
120}
121
122// ---------------------------------------------------------------------------
123// HAnchor
124// ---------------------------------------------------------------------------
125
126/// Horizontal anchor flags — how a widget sizes and positions itself
127/// horizontally within the slot assigned by its parent.
128///
129/// | Constant | Meaning |
130/// |---|---|
131/// | `ABSOLUTE` | No automatic sizing or positioning (manual bounds). |
132/// | `LEFT` | Align to the left edge of the slot (respecting margin). |
133/// | `CENTER` | Center horizontally in the slot (respecting margin). |
134/// | `RIGHT` | Align to the right edge of the slot (respecting margin). |
135/// | `FIT` | Width encloses natural content (default). |
136/// | `STRETCH` | Fill the slot width (`LEFT \| RIGHT`). |
137/// | `MAX_FIT_OR_STRETCH` | Take the larger of Fit or Stretch. |
138/// | `MIN_FIT_OR_STRETCH` | Take the smaller of Fit or Stretch. |
139///
140/// At most one of `LEFT`, `CENTER`, `RIGHT` may be set for position anchoring;
141/// combining `LEFT | RIGHT` means "stretch", not "anchor to both edges".
142#[derive(Copy, Clone, Debug, PartialEq, Eq)]
143#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
144#[cfg_attr(feature = "reflect", reflect(opaque))]
145pub struct HAnchor(u8);
146
147impl HAnchor {
148    pub const ABSOLUTE: Self = HAnchor(0);
149    pub const LEFT: Self = HAnchor(1);
150    pub const CENTER: Self = HAnchor(2);
151    pub const RIGHT: Self = HAnchor(4);
152    /// Width fits natural content size (default).
153    pub const FIT: Self = HAnchor(8);
154    /// Fill parent slot width (`LEFT | RIGHT`).
155    pub const STRETCH: Self = HAnchor(5); // 1 | 4
156    /// Take the larger of Fit or Stretch.
157    pub const MAX_FIT_OR_STRETCH: Self = HAnchor(13); // 8 | 5
158    /// Take the smaller of Fit or Stretch.
159    pub const MIN_FIT_OR_STRETCH: Self = HAnchor(16);
160
161    /// Returns `true` if all bits in `flags` are set in `self`.
162    #[inline]
163    pub fn contains(self, flags: Self) -> bool {
164        flags.0 != 0 && (self.0 & flags.0) == flags.0
165    }
166
167    /// Returns `true` if this anchor causes horizontal stretching
168    /// (both LEFT and RIGHT are set, or MIN/MAX_FIT_OR_STRETCH resolves to stretch).
169    #[inline]
170    pub fn is_stretch(self) -> bool {
171        self.contains(Self::LEFT) && self.contains(Self::RIGHT)
172    }
173
174    /// Raw bit value — used by the inspector to store/cycle anchor presets.
175    #[inline]
176    pub fn bits(self) -> u8 {
177        self.0
178    }
179
180    /// Short display name for the inspector properties pane.
181    pub fn display_name(self) -> &'static str {
182        match self.0 {
183            b if b == Self::FIT.0 => "Fit",
184            b if b == Self::STRETCH.0 => "Stretch",
185            b if b == Self::LEFT.0 => "Left",
186            b if b == Self::CENTER.0 => "Center",
187            b if b == Self::RIGHT.0 => "Right",
188            b if b == Self::MAX_FIT_OR_STRETCH.0 => "MaxFitStr",
189            b if b == Self::MIN_FIT_OR_STRETCH.0 => "MinFitStr",
190            _ => "Abs",
191        }
192    }
193}
194
195impl Default for HAnchor {
196    /// Default is [`FIT`](HAnchor::FIT): take natural content width.
197    fn default() -> Self {
198        Self::FIT
199    }
200}
201
202impl std::ops::BitOr for HAnchor {
203    type Output = Self;
204    fn bitor(self, rhs: Self) -> Self {
205        HAnchor(self.0 | rhs.0)
206    }
207}
208
209impl std::ops::BitAnd for HAnchor {
210    type Output = Self;
211    fn bitand(self, rhs: Self) -> Self {
212        HAnchor(self.0 & rhs.0)
213    }
214}
215
216// ---------------------------------------------------------------------------
217// VAnchor
218// ---------------------------------------------------------------------------
219
220/// Vertical anchor flags — how a widget sizes and positions itself vertically
221/// within the slot assigned by its parent.
222///
223/// Mirrors [`HAnchor`] with `BOTTOM` / `TOP` instead of `LEFT` / `RIGHT`.
224/// Y-up convention: `BOTTOM` is the visually lower edge (small Y), `TOP` is
225/// the visually upper edge (large Y).
226#[derive(Copy, Clone, Debug, PartialEq, Eq)]
227#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
228#[cfg_attr(feature = "reflect", reflect(opaque))]
229pub struct VAnchor(u8);
230
231impl VAnchor {
232    pub const ABSOLUTE: Self = VAnchor(0);
233    pub const BOTTOM: Self = VAnchor(1);
234    pub const CENTER: Self = VAnchor(2);
235    pub const TOP: Self = VAnchor(4);
236    /// Height fits natural content size (default).
237    pub const FIT: Self = VAnchor(8);
238    /// Fill parent slot height (`BOTTOM | TOP`).
239    pub const STRETCH: Self = VAnchor(5); // 1 | 4
240    /// Take the larger of Fit or Stretch.
241    pub const MAX_FIT_OR_STRETCH: Self = VAnchor(13); // 8 | 5
242    /// Take the smaller of Fit or Stretch.
243    pub const MIN_FIT_OR_STRETCH: Self = VAnchor(16);
244
245    /// Returns `true` if all bits in `flags` are set in `self`.
246    #[inline]
247    pub fn contains(self, flags: Self) -> bool {
248        flags.0 != 0 && (self.0 & flags.0) == flags.0
249    }
250
251    /// Returns `true` if this anchor causes vertical stretching.
252    #[inline]
253    pub fn is_stretch(self) -> bool {
254        self.contains(Self::BOTTOM) && self.contains(Self::TOP)
255    }
256
257    /// Raw bit value — used by the inspector to store/cycle anchor presets.
258    #[inline]
259    pub fn bits(self) -> u8 {
260        self.0
261    }
262
263    /// Short display name for the inspector properties pane.
264    pub fn display_name(self) -> &'static str {
265        match self.0 {
266            b if b == Self::FIT.0 => "Fit",
267            b if b == Self::STRETCH.0 => "Stretch",
268            b if b == Self::BOTTOM.0 => "Bottom",
269            b if b == Self::CENTER.0 => "Center",
270            b if b == Self::TOP.0 => "Top",
271            b if b == Self::MAX_FIT_OR_STRETCH.0 => "MaxFitStr",
272            b if b == Self::MIN_FIT_OR_STRETCH.0 => "MinFitStr",
273            _ => "Abs",
274        }
275    }
276}
277
278impl Default for VAnchor {
279    /// Default is [`FIT`](VAnchor::FIT): take natural content height.
280    fn default() -> Self {
281        Self::FIT
282    }
283}
284
285impl std::ops::BitOr for VAnchor {
286    type Output = Self;
287    fn bitor(self, rhs: Self) -> Self {
288        VAnchor(self.0 | rhs.0)
289    }
290}
291
292impl std::ops::BitAnd for VAnchor {
293    type Output = Self;
294    fn bitand(self, rhs: Self) -> Self {
295        VAnchor(self.0 & rhs.0)
296    }
297}
298
299// ---------------------------------------------------------------------------
300// WidgetBase
301// ---------------------------------------------------------------------------
302
303/// Stores the five universal layout properties that every widget carries.
304///
305/// Embed in every concrete widget and delegate the five
306/// [`Widget`](crate::widget::Widget) layout-property getters to the
307/// corresponding fields.  The builder methods return `Self` so they can be
308/// chained on the concrete type.
309///
310/// ```rust,ignore
311/// pub struct MyWidget {
312///     bounds:   Rect,
313///     children: Vec<Box<dyn Widget>>,
314///     base:     WidgetBase,
315///     // ...widget-specific fields...
316/// }
317///
318/// impl Widget for MyWidget {
319///     fn margin(&self)   -> Insets  { self.base.margin }
320///     fn h_anchor(&self) -> HAnchor { self.base.h_anchor }
321///     fn v_anchor(&self) -> VAnchor { self.base.v_anchor }
322///     fn min_size(&self) -> Size    { self.base.min_size }
323///     fn max_size(&self) -> Size    { self.base.max_size }
324///     // ...
325/// }
326///
327/// impl MyWidget {
328///     pub fn with_margin(mut self, m: Insets)    -> Self { self.base.margin   = m; self }
329///     pub fn with_h_anchor(mut self, h: HAnchor) -> Self { self.base.h_anchor = h; self }
330///     pub fn with_v_anchor(mut self, v: VAnchor) -> Self { self.base.v_anchor = v; self }
331///     pub fn with_min_size(mut self, s: Size)    -> Self { self.base.min_size = s; self }
332///     pub fn with_max_size(mut self, s: Size)    -> Self { self.base.max_size = s; self }
333/// }
334/// ```
335#[derive(Clone, Debug)]
336#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
337pub struct WidgetBase {
338    /// Space outside this widget's bounds (read by the parent during layout).
339    pub margin: Insets,
340    /// Horizontal anchor — how this widget positions/sizes itself horizontally.
341    pub h_anchor: HAnchor,
342    /// Vertical anchor — how this widget positions/sizes itself vertically.
343    pub v_anchor: VAnchor,
344    /// Minimum size constraint (logical units).  The parent will never assign
345    /// a slot smaller than this in either axis.
346    pub min_size: Size,
347    /// Maximum size constraint (logical units).  The parent will never assign
348    /// a slot larger than this in either axis.
349    pub max_size: Size,
350    /// Per-widget override of the global pixel-alignment policy.  When
351    /// `true` (the common default) `paint_subtree` rounds the child
352    /// translation to the physical pixel grid before painting, so crisp text
353    /// and strokes land on whole pixels regardless of fractional Label
354    /// heights (`font_size × 1.5`) accumulating through a flex stack.
355    /// Disable for widgets that deliberately want sub-pixel positioning
356    /// (smooth-scrolling markers, zoomed canvases).
357    ///
358    /// Mirrors MatterCAD's `GuiWidget.EnforceIntegerBounds`.  Captured from
359    /// [`pixel_bounds::default_enforce_integer_bounds`] at construction;
360    /// later global changes do NOT retroactively alter existing widgets.
361    pub enforce_integer_bounds: bool,
362    /// Optional hover-help text.  When `Some`, the central tooltip controller
363    /// (see [`crate::widgets::tooltip`]) shows this string after the initial
364    /// hover delay whenever this widget is the deepest hovered tipped widget —
365    /// no wrapper needed.  This is the universal storage behind
366    /// [`Widget::with_tooltip`](crate::widget::Widget::with_tooltip) /
367    /// [`Widget::tooltip_text`](crate::widget::Widget::tooltip_text): any widget
368    /// that embeds a `WidgetBase` opts in for free.
369    ///
370    /// A `String` here is why `WidgetBase` is `Clone` but not `Copy`.
371    pub tooltip: Option<String>,
372    /// Marks this widget as its scope's keyboard **default action** — the
373    /// control Return / Enter activates when the key reaches the scope
374    /// unconsumed (focus is not on a text input, which consumes Enter
375    /// itself). SwiftUI `.keyboardShortcut(.defaultAction)` / AppKit's
376    /// default button.
377    ///
378    /// [`ModalSheet`](crate::widgets::ModalSheet) searches its content for
379    /// the first visible widget with this flag when Enter bubbles to it and
380    /// activates it by dispatching that Enter (a `Button` fires its click);
381    /// the [`App`](crate::widget::App) does the same for the whole tree when
382    /// no modal is showing, so only the topmost scope's default fires.
383    /// Set via `Button::with_default_action`; any widget embedding a
384    /// `WidgetBase` can opt in the same way.
385    pub default_action: bool,
386    /// Escape counterpart of [`default_action`](Self::default_action) —
387    /// SwiftUI `.keyboardShortcut(.cancelAction)`. A `ModalSheet` whose
388    /// content holds a cancel-action widget activates it on Escape instead
389    /// of closing itself, so the Cancel handler owns dismissal. Set via
390    /// `Button::with_cancel_action`.
391    pub cancel_action: bool,
392}
393
394impl WidgetBase {
395    /// Construct a `WidgetBase` with all defaults:
396    /// zero margin, `FIT` anchors, `ZERO` min size, `Size::MAX` max size.
397    /// `enforce_integer_bounds` captures the current process-wide default.
398    pub fn new() -> Self {
399        Self {
400            margin: Insets::ZERO,
401            h_anchor: HAnchor::FIT,
402            v_anchor: VAnchor::FIT,
403            min_size: Size::ZERO,
404            max_size: Size::MAX,
405            enforce_integer_bounds: crate::pixel_bounds::default_enforce_integer_bounds(),
406            tooltip: None,
407            default_action: false,
408            cancel_action: false,
409        }
410    }
411
412    // ----- consuming builder methods ----------------------------------------
413
414    pub fn with_margin(mut self, m: Insets) -> Self {
415        self.margin = m;
416        self
417    }
418    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
419        self.h_anchor = h;
420        self
421    }
422    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
423        self.v_anchor = v;
424        self
425    }
426    pub fn with_min_size(mut self, s: Size) -> Self {
427        self.min_size = s;
428        self
429    }
430    pub fn with_max_size(mut self, s: Size) -> Self {
431        self.max_size = s;
432        self
433    }
434    /// Attach hover-help text read by the central tooltip controller.
435    pub fn with_tooltip(mut self, text: impl Into<String>) -> Self {
436        self.tooltip = Some(text.into());
437        self
438    }
439
440    // ----- helpers ----------------------------------------------------------
441
442    /// Clamp `proposed` to `[min_size, max_size]`.
443    #[inline]
444    pub fn clamp_size(&self, proposed: Size) -> Size {
445        Size::new(
446            proposed
447                .width
448                .clamp(self.min_size.width, self.max_size.width),
449            proposed
450                .height
451                .clamp(self.min_size.height, self.max_size.height),
452        )
453    }
454
455    /// Return [`margin`](Self::margin) in logical units.
456    ///
457    /// Previously multiplied by [`device_scale`](crate::device_scale::device_scale)
458    /// when margin handling was spread across widgets.  DPI scaling is now
459    /// applied once at the [`App`](crate::widget::App) boundary via a paint-
460    /// ctx transform, so widgets work in logical units end-to-end and this
461    /// helper is a simple passthrough kept for call-site readability.
462    pub fn scaled_margin(&self) -> Insets {
463        self.margin
464    }
465}
466
467impl Default for WidgetBase {
468    fn default() -> Self {
469        Self::new()
470    }
471}
472
473// ---------------------------------------------------------------------------
474// Helper: resolve MIN/MAX_FIT_OR_STRETCH
475// ---------------------------------------------------------------------------
476
477/// Given a natural (fit) size and a stretch (fill) size for one axis, resolve
478/// the `MIN_FIT_OR_STRETCH` or `MAX_FIT_OR_STRETCH` anchor to a concrete size.
479///
480/// Used by layout containers when a child has one of the composite anchors.
481#[inline]
482pub fn resolve_fit_or_stretch(fit_size: f64, stretch_size: f64, max_mode: bool) -> f64 {
483    if max_mode {
484        fit_size.max(stretch_size)
485    } else {
486        fit_size.min(stretch_size)
487    }
488}