Skip to main content

densa_ui/
style.rs

1//! How a widget is sized and how it places its children: flexbox, in logical pixels.
2
3use taffy::style::{
4    AlignContent, AlignItems, Dimension, Display, FlexDirection, FlexWrap, LengthPercentage,
5    LengthPercentageAuto, Overflow as TaffyOverflow, Position as TaffyPosition,
6};
7
8/// A length along one axis.
9#[derive(Debug, Clone, Copy, PartialEq, Default)]
10#[non_exhaustive]
11pub enum Length {
12    /// Decided by layout: the content's size, or what flexing gives.
13    #[default]
14    Auto,
15    /// Logical pixels, multiplied by the UI's scale factor.
16    Px(f32),
17    /// A share of the parent's size: `0.5` is half.
18    Fraction(f32),
19}
20
21/// One value per side of a box.
22#[derive(Debug, Clone, Copy, PartialEq, Default)]
23pub struct Edges<T> {
24    /// Left side.
25    pub left: T,
26    /// Top side.
27    pub top: T,
28    /// Right side.
29    pub right: T,
30    /// Bottom side.
31    pub bottom: T,
32}
33
34impl<T: Copy> Edges<T> {
35    /// `value` on every side.
36    pub const fn all(value: T) -> Self {
37        Self {
38            left: value,
39            top: value,
40            right: value,
41            bottom: value,
42        }
43    }
44
45    /// `horizontal` on the left and right, `vertical` on the top and bottom.
46    pub const fn symmetric(horizontal: T, vertical: T) -> Self {
47        Self {
48            left: horizontal,
49            top: vertical,
50            right: horizontal,
51            bottom: vertical,
52        }
53    }
54}
55
56/// The axis children are laid out along.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58#[non_exhaustive]
59pub enum Direction {
60    /// Left to right.
61    #[default]
62    Row,
63    /// Top to bottom.
64    Column,
65}
66
67/// Where items sit across the main axis (or one item, as [`Style::align_self`]).
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
69#[non_exhaustive]
70pub enum Align {
71    /// Filling the cross axis.
72    #[default]
73    Stretch,
74    /// At the start.
75    Start,
76    /// Centered.
77    Center,
78    /// At the end.
79    End,
80}
81
82/// How free space along the main axis is shared out.
83#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
84#[non_exhaustive]
85pub enum Justify {
86    /// Items packed at the start.
87    #[default]
88    Start,
89    /// Items centered.
90    Center,
91    /// Items packed at the end.
92    End,
93    /// Equal space between items, none at the ends.
94    SpaceBetween,
95    /// Equal space around every item.
96    SpaceAround,
97}
98
99/// What happens to content larger than the widget.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
101#[non_exhaustive]
102pub enum Overflow {
103    /// Shown outside the widget.
104    #[default]
105    Visible,
106    /// Cut at the widget's edges.
107    Clip,
108    /// Cut at the edges and scrollable: its size does not grow the widget.
109    Scroll,
110}
111
112/// How a widget takes part in its parent's layout.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
114#[non_exhaustive]
115pub enum Position {
116    /// In the flow of its siblings.
117    #[default]
118    InFlow,
119    /// Placed by [`Style::inset`] against the parent, on top of its siblings.
120    Absolute,
121}
122
123/// A widget's layout style: its size, spacing, and how it lays out its children (flexbox).
124/// Lengths are in logical pixels.
125///
126/// ```
127/// use densa_ui::{Align, Length, Style};
128///
129/// let toolbar = Style::row().gap(4.0).padding(6.0).align_items(Align::Center);
130/// let content = Style::column().grow(1.0).height(Length::Auto);
131/// # let _ = (toolbar, content);
132/// ```
133///
134/// The default is [`Style::row`]: CSS's defaults (`flex-shrink: 1`, items stretched).
135///
136/// As in CSS, a widget is never laid out smaller than its content unless its minimum size
137/// says so (`min-width: auto`): a region meant to shrink and scroll its content, and every
138/// container between it and the window, needs `min_size(Length::Px(0.0), Length::Px(0.0))`
139/// (or `Overflow::Clip` or `Overflow::Scroll`, which imply it for the widget itself).
140#[derive(Debug, Clone, PartialEq)]
141#[non_exhaustive]
142pub struct Style {
143    /// Not shown and taking no space.
144    pub hidden: bool,
145    /// The axis children are laid out along.
146    pub direction: Direction,
147    /// Whether children wrap onto more lines when they do not fit.
148    pub wrap: bool,
149    /// Width.
150    pub width: Length,
151    /// Height.
152    pub height: Length,
153    /// Smallest width.
154    pub min_width: Length,
155    /// Smallest height.
156    pub min_height: Length,
157    /// Largest width.
158    pub max_width: Length,
159    /// Largest height.
160    pub max_height: Length,
161    /// Share of free space along the parent's main axis.
162    pub grow: f32,
163    /// Share of missing space given up when the parent is too small.
164    pub shrink: f32,
165    /// Size along the parent's main axis before growing or shrinking.
166    pub basis: Length,
167    /// Space between the edges and the content.
168    pub padding: Edges<f32>,
169    /// Space outside the edges; `Auto` takes free space (pushing siblings away).
170    pub margin: Edges<Length>,
171    /// Space between children.
172    pub gap: f32,
173    /// Where children sit across the main axis.
174    pub align_items: Align,
175    /// Where this widget sits across its parent's main axis, instead of the parent's
176    /// `align_items`.
177    pub align_self: Option<Align>,
178    /// How free space along the main axis is shared out.
179    pub justify: Justify,
180    /// In the flow of its siblings, or placed by `inset`.
181    pub position: Position,
182    /// Distances from the parent's edges, for [`Position::Absolute`].
183    pub inset: Edges<Length>,
184    /// What happens to content larger than the widget.
185    pub overflow: Overflow,
186    /// Width divided by height, kept when one of them is decided by layout.
187    pub aspect_ratio: Option<f32>,
188}
189
190impl Default for Style {
191    fn default() -> Self {
192        Self {
193            hidden: false,
194            direction: Direction::Row,
195            wrap: false,
196            width: Length::Auto,
197            height: Length::Auto,
198            min_width: Length::Auto,
199            min_height: Length::Auto,
200            max_width: Length::Auto,
201            max_height: Length::Auto,
202            grow: 0.0,
203            shrink: 1.0,
204            basis: Length::Auto,
205            padding: Edges::all(0.0),
206            margin: Edges::all(Length::Px(0.0)),
207            gap: 0.0,
208            align_items: Align::Stretch,
209            align_self: None,
210            justify: Justify::Start,
211            position: Position::InFlow,
212            inset: Edges::all(Length::Auto),
213            overflow: Overflow::Visible,
214            aspect_ratio: None,
215        }
216    }
217}
218
219impl Style {
220    /// Children left to right.
221    pub fn row() -> Self {
222        Self::default()
223    }
224
225    /// Children top to bottom.
226    pub fn column() -> Self {
227        Self {
228            direction: Direction::Column,
229            ..Self::row()
230        }
231    }
232
233    /// The style with `width`.
234    #[must_use]
235    pub fn width(mut self, width: Length) -> Self {
236        self.width = width;
237        self
238    }
239
240    /// The style with `height`.
241    #[must_use]
242    pub fn height(mut self, height: Length) -> Self {
243        self.height = height;
244        self
245    }
246
247    /// The style with a fixed size in logical pixels.
248    #[must_use]
249    pub fn size(self, width: f32, height: f32) -> Self {
250        self.width(Length::Px(width)).height(Length::Px(height))
251    }
252
253    /// The style with smallest sizes.
254    #[must_use]
255    pub fn min_size(mut self, width: Length, height: Length) -> Self {
256        self.min_width = width;
257        self.min_height = height;
258        self
259    }
260
261    /// The style with largest sizes.
262    #[must_use]
263    pub fn max_size(mut self, width: Length, height: Length) -> Self {
264        self.max_width = width;
265        self.max_height = height;
266        self
267    }
268
269    /// The style taking a `grow` share of free space.
270    #[must_use]
271    pub fn grow(mut self, grow: f32) -> Self {
272        self.grow = grow;
273        self
274    }
275
276    /// The style giving up a `shrink` share of missing space.
277    #[must_use]
278    pub fn shrink(mut self, shrink: f32) -> Self {
279        self.shrink = shrink;
280        self
281    }
282
283    /// The style with a size along the main axis before growing or shrinking.
284    #[must_use]
285    pub fn basis(mut self, basis: Length) -> Self {
286        self.basis = basis;
287        self
288    }
289
290    /// The style with `padding` on every side.
291    #[must_use]
292    pub fn padding(mut self, padding: f32) -> Self {
293        self.padding = Edges::all(padding);
294        self
295    }
296
297    /// The style with these paddings.
298    #[must_use]
299    pub fn padding_edges(mut self, padding: Edges<f32>) -> Self {
300        self.padding = padding;
301        self
302    }
303
304    /// The style with these margins.
305    #[must_use]
306    pub fn margin(mut self, margin: Edges<Length>) -> Self {
307        self.margin = margin;
308        self
309    }
310
311    /// The style with `gap` between children.
312    #[must_use]
313    pub fn gap(mut self, gap: f32) -> Self {
314        self.gap = gap;
315        self
316    }
317
318    /// The style placing children across the main axis.
319    #[must_use]
320    pub fn align_items(mut self, align: Align) -> Self {
321        self.align_items = align;
322        self
323    }
324
325    /// The style placing this widget across its parent's main axis.
326    #[must_use]
327    pub fn align_self(mut self, align: Align) -> Self {
328        self.align_self = Some(align);
329        self
330    }
331
332    /// The style sharing out free space along the main axis.
333    #[must_use]
334    pub fn justify(mut self, justify: Justify) -> Self {
335        self.justify = justify;
336        self
337    }
338
339    /// The style placed at `inset` from the parent's edges, over its siblings.
340    #[must_use]
341    pub fn absolute(mut self, inset: Edges<Length>) -> Self {
342        self.position = Position::Absolute;
343        self.inset = inset;
344        self
345    }
346
347    /// The style with `overflow`.
348    #[must_use]
349    pub fn overflow(mut self, overflow: Overflow) -> Self {
350        self.overflow = overflow;
351        self
352    }
353
354    /// The style wrapping children onto more lines, or not.
355    #[must_use]
356    pub fn wrap(mut self, wrap: bool) -> Self {
357        self.wrap = wrap;
358        self
359    }
360
361    /// The style hidden (taking no space), or not.
362    #[must_use]
363    pub fn hidden(mut self, hidden: bool) -> Self {
364        self.hidden = hidden;
365        self
366    }
367
368    /// The style keeping width divided by height at `ratio`.
369    #[must_use]
370    pub fn aspect_ratio(mut self, ratio: f32) -> Self {
371        self.aspect_ratio = Some(ratio);
372        self
373    }
374
375    /// The layout engine's style, in physical pixels (`scale` per logical pixel).
376    pub(crate) fn to_taffy(&self, scale: f32) -> taffy::Style {
377        let px = |v: f32| v * scale;
378        let dimension = |l: Length| match l {
379            Length::Auto => Dimension::auto(),
380            Length::Px(v) => Dimension::length(px(v)),
381            Length::Fraction(f) => Dimension::percent(f),
382        };
383        let or_auto = |l: Length| match l {
384            Length::Auto => LengthPercentageAuto::auto(),
385            Length::Px(v) => LengthPercentageAuto::length(px(v)),
386            Length::Fraction(f) => LengthPercentageAuto::percent(f),
387        };
388        let fixed = |v: f32| LengthPercentage::length(px(v));
389        let overflow = match self.overflow {
390            Overflow::Visible => TaffyOverflow::Visible,
391            Overflow::Clip => TaffyOverflow::Clip,
392            Overflow::Scroll => TaffyOverflow::Scroll,
393        };
394        taffy::Style {
395            display: if self.hidden {
396                Display::None
397            } else {
398                Display::Flex
399            },
400            flex_direction: match self.direction {
401                Direction::Row => FlexDirection::Row,
402                Direction::Column => FlexDirection::Column,
403            },
404            flex_wrap: if self.wrap {
405                FlexWrap::Wrap
406            } else {
407                FlexWrap::NoWrap
408            },
409            size: taffy::Size {
410                width: dimension(self.width),
411                height: dimension(self.height),
412            },
413            min_size: taffy::Size {
414                width: or_auto(self.min_width),
415                height: or_auto(self.min_height),
416            },
417            max_size: taffy::Size {
418                width: or_auto(self.max_width),
419                height: or_auto(self.max_height),
420            },
421            flex_grow: self.grow,
422            flex_shrink: self.shrink,
423            flex_basis: dimension(self.basis),
424            padding: taffy::Rect {
425                left: fixed(self.padding.left),
426                right: fixed(self.padding.right),
427                top: fixed(self.padding.top),
428                bottom: fixed(self.padding.bottom),
429            },
430            margin: taffy::Rect {
431                left: or_auto(self.margin.left),
432                right: or_auto(self.margin.right),
433                top: or_auto(self.margin.top),
434                bottom: or_auto(self.margin.bottom),
435            },
436            gap: taffy::Size {
437                width: fixed(self.gap),
438                height: fixed(self.gap),
439            },
440            align_items: align(self.align_items),
441            align_self: self.align_self.map(align),
442            justify_content: justify(self.justify),
443            align_content: AlignContent::START,
444            position: match self.position {
445                Position::InFlow => TaffyPosition::Relative,
446                Position::Absolute => TaffyPosition::Absolute,
447            },
448            inset: taffy::Rect {
449                left: or_auto(self.inset.left),
450                right: or_auto(self.inset.right),
451                top: or_auto(self.inset.top),
452                bottom: or_auto(self.inset.bottom),
453            },
454            overflow: taffy::Point {
455                x: overflow,
456                y: overflow,
457            },
458            scrollbar_width: 0.0,
459            aspect_ratio: self.aspect_ratio,
460            ..taffy::Style::default()
461        }
462    }
463}
464
465fn align(align: Align) -> AlignItems {
466    match align {
467        Align::Stretch => AlignItems::STRETCH,
468        Align::Start => AlignItems::FLEX_START,
469        Align::Center => AlignItems::CENTER,
470        Align::End => AlignItems::FLEX_END,
471    }
472}
473
474fn justify(justify: Justify) -> AlignContent {
475    match justify {
476        Justify::Start => AlignContent::FLEX_START,
477        Justify::Center => AlignContent::CENTER,
478        Justify::End => AlignContent::FLEX_END,
479        Justify::SpaceBetween => AlignContent::SPACE_BETWEEN,
480        Justify::SpaceAround => AlignContent::SPACE_AROUND,
481    }
482}