Skip to main content

slt/layout/
command.rs

1use super::*;
2
3/// Main axis direction for a container's children.
4#[non_exhaustive]
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum Direction {
7    /// Lay out children horizontally (left to right).
8    Row,
9    /// Lay out children vertically (top to bottom).
10    Column,
11}
12
13/// Arguments for [`Command::BeginContainer`].
14///
15/// Boxed inside the variant so that the surrounding `Command` enum stays
16/// small — a frame may contain hundreds of commands, and most variants
17/// (for example `EndContainer`) carry no payload.
18#[derive(Debug, Clone)]
19pub(crate) struct BeginContainerArgs {
20    pub direction: Direction,
21    /// Signed inter-child gap (negative = overlap). See
22    /// [`crate::ContainerBuilder::gap_overlap`] (#222).
23    pub gap: i32,
24    pub align: Align,
25    pub align_self: Option<Align>,
26    pub justify: Justify,
27    pub border: Option<Border>,
28    pub border_sides: BorderSides,
29    pub border_style: Style,
30    pub bg_color: Option<Color>,
31    pub padding: Padding,
32    pub margin: Margin,
33    pub constraints: Constraints,
34    pub title: Option<(String, Style)>,
35    pub grow: u16,
36    pub group_name: Option<std::sync::Arc<str>>,
37}
38
39/// Arguments for [`Command::BeginScrollable`].
40///
41/// Boxed for the same reason as [`BeginContainerArgs`] — keeps the
42/// `Command` enum from being dragged up to the width of this payload.
43///
44/// `direction` selects the scroll axis (#247): `Direction::Column` scrolls
45/// vertically (the historic default), `Direction::Row` scrolls horizontally.
46/// Both vertical and horizontal offsets are carried so the tree builder can
47/// apply the one matching `direction` without re-querying state mid-frame.
48#[derive(Debug, Clone)]
49pub(crate) struct BeginScrollableArgs {
50    pub grow: u16,
51    /// Scroll axis: `Column` => vertical scroll, `Row` => horizontal (#247).
52    pub direction: Direction,
53    pub border: Option<Border>,
54    pub border_sides: BorderSides,
55    pub border_style: Style,
56    /// Background color (dark-mode resolved). Fixes #142.
57    pub bg_color: Option<Color>,
58    /// Main-axis child alignment. Fixes #142.
59    pub align: Align,
60    /// Cross-axis self alignment. Fixes #142.
61    pub align_self: Option<Align>,
62    /// Main-axis justification. Fixes #142.
63    pub justify: Justify,
64    /// Signed gap between children in cells (negative = overlap, #222).
65    /// Fixes #142.
66    pub gap: i32,
67    pub padding: Padding,
68    pub margin: Margin,
69    pub constraints: Constraints,
70    pub title: Option<(String, Style)>,
71    /// Vertical scroll offset in rows (used when `direction == Column`).
72    pub scroll_offset: u32,
73    /// Horizontal scroll offset in columns (used when `direction == Row`, #247).
74    pub scroll_offset_x: u32,
75    /// Group name for hover/focus registration. Fixes #141.
76    pub group_name: Option<std::sync::Arc<str>>,
77}
78
79#[derive(Debug, Clone)]
80pub(crate) enum Command {
81    Text {
82        content: String,
83        cursor_offset: Option<usize>,
84        style: Style,
85        grow: u16,
86        align: Align,
87        wrap: bool,
88        truncate: bool,
89        margin: Margin,
90        constraints: Constraints,
91    },
92    BeginContainer(Box<BeginContainerArgs>),
93    BeginScrollable(Box<BeginScrollableArgs>),
94    Link {
95        text: String,
96        url: String,
97        style: Style,
98        wrap: bool,
99        margin: Margin,
100        constraints: Constraints,
101    },
102    RichText {
103        segments: Vec<(String, Style)>,
104        wrap: bool,
105        align: Align,
106        margin: Margin,
107        constraints: Constraints,
108    },
109    EndContainer,
110    BeginOverlay {
111        modal: bool,
112    },
113    EndOverlay,
114    Spacer {
115        grow: u16,
116    },
117    FocusMarker(usize),
118    InteractionMarker(usize),
119    /// Marks the next container / scrollable as opt-in flex-shrink.
120    ///
121    /// Pushed by [`crate::context::ContainerBuilder::shrink`] just before the
122    /// matching `BeginContainer` / `BeginScrollable`. Consumed by
123    /// `build_children` like [`Command::FocusMarker`] (buffered into
124    /// `pending_shrink`, applied to the next built [`super::tree::LayoutNode`]).
125    /// Closes #161.
126    ShrinkMarker,
127    /// Marks the next container as a wrapping (multi-line) row, carrying the
128    /// signed cross-axis (between-line) gap.
129    ///
130    /// Pushed by [`crate::context::ContainerBuilder::wrap`] just before the
131    /// matching `BeginContainer` / `BeginScrollable`. Consumed by
132    /// `build_children` like [`Command::ShrinkMarker`] (buffered into
133    /// `pending_wrap`, applied to the next built
134    /// [`super::tree::LayoutNode`]). On a `Row` container the child layout
135    /// flows children onto subsequent lines on main-axis overflow, with the
136    /// payload as the between-line gap; on a `Column` container it is a
137    /// documented no-op. Kept a scalar variant so the `Command` enum stays
138    /// small. Closes #258.
139    WrapMarker(i32),
140    /// Sets the flex-basis (initial main-axis size, in cells) on the next
141    /// container.
142    ///
143    /// Pushed by [`crate::context::ContainerBuilder::basis`] just before the
144    /// matching `BeginContainer` / `BeginScrollable`. Buffered into
145    /// `pending_basis` and applied to the next built
146    /// [`super::tree::LayoutNode::flex_basis`]. A scalar variant so the
147    /// `Command` enum stays small. Closes #258.
148    BasisMarker(u32),
149    RawDraw {
150        draw_id: usize,
151        constraints: Constraints,
152        grow: u16,
153        margin: Margin,
154    },
155}
156
157#[cfg(test)]
158mod size_tests {
159    use super::Command;
160
161    /// Regression guard for the `Command` enum size.
162    ///
163    /// A frame may push hundreds of `Command` values into a single `Vec`,
164    /// so every byte in the enum variant-union multiplies across the frame.
165    /// Fat variants (`BeginContainer`, `BeginScrollable`) are boxed to keep
166    /// the common 0-payload variants (e.g. `EndContainer`) cheap.
167    ///
168    /// The current ceiling reflects the largest remaining inline variant
169    /// (`Text`, which carries a `String` + `Constraints` + `Style` + small
170    /// scalars). If this test fires after a refactor, either box the new
171    /// fat variant or bump this bound with justification.
172    #[test]
173    fn command_enum_size_is_bounded() {
174        const MAX_BYTES: usize = 128;
175        let actual = std::mem::size_of::<Command>();
176        assert!(
177            actual <= MAX_BYTES,
178            "Command enum grew to {actual} bytes (limit {MAX_BYTES}); \
179             consider boxing the new fat variant"
180        );
181    }
182}