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}