panes 0.19.0

Renderer-agnostic layout engine with declarative ergonomics
Documentation
use std::sync::Arc;

use crate::builder::LayoutBuilder;
use crate::error::{ConstraintError, PaneError, TreeError};
use crate::layout::Layout;
use crate::panel::{fixed, grow};
use crate::validate::{check_f32_non_negative, float_invalid_to_constraint};

/// Collect an iterator of string-like items into an `Arc<[Arc<str>]>`.
pub(crate) fn collect_kinds(
    kinds: impl IntoIterator<Item = impl Into<Arc<str>>>,
) -> Arc<[Arc<str>]> {
    kinds.into_iter().map(Into::into).collect()
}

/// Build a single-panel layout. Shared by presets that degenerate when given one kind.
pub(crate) fn build_single(kind: Arc<str>) -> Result<Layout, PaneError> {
    let mut b = LayoutBuilder::new();
    b.row(|r| {
        r.panel(kind);
    })?;
    b.build()
}

/// Validate that at least one kind was provided.
pub(crate) fn validate_kinds(kinds: &[Arc<str>]) -> Result<(), PaneError> {
    match kinds.is_empty() {
        true => Err(PaneError::InvalidTree(TreeError::NoKinds)),
        false => Ok(()),
    }
}

/// Validate that an `f32` parameter is finite and non-negative.
pub(crate) fn validate_f32_param(name: &'static str, value: f32) -> Result<(), PaneError> {
    check_f32_non_negative(value)
        .map_err(|e| PaneError::InvalidConstraint(float_invalid_to_constraint(name, e)))
}

/// Validate that an `f32` share parameter is finite, non-negative, and at most `1.0`.
pub(crate) fn validate_share_param(name: &'static str, value: f32) -> Result<(), PaneError> {
    validate_f32_param(name, value)?;
    match value <= 1.0 {
        true => Ok(()),
        false => Err(PaneError::InvalidConstraint(ConstraintError::ExceedsOne(
            name,
        ))),
    }
}

/// Validate that `active` is within bounds.
pub(crate) fn validate_active(active: usize, len: usize) -> Result<(), PaneError> {
    match active >= len {
        true => Err(PaneError::InvalidTree(TreeError::ActiveOutOfBounds {
            active,
            len,
        })),
        false => Ok(()),
    }
}

/// Add panels where only the active one grows; the rest are hidden (fixed 0).
pub(crate) fn add_active_hidden_panels(
    ctx: &mut crate::ContainerCtx,
    kinds: &[Arc<str>],
    active: usize,
) {
    for (i, kind) in kinds.iter().enumerate() {
        let constraint = match i == active {
            true => grow(1.0),
            false => fixed(0.0),
        };
        ctx.panel_with(Arc::clone(kind), constraint);
    }
}

/// Build a flex style with the given direction, grow factor, and gap.
fn flex_style(direction: taffy::FlexDirection, flex_grow: f32, gap_px: f32) -> taffy::Style {
    let gap = match direction {
        taffy::FlexDirection::Row | taffy::FlexDirection::RowReverse => taffy::Size {
            width: taffy::LengthPercentage::length(gap_px),
            height: taffy::LengthPercentage::length(0.0),
        },
        _ => taffy::Size {
            width: taffy::LengthPercentage::length(0.0),
            height: taffy::LengthPercentage::length(gap_px),
        },
    };
    taffy::Style {
        flex_direction: direction,
        flex_grow,
        flex_basis: taffy::Dimension::length(0.0),
        flex_shrink: 1.0,
        gap,
        ..Default::default()
    }
}

/// A column-direction taffy style with a specific grow factor and gap.
pub(crate) fn col_style(flex_grow: f32, gap_px: f32) -> taffy::Style {
    flex_style(taffy::FlexDirection::Column, flex_grow, gap_px)
}

/// A row-direction taffy style with a specific grow factor and gap.
pub(crate) fn row_style(flex_grow: f32, gap_px: f32) -> taffy::Style {
    flex_style(taffy::FlexDirection::Row, flex_grow, gap_px)
}

/// Add one panel per kind with the given constraints.
pub(crate) fn add_panels(
    ctx: &mut crate::ContainerCtx,
    kinds: &[Arc<str>],
    constraints: crate::Constraints,
) {
    for kind in kinds {
        ctx.panel_with(Arc::clone(kind), constraints);
    }
}

// ---------------------------------------------------------------------------
// CSS Grid helpers (shared by dashboard, grid, columns presets)
// ---------------------------------------------------------------------------

use taffy::prelude::{fr, minmax, repeat};
use taffy::style::{GridTemplateComponent, MaxTrackSizingFunction, MinTrackSizingFunction};

use crate::strategy::GridColumnMode;

/// Convert a [`GridColumnMode`] to taffy grid template columns.
pub(crate) fn columns_to_taffy(columns: GridColumnMode) -> Vec<GridTemplateComponent<String>> {
    match columns {
        GridColumnMode::Fixed(n) => vec![fr(1.0); n],
        GridColumnMode::AutoFill { min_width } => vec![auto_repeat_track("auto-fill", min_width)],
        GridColumnMode::AutoFit { min_width } => vec![auto_repeat_track("auto-fit", min_width)],
    }
}

/// Build a `repeat(auto-fill|auto-fit, minmax(min_width, 1fr))` track.
pub(crate) fn auto_repeat_track(kind: &str, min_width: f32) -> GridTemplateComponent<String> {
    let track = minmax(
        MinTrackSizingFunction::length(min_width),
        MaxTrackSizingFunction::fr(1.0),
    );
    repeat(kind, vec![track])
}

/// Build a `Display::Grid` root style for CSS Grid presets.
pub(crate) fn simple_grid_style(mode: GridColumnMode, gap: f32, auto_rows: bool) -> taffy::Style {
    let gap_len = taffy::LengthPercentage::length(gap);
    let grid_auto_rows = match auto_rows {
        true => vec![minmax(
            MinTrackSizingFunction::auto(),
            MaxTrackSizingFunction::auto(),
        )],
        false => vec![fr(1.0)],
    };
    taffy::Style {
        display: taffy::Display::Grid,
        size: taffy::Size {
            width: taffy::Dimension::percent(1.0),
            height: taffy::Dimension::percent(1.0),
        },
        grid_template_columns: columns_to_taffy(mode),
        grid_auto_rows,
        gap: taffy::Size {
            width: gap_len,
            height: gap_len,
        },
        ..Default::default()
    }
}

/// Validate that a grid column mode is valid (non-zero columns, positive finite min width).
///
/// Reuses existing dashboard error variants for semantic consistency.
pub(crate) fn validate_grid_columns(columns: GridColumnMode) -> Result<(), PaneError> {
    match columns {
        GridColumnMode::Fixed(0) => Err(PaneError::InvalidTree(TreeError::DashboardNoColumns)),
        GridColumnMode::AutoFill { min_width } | GridColumnMode::AutoFit { min_width }
            if !(min_width > 0.0 && min_width.is_finite()) =>
        {
            Err(PaneError::InvalidTree(TreeError::DashboardMinWidthInvalid))
        }
        _ => Ok(()),
    }
}

/// Validate that a grid span fits in a u16 (taffy grid placement limit).
pub(crate) fn validate_grid_span(span: crate::strategy::CardSpan) -> Result<(), PaneError> {
    match span {
        crate::strategy::CardSpan::Columns(n) => {
            u16::try_from(n).map_err(|_| {
                PaneError::InvalidConstraint(crate::error::ConstraintError::GridSpanOverflow(n))
            })?;
            Ok(())
        }
        crate::strategy::CardSpan::FullWidth => Ok(()),
    }
}

/// Build a taffy style for a grid item with column span placement.
///
/// Callers must validate the span via `validate_grid_span` before calling.
/// The builder validates at construction; the compiler processes pre-validated nodes.
pub(crate) fn grid_item_style(span: crate::strategy::CardSpan) -> taffy::Style {
    use crate::strategy::CardSpan;
    use taffy::prelude::TaffyGridLine;

    let grid_column = match span {
        CardSpan::FullWidth => taffy::Line {
            start: taffy::GridPlacement::from_line_index(1),
            end: taffy::GridPlacement::from_line_index(-1),
        },
        CardSpan::Columns(n) => {
            // Builder validated this fits in u16 at construction time.
            let span_u16 = n as u16;
            taffy::Line {
                start: taffy::GridPlacement::Auto,
                end: taffy::GridPlacement::Span(span_u16),
            }
        }
    };
    taffy::Style {
        grid_column,
        ..Default::default()
    }
}

// Macro lives here because it references preset-specific builder methods.
macro_rules! impl_preset {
    ($Type:ty, runtime($kinds_field:ident, |$this:ident| $($strategy_tokens:tt)+)) => {
        impl $Type {
            /// Consume the builder and produce a [`crate::runtime::LayoutRuntime`].
            pub fn into_runtime(self) -> Result<$crate::runtime::LayoutRuntime, $crate::PaneError> {
                let $this = self;
                let strategy = $($strategy_tokens)+;
                $crate::runtime::LayoutRuntime::from_strategy(strategy, &$this.$kinds_field)
            }
        }

        super::impl_preset!($Type);
    };
    ($Type:ty) => {
        impl $Type {
            /// Build and resolve the preset at the given viewport size.
            pub fn resolve(
                &self,
                width: f32,
                height: f32,
            ) -> Result<$crate::ResolvedLayout, $crate::PaneError> {
                self.build()?.resolve(width, height)
            }
        }

        impl TryFrom<$Type> for $crate::Layout {
            type Error = $crate::PaneError;

            fn try_from(preset: $Type) -> Result<Self, Self::Error> {
                preset.build()
            }
        }
    };
}

pub(crate) use impl_preset;