Skip to main content

gizmo_ui/
components.rs

1use gizmo_math::{Vec2, Vec4};
2
3/// A length used by [`Style`].
4///
5/// Plain old data: a tag plus one `f32`. This is deliberately *our* type and not
6/// `taffy`'s — see the [`Style`] docs for why.
7///
8/// `Percent` uses the **CSS scale**, `0.0..=100.0`, not the `0.0..=1.0` fraction
9/// `taffy` uses internally. `Val::Percent(50.0)` is half of the parent.
10#[derive(Clone, Copy, Debug, PartialEq, Default)]
11#[non_exhaustive]
12pub enum Val {
13    /// The layout algorithm decides. What "auto" means depends on the property:
14    /// for a size it is content-based, for a margin/inset it is the CSS `auto`
15    /// keyword, and for padding/border/gap — which have no `auto` in CSS — it is
16    /// treated as zero.
17    #[default]
18    Auto,
19    /// An absolute length in logical pixels.
20    Px(f32),
21    /// A percentage of the parent's corresponding dimension, on the CSS scale
22    /// (`100.0` = 100%, i.e. the full parent dimension).
23    Percent(f32),
24}
25
26impl Val {
27    /// Zero pixels.
28    pub const ZERO: Val = Val::Px(0.0);
29}
30
31/// Four per-side [`Val`]s, used for `inset`, `margin`, `padding` and `border`.
32///
33/// [`Default`] is [`UiRect::ZERO`], matching the CSS initial value of margin,
34/// padding and border. `inset` is the exception and defaults to
35/// [`UiRect::AUTO`]; [`Style::DEFAULT`] sets it explicitly.
36#[derive(Clone, Copy, Debug, PartialEq)]
37pub struct UiRect {
38    /// Left edge.
39    pub left: Val,
40    /// Right edge.
41    pub right: Val,
42    /// Top edge.
43    pub top: Val,
44    /// Bottom edge.
45    pub bottom: Val,
46}
47
48impl UiRect {
49    /// All four sides zero pixels.
50    pub const ZERO: UiRect = UiRect::all(Val::ZERO);
51    /// All four sides [`Val::Auto`].
52    pub const AUTO: UiRect = UiRect::all(Val::Auto);
53
54    /// A rect with the same value on all four sides.
55    pub const fn all(value: Val) -> Self {
56        Self { left: value, right: value, top: value, bottom: value }
57    }
58
59    /// A rect with one value on the left/right sides and another on top/bottom.
60    pub const fn axes(horizontal: Val, vertical: Val) -> Self {
61        Self { left: horizontal, right: horizontal, top: vertical, bottom: vertical }
62    }
63
64    /// A rect with an explicit value per side.
65    pub const fn new(left: Val, right: Val, top: Val, bottom: Val) -> Self {
66        Self { left, right, top, bottom }
67    }
68}
69
70impl Default for UiRect {
71    fn default() -> Self {
72        Self::ZERO
73    }
74}
75
76/// Which layout algorithm lays out an element's children.
77#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
78#[non_exhaustive]
79pub enum Display {
80    /// Flexbox. The default, and the layout mode this crate is built around.
81    #[default]
82    Flex,
83    /// Block layout: children stack vertically.
84    Block,
85    /// The element and its whole subtree generate no boxes and are skipped.
86    None,
87}
88
89/// How an element's `inset` is interpreted.
90#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
91#[non_exhaustive]
92pub enum PositionType {
93    /// Laid out in flow; `inset` nudges the element afterwards without moving
94    /// its siblings. The CSS `position: relative` behaviour.
95    #[default]
96    Relative,
97    /// Taken out of flow and positioned against the nearest positioned
98    /// ancestor. The CSS `position: absolute` behaviour.
99    Absolute,
100}
101
102/// Direction of the flexbox main axis.
103#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
104#[non_exhaustive]
105pub enum FlexDirection {
106    /// Left to right.
107    #[default]
108    Row,
109    /// Top to bottom.
110    Column,
111    /// Right to left.
112    RowReverse,
113    /// Bottom to top.
114    ColumnReverse,
115}
116
117/// Whether flex items may wrap onto more than one line.
118#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
119#[non_exhaustive]
120pub enum FlexWrap {
121    /// Everything on a single line.
122    #[default]
123    NoWrap,
124    /// Wrap along the [`FlexDirection`].
125    Wrap,
126    /// Wrap in the opposite direction.
127    WrapReverse,
128}
129
130/// Alignment of items along the cross axis.
131///
132/// Also used for `align_self` via the [`AlignSelf`] alias.
133#[derive(Clone, Copy, Debug, PartialEq, Eq)]
134#[non_exhaustive]
135pub enum AlignItems {
136    /// Packed toward the start of the axis.
137    Start,
138    /// Packed toward the end of the axis.
139    End,
140    /// Packed toward the flex-relative start (start, or end when the direction
141    /// is reversed).
142    FlexStart,
143    /// Packed toward the flex-relative end.
144    FlexEnd,
145    /// Centred on the axis.
146    Center,
147    /// Aligned so the items' baselines line up.
148    Baseline,
149    /// Stretched to fill the container.
150    Stretch,
151}
152
153/// Alignment of an element against its parent's cross axis, overriding the
154/// parent's [`AlignItems`].
155pub type AlignSelf = AlignItems;
156
157/// Distribution of free space between and around items.
158///
159/// Also used for `justify_content` via the [`JustifyContent`] alias.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161#[non_exhaustive]
162pub enum AlignContent {
163    /// Packed toward the start of the axis.
164    Start,
165    /// Packed toward the end of the axis.
166    End,
167    /// Packed toward the flex-relative start.
168    FlexStart,
169    /// Packed toward the flex-relative end.
170    FlexEnd,
171    /// Centred on the axis.
172    Center,
173    /// Stretched to fill the container.
174    Stretch,
175    /// First and last items flush with the edges, gaps distributed evenly.
176    SpaceBetween,
177    /// Gaps between, before and after the items are all equal.
178    SpaceEvenly,
179    /// The outer gaps are half the size of the gaps between items.
180    SpaceAround,
181}
182
183/// Distribution of free space along the main axis.
184pub type JustifyContent = AlignContent;
185
186/// Layout style of a UI element.
187///
188/// This is **plain old data** — enums, `f32`s and `Option<f32>`, nothing else.
189/// It is the ECS component, so it has to be `Send + Sync`, and being POD it is
190/// so *derived*, with no `unsafe impl` anywhere in this crate for it.
191///
192/// The layout engine underneath is [`taffy`], but no `taffy` type appears in
193/// this crate's public API. The conversion happens in one place —
194/// `UiContext::to_taffy_style` in `layout.rs` — and the layout system calls it
195/// once per styled entity per frame.
196///
197/// # Not modelled
198///
199/// This type covers the flexbox/block surface the crate's layout system
200/// actually drives. Everything `taffy` supports that is **not** listed as a
201/// field below is deliberately absent, and setting it is impossible rather than
202/// merely awkward:
203///
204/// - **CSS Grid.** No `Display::Grid`, no `grid_template_*`, `grid_auto_*`,
205///   `grid_row`/`grid_column` placement, no `fr` units, and no
206///   `justify_items`/`justify_self` (both are `#[cfg(feature = "grid")]` in
207///   `taffy` and ignored by flexbox — `align_items`/`align_self` are the
208///   flexbox cross-axis pair, and they *are* modelled). `taffy`'s grid
209///   algorithm is still compiled in (see `Cargo.toml`) but is unreachable from
210///   here; exposing it needs a grid-track type of its own.
211/// - **`overflow` and `scrollbar_width`.** This crate does no clipping and no
212///   scrolling, so a scroll container would compute geometry nothing honours.
213/// - **`box_sizing`.** `taffy`'s default, border-box, is always used.
214/// - **`direction` (LTR/RTL)** and **`text_align`.** There is no text rendering
215///   in this engine at all.
216/// - **`float` / `clear`**, **`item_is_table`**, **`item_is_replaced`**.
217/// - **Intrinsic sizing keywords** (`min-content`, `max-content`,
218///   `fit-content`) and **`calc()` lengths**: [`Val`] is `Auto`/`Px`/`Percent`
219///   only. Excluding `calc()` is load-bearing, not an oversight — see the
220///   `SAFETY` note on `UiContext` in `layout.rs`.
221///
222/// # Example
223///
224/// ```
225/// use gizmo_ui::components::{Style, Val, UiRect, FlexDirection};
226///
227/// let style = Style {
228///     width: Val::Percent(100.0),
229///     height: Val::Px(64.0),
230///     flex_direction: FlexDirection::Column,
231///     padding: UiRect::all(Val::Px(8.0)),
232///     ..Default::default()
233/// };
234/// assert_eq!(style.flex_shrink, 1.0); // CSS default, not 0.0
235/// ```
236#[derive(Clone, Copy, Debug, PartialEq)]
237pub struct Style {
238    /// Layout algorithm used for this element's children.
239    pub display: Display,
240    /// How [`Style::inset`] is interpreted.
241    pub position_type: PositionType,
242    /// Per-side offset, applied according to [`Style::position_type`].
243    pub inset: UiRect,
244
245    /// Preferred width.
246    pub width: Val,
247    /// Preferred height.
248    pub height: Val,
249    /// Lower bound on the width.
250    pub min_width: Val,
251    /// Lower bound on the height.
252    pub min_height: Val,
253    /// Upper bound on the width.
254    pub max_width: Val,
255    /// Upper bound on the height.
256    pub max_height: Val,
257    /// Preferred width-divided-by-height ratio, if any.
258    pub aspect_ratio: Option<f32>,
259
260    /// Space outside the border box. `Val::Auto` here is a real CSS auto margin.
261    pub margin: UiRect,
262    /// Space between the border and the content. `Val::Auto` is treated as zero.
263    pub padding: UiRect,
264    /// Border thickness reserved by layout. `Val::Auto` is treated as zero.
265    pub border: UiRect,
266
267    /// Cross-axis alignment applied to the children of this element.
268    pub align_items: Option<AlignItems>,
269    /// Cross-axis alignment of this element, overriding the parent's
270    /// [`Style::align_items`].
271    pub align_self: Option<AlignSelf>,
272    /// Cross-axis distribution of this element's lines of content.
273    pub align_content: Option<AlignContent>,
274    /// Main-axis distribution of this element's children.
275    pub justify_content: Option<JustifyContent>,
276
277    /// Direction of the main axis.
278    pub flex_direction: FlexDirection,
279    /// Whether children may wrap onto multiple lines.
280    pub flex_wrap: FlexWrap,
281    /// Share of leftover main-axis space this element grows into. CSS default `0.0`.
282    pub flex_grow: f32,
283    /// Rate at which this element shrinks when space is short. CSS default `1.0`.
284    pub flex_shrink: f32,
285    /// Initial main-axis size, before grow/shrink.
286    pub flex_basis: Val,
287
288    /// Gap between rows. `Val::Auto` is treated as zero.
289    pub row_gap: Val,
290    /// Gap between columns. `Val::Auto` is treated as zero.
291    pub column_gap: Val,
292}
293
294impl Style {
295    /// The default style, usable in `const` context.
296    ///
297    /// These values match CSS/`taffy` defaults, which are not all "zero": a flex
298    /// container laid out as a row, `flex_shrink` of `1.0`, `auto` sizes and
299    /// `auto` inset, but *zero* margin/padding/border/gap.
300    pub const DEFAULT: Style = Style {
301        display: Display::Flex,
302        position_type: PositionType::Relative,
303        inset: UiRect::AUTO,
304
305        width: Val::Auto,
306        height: Val::Auto,
307        min_width: Val::Auto,
308        min_height: Val::Auto,
309        max_width: Val::Auto,
310        max_height: Val::Auto,
311        aspect_ratio: None,
312
313        margin: UiRect::ZERO,
314        padding: UiRect::ZERO,
315        border: UiRect::ZERO,
316
317        align_items: None,
318        align_self: None,
319        align_content: None,
320        justify_content: None,
321
322        flex_direction: FlexDirection::Row,
323        flex_wrap: FlexWrap::NoWrap,
324        flex_grow: 0.0,
325        flex_shrink: 1.0,
326        flex_basis: Val::Auto,
327
328        row_gap: Val::ZERO,
329        column_gap: Val::ZERO,
330    };
331}
332
333impl Default for Style {
334    fn default() -> Self {
335        Self::DEFAULT
336    }
337}
338
339/// Computed layout of a UI element, written back each frame by the layout system.
340///
341/// `size` is the element's width/height and `position` is its top-left corner,
342/// both in window pixel coordinates.
343#[derive(Clone, Copy, Debug, PartialEq)]
344pub struct Node {
345    /// Computed width and height in pixels.
346    pub size: Vec2,
347    /// Computed top-left position in window pixel coordinates.
348    pub position: Vec2,
349}
350
351impl Default for Node {
352    fn default() -> Self {
353        Self {
354            size: Vec2::ZERO,
355            position: Vec2::ZERO,
356        }
357    }
358}
359
360/// Current pointer interaction state of a UI element, updated each frame by the
361/// interaction system.
362#[derive(Clone, Copy, Debug, PartialEq, Eq)]
363#[derive(Default)]
364#[non_exhaustive]
365pub enum Interaction {
366    /// The pointer is neither over nor pressing the element.
367    #[default]
368    None,
369    /// The pointer is over the element but not pressed.
370    Hovered,
371    /// The pointer is over the element and the primary button is held.
372    Pressed,
373}
374
375
376/// Fill color of a UI element, stored as a linear RGBA vector with each
377/// channel in the `0.0..=1.0` range.
378#[derive(Clone, Copy, Debug, PartialEq)]
379pub struct BackgroundColor(pub Vec4);
380
381impl Default for BackgroundColor {
382    fn default() -> Self {
383        Self(Vec4::new(1.0, 1.0, 1.0, 1.0))
384    }
385}
386
387/// Marker component for the root of a UI tree.
388#[derive(Clone, Copy, Debug, PartialEq, Eq)]
389pub struct UiRoot;
390gizmo_core::impl_component!(Style, Node, Interaction, BackgroundColor, UiRoot);
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    /// `Style` must be `Send + Sync` because it is an ECS component
397    /// (`Component: 'static + Send + Sync + Clone`).
398    ///
399    /// Before the A3-followup change this was only true thanks to two
400    /// hand-written `unsafe impl`s, because the component stored a
401    /// `taffy::style::Style` by value and that type is structurally `!Send`.
402    /// The POD `Style` gets both auto traits derived, so those `unsafe impl`s
403    /// are gone — this test now asserts a property the compiler proves rather
404    /// than one we asserted by hand.
405    #[test]
406    fn style_is_send_and_sync_without_unsafe() {
407        fn assert_send_sync<T: Send + Sync>() {}
408        assert_send_sync::<Style>();
409        assert_send_sync::<Val>();
410        assert_send_sync::<UiRect>();
411    }
412
413    /// `Style` is plain old data — it must stay `Copy`, with no heap ownership,
414    /// no interior mutability and no pointers. `Copy` is the cheapest available
415    /// proof of that: it stops compiling the moment a `Vec`, `Rc`, `String` or
416    /// raw pointer field is added, which is exactly the class of change that
417    /// would put an `unsafe impl` back into this file.
418    #[test]
419    fn style_is_copy_plain_old_data() {
420        fn assert_copy<T: Copy>() {}
421        assert_copy::<Style>();
422        let a = Style { width: Val::Px(3.0), ..Default::default() };
423        let b = a; // copy, not move
424        assert_eq!(a, b);
425        assert_eq!(a.width, Val::Px(3.0));
426    }
427
428    /// The defaults are a contract: the bundles spawn `Style::default()`, so a
429    /// drift here silently changes the layout of every element that does not
430    /// set a field. In particular the non-zero ones (flex row, `flex_shrink`
431    /// 1.0, `auto` sizes/inset but zero margin/padding/gap) are CSS defaults,
432    /// not the `Default::default()` of each field type.
433    #[test]
434    fn defaults_are_css_defaults_not_zero() {
435        let s = Style::default();
436        assert_eq!(s, Style::DEFAULT);
437        assert_eq!(s.display, Display::Flex);
438        assert_eq!(s.position_type, PositionType::Relative);
439        assert_eq!(s.flex_direction, FlexDirection::Row);
440        assert_eq!(s.flex_wrap, FlexWrap::NoWrap);
441        assert_eq!(s.flex_grow, 0.0);
442        assert_eq!(s.flex_shrink, 1.0, "CSS default is 1.0; 0.0 would disable shrinking");
443        assert_eq!(s.flex_basis, Val::Auto);
444        assert_eq!(s.width, Val::Auto);
445        assert_eq!(s.aspect_ratio, None);
446        // `inset` defaults to auto, the other three rects to zero. An all-auto
447        // margin means CSS auto margins (centring), which is NOT the default.
448        assert_eq!(s.inset, UiRect::AUTO);
449        assert_eq!(s.margin, UiRect::ZERO);
450        assert_eq!(s.padding, UiRect::ZERO);
451        assert_eq!(s.border, UiRect::ZERO);
452        assert_eq!(s.row_gap, Val::ZERO);
453        assert_eq!(s.column_gap, Val::ZERO);
454    }
455
456    /// `UiRect::default()` is ZERO, not AUTO, even though `Val::default()` is
457    /// `Auto`. The two differ on purpose (CSS margin/padding/border start at
458    /// zero) and the difference is documented on both types, so pin it.
459    #[test]
460    fn ui_rect_default_is_zero_while_val_default_is_auto() {
461        assert_eq!(Val::default(), Val::Auto);
462        assert_eq!(UiRect::default(), UiRect::ZERO);
463        assert_ne!(UiRect::default(), UiRect::AUTO);
464        assert_eq!(
465            UiRect::axes(Val::Px(4.0), Val::Percent(10.0)),
466            UiRect::new(Val::Px(4.0), Val::Px(4.0), Val::Percent(10.0), Val::Percent(10.0))
467        );
468    }
469}