superlighttui 0.21.0

Super Light TUI - A lightweight, ergonomic terminal UI library
Documentation
use super::*;

/// Main axis direction for a container's children.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
    /// Lay out children horizontally (left to right).
    Row,
    /// Lay out children vertically (top to bottom).
    Column,
}

/// Arguments for [`Command::BeginContainer`].
///
/// Boxed inside the variant so that the surrounding `Command` enum stays
/// small — a frame may contain hundreds of commands, and most variants
/// (for example `EndContainer`) carry no payload.
#[derive(Debug, Clone)]
pub(crate) struct BeginContainerArgs {
    pub direction: Direction,
    /// Signed inter-child gap (negative = overlap). See
    /// [`crate::ContainerBuilder::gap_overlap`] (#222).
    pub gap: i32,
    pub align: Align,
    pub align_self: Option<Align>,
    pub justify: Justify,
    pub border: Option<Border>,
    pub border_sides: BorderSides,
    pub border_style: Style,
    pub bg_color: Option<Color>,
    pub padding: Padding,
    pub margin: Margin,
    pub constraints: Constraints,
    pub title: Option<(String, Style)>,
    pub grow: u16,
    pub group_name: Option<std::sync::Arc<str>>,
}

/// Arguments for [`Command::BeginScrollable`].
///
/// Boxed for the same reason as [`BeginContainerArgs`] — keeps the
/// `Command` enum from being dragged up to the width of this payload.
///
/// `direction` selects the scroll axis (#247): `Direction::Column` scrolls
/// vertically (the historic default), `Direction::Row` scrolls horizontally.
/// Both vertical and horizontal offsets are carried so the tree builder can
/// apply the one matching `direction` without re-querying state mid-frame.
#[derive(Debug, Clone)]
pub(crate) struct BeginScrollableArgs {
    pub grow: u16,
    /// Scroll axis: `Column` => vertical scroll, `Row` => horizontal (#247).
    pub direction: Direction,
    pub border: Option<Border>,
    pub border_sides: BorderSides,
    pub border_style: Style,
    /// Background color (dark-mode resolved). Fixes #142.
    pub bg_color: Option<Color>,
    /// Main-axis child alignment. Fixes #142.
    pub align: Align,
    /// Cross-axis self alignment. Fixes #142.
    pub align_self: Option<Align>,
    /// Main-axis justification. Fixes #142.
    pub justify: Justify,
    /// Signed gap between children in cells (negative = overlap, #222).
    /// Fixes #142.
    pub gap: i32,
    pub padding: Padding,
    pub margin: Margin,
    pub constraints: Constraints,
    pub title: Option<(String, Style)>,
    /// Vertical scroll offset in rows (used when `direction == Column`).
    pub scroll_offset: u32,
    /// Horizontal scroll offset in columns (used when `direction == Row`, #247).
    pub scroll_offset_x: u32,
    /// Group name for hover/focus registration. Fixes #141.
    pub group_name: Option<std::sync::Arc<str>>,
}

#[derive(Debug, Clone)]
pub(crate) enum Command {
    Text {
        content: String,
        cursor_offset: Option<usize>,
        style: Style,
        grow: u16,
        align: Align,
        wrap: bool,
        truncate: bool,
        margin: Margin,
        constraints: Constraints,
    },
    BeginContainer(Box<BeginContainerArgs>),
    BeginScrollable(Box<BeginScrollableArgs>),
    Link {
        text: String,
        url: String,
        style: Style,
        margin: Margin,
        constraints: Constraints,
    },
    RichText {
        segments: Vec<(String, Style)>,
        wrap: bool,
        align: Align,
        margin: Margin,
        constraints: Constraints,
    },
    EndContainer,
    BeginOverlay {
        modal: bool,
    },
    EndOverlay,
    Spacer {
        grow: u16,
    },
    FocusMarker(usize),
    InteractionMarker(usize),
    /// Marks the next container / scrollable as opt-in flex-shrink.
    ///
    /// Pushed by [`crate::context::ContainerBuilder::shrink`] just before the
    /// matching `BeginContainer` / `BeginScrollable`. Consumed by
    /// `build_children` like [`Command::FocusMarker`] (buffered into
    /// `pending_shrink`, applied to the next built [`super::tree::LayoutNode`]).
    /// Closes #161.
    ShrinkMarker,
    /// Marks the next container as a wrapping (multi-line) row, carrying the
    /// signed cross-axis (between-line) gap.
    ///
    /// Pushed by [`crate::context::ContainerBuilder::wrap`] just before the
    /// matching `BeginContainer` / `BeginScrollable`. Consumed by
    /// `build_children` like [`Command::ShrinkMarker`] (buffered into
    /// `pending_wrap`, applied to the next built
    /// [`super::tree::LayoutNode`]). On a `Row` container the child layout
    /// flows children onto subsequent lines on main-axis overflow, with the
    /// payload as the between-line gap; on a `Column` container it is a
    /// documented no-op. Kept a scalar variant so the `Command` enum stays
    /// small. Closes #258.
    WrapMarker(i32),
    /// Sets the flex-basis (initial main-axis size, in cells) on the next
    /// container.
    ///
    /// Pushed by [`crate::context::ContainerBuilder::basis`] just before the
    /// matching `BeginContainer` / `BeginScrollable`. Buffered into
    /// `pending_basis` and applied to the next built
    /// [`super::tree::LayoutNode::flex_basis`]. A scalar variant so the
    /// `Command` enum stays small. Closes #258.
    BasisMarker(u32),
    RawDraw {
        draw_id: usize,
        constraints: Constraints,
        grow: u16,
        margin: Margin,
    },
}

#[cfg(test)]
mod size_tests {
    use super::Command;

    /// Regression guard for the `Command` enum size.
    ///
    /// A frame may push hundreds of `Command` values into a single `Vec`,
    /// so every byte in the enum variant-union multiplies across the frame.
    /// Fat variants (`BeginContainer`, `BeginScrollable`) are boxed to keep
    /// the common 0-payload variants (e.g. `EndContainer`) cheap.
    ///
    /// The current ceiling reflects the largest remaining inline variant
    /// (`Text`, which carries a `String` + `Constraints` + `Style` + small
    /// scalars). If this test fires after a refactor, either box the new
    /// fat variant or bump this bound with justification.
    #[test]
    fn command_enum_size_is_bounded() {
        const MAX_BYTES: usize = 128;
        let actual = std::mem::size_of::<Command>();
        assert!(
            actual <= MAX_BYTES,
            "Command enum grew to {actual} bytes (limit {MAX_BYTES}); \
             consider boxing the new fat variant"
        );
    }
}