panes 0.19.0

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

use crate::builder::{ContainerCtx, Grid, GridCtx, LayoutBuilder};
use crate::compiler::compile;
use crate::error::PaneError;
use crate::preset::{PanelInputKind, PresetInfo};
use crate::resolver::ResolvedLayout;
use crate::tree::LayoutTree;

/// An immutable, validated layout ready for resolution.
pub struct Layout {
    tree: LayoutTree,
}

impl std::fmt::Debug for Layout {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Layout")
            .field("panel_count", &self.tree.panel_count())
            .finish()
    }
}

impl Layout {
    /// Create a `Layout` from a validated tree. Called by `LayoutBuilder::build()`.
    pub(crate) fn from_tree(tree: LayoutTree) -> Self {
        Self { tree }
    }

    /// Borrow the underlying tree for read-only traversal.
    pub fn tree(&self) -> &LayoutTree {
        &self.tree
    }

    /// How many panels the active window shows at once.
    pub fn window_panel_count(&self) -> usize {
        self.tree.window_panel_count()
    }

    /// Compile, compute, and resolve the layout at the given viewport size.
    pub fn resolve(&self, width: f32, height: f32) -> Result<ResolvedLayout, PaneError> {
        self.tree.resolve(width, height)
    }

    // -- Convenience constructors --

    /// Build a row layout from a closure.
    pub fn build_row(f: impl FnOnce(&mut ContainerCtx)) -> Result<Self, PaneError> {
        Self::build_axis(true, 0.0, f)
    }

    /// Build a column layout from a closure.
    pub fn build_col(f: impl FnOnce(&mut ContainerCtx)) -> Result<Self, PaneError> {
        Self::build_axis(false, 0.0, f)
    }

    /// Build a row layout with gap from a closure.
    pub fn build_row_gap(gap: f32, f: impl FnOnce(&mut ContainerCtx)) -> Result<Self, PaneError> {
        Self::build_axis(true, gap, f)
    }

    /// Build a column layout with gap from a closure.
    pub fn build_col_gap(gap: f32, f: impl FnOnce(&mut ContainerCtx)) -> Result<Self, PaneError> {
        Self::build_axis(false, gap, f)
    }

    /// Build a grid layout from a closure.
    pub fn build_grid(grid: Grid, f: impl FnOnce(&mut GridCtx)) -> Result<Self, PaneError> {
        let mut b = LayoutBuilder::new();
        b.grid(grid, f)?;
        b.build()
    }

    /// Equal-grow panels in a row, zero gap.
    pub fn row(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Result<Self, PaneError> {
        Self::equal_grow_axis(true, kinds)
    }

    /// Equal-grow panels in a column, zero gap.
    pub fn col(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> Result<Self, PaneError> {
        Self::equal_grow_axis(false, kinds)
    }

    /// Panels in a row with explicit constraints per panel.
    pub fn row_with(
        panels: impl IntoIterator<Item = (impl Into<Arc<str>>, crate::panel::Constraints)>,
    ) -> Result<Self, PaneError> {
        Self::constrained_axis(true, panels)
    }

    /// Panels in a column with explicit constraints per panel.
    pub fn col_with(
        panels: impl IntoIterator<Item = (impl Into<Arc<str>>, crate::panel::Constraints)>,
    ) -> Result<Self, PaneError> {
        Self::constrained_axis(false, panels)
    }

    fn build_axis(
        is_row: bool,
        gap: f32,
        f: impl FnOnce(&mut ContainerCtx),
    ) -> Result<Self, PaneError> {
        let mut b = LayoutBuilder::new();
        match is_row {
            true => b.row_gap(gap, f)?,
            false => b.col_gap(gap, f)?,
        }
        b.build()
    }

    fn equal_grow_axis(
        is_row: bool,
        kinds: impl IntoIterator<Item = impl Into<Arc<str>>>,
    ) -> Result<Self, PaneError> {
        let kinds = kinds.into_iter();
        Self::build_axis(is_row, 0.0, |ctx| {
            for kind in kinds {
                ctx.panel(kind.into());
            }
        })
    }

    fn constrained_axis(
        is_row: bool,
        panels: impl IntoIterator<Item = (impl Into<Arc<str>>, crate::panel::Constraints)>,
    ) -> Result<Self, PaneError> {
        let panels = panels.into_iter();
        Self::build_axis(is_row, 0.0, |ctx| {
            for (kind, constraints) in panels {
                ctx.panel_with(kind.into(), constraints);
            }
        })
    }

    /// Return metadata for all built-in presets, sorted alphabetically by name.
    pub fn presets() -> &'static [PresetInfo] {
        &crate::preset::catalog::PRESETS
    }

    /// Build a runtime from a built-in preset name and panel kinds.
    pub fn runtime_from_preset(
        name: &str,
        kinds: &[&str],
    ) -> Result<crate::runtime::LayoutRuntime, String> {
        let preset = Self::presets()
            .iter()
            .find(|preset| preset.name == name)
            .ok_or_else(|| format!("unknown preset: {name}"))?;

        match preset.input {
            PanelInputKind::DynamicList => Self::dynamic_runtime_from_preset(preset.name, kinds),
            PanelInputKind::FixedSlots => Self::fixed_runtime_from_preset(preset.name, kinds),
        }
    }

    fn dynamic_runtime_from_preset(
        preset: &str,
        kinds: &[&str],
    ) -> Result<crate::runtime::LayoutRuntime, String> {
        let iter = || kinds.iter().copied();
        let runtime = match preset {
            "master-stack" => Self::master_stack(iter()).into_runtime(),
            "centered-master" => Self::centered_master(iter()).into_runtime(),
            "monocle" => Self::monocle(iter()).into_runtime(),
            "scrollable" => Self::scrollable(iter()).into_runtime(),
            "dwindle" => Self::dwindle(iter()).into_runtime(),
            "spiral" => Self::spiral(iter()).into_runtime(),
            "deck" => Self::deck(iter()).into_runtime(),
            "tabbed" => Self::tabbed(iter()).into_runtime(),
            "stacked" => Self::stacked(iter()).into_runtime(),
            "dashboard" => Self::dashboard(iter().map(|kind| (kind, 1usize))).into_runtime(),
            _ => return Err(format!("unsupported dynamic preset in catalog: {preset}")),
        };
        runtime.map_err(|error| error.to_string())
    }

    fn fixed_runtime_from_preset(
        preset: &str,
        kinds: &[&str],
    ) -> Result<crate::runtime::LayoutRuntime, String> {
        let runtime = match preset {
            "sidebar" => {
                require_preset_slots(kinds, 2, preset)?;
                Self::sidebar(kinds[0], kinds[1]).into_runtime()
            }
            "holy-grail" => {
                require_preset_slots(kinds, 5, preset)?;
                Self::holy_grail(kinds[0], kinds[1], kinds[2], kinds[3], kinds[4]).into_runtime()
            }
            "split" => {
                require_preset_slots(kinds, 2, preset)?;
                Self::split(kinds[0], kinds[1]).into_runtime()
            }
            _ => {
                return Err(format!(
                    "unsupported fixed-slot preset in catalog: {preset}"
                ));
            }
        };
        runtime.map_err(|error| error.to_string())
    }

    // -- Preset constructors --

    /// Create a [`MasterStack`](crate::preset::MasterStack) builder.
    pub fn master_stack(
        kinds: impl IntoIterator<Item = impl Into<Arc<str>>>,
    ) -> crate::preset::MasterStack {
        crate::preset::MasterStack::new(kinds)
    }

    /// Create a [`CenteredMaster`](crate::preset::CenteredMaster) builder.
    pub fn centered_master(
        kinds: impl IntoIterator<Item = impl Into<Arc<str>>>,
    ) -> crate::preset::CenteredMaster {
        crate::preset::CenteredMaster::new(kinds)
    }

    /// Create a [`Monocle`](crate::preset::Monocle) builder.
    pub fn monocle(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> crate::preset::Monocle {
        crate::preset::Monocle::new(kinds)
    }

    /// Create a [`Scrollable`](crate::preset::Scrollable) builder.
    pub fn scrollable(
        kinds: impl IntoIterator<Item = impl Into<Arc<str>>>,
    ) -> crate::preset::Scrollable {
        crate::preset::Scrollable::new(kinds)
    }

    /// Create a [`Dwindle`](crate::preset::Dwindle) builder.
    pub fn dwindle(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> crate::preset::Dwindle {
        crate::preset::Dwindle::new(kinds)
    }

    /// Create a [`Spiral`](crate::preset::Spiral) builder.
    pub fn spiral(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> crate::preset::Spiral {
        crate::preset::Spiral::new(kinds)
    }

    /// Create a [`Deck`](crate::preset::Deck) builder.
    pub fn deck(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> crate::preset::Deck {
        crate::preset::Deck::new(kinds)
    }

    /// Create a [`Tabbed`](crate::preset::Tabbed) builder.
    pub fn tabbed(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> crate::preset::Tabbed {
        crate::preset::ActivePanelPreset::new_tabbed(kinds)
    }

    /// Create a [`Stacked`](crate::preset::Stacked) builder.
    pub fn stacked(kinds: impl IntoIterator<Item = impl Into<Arc<str>>>) -> crate::preset::Stacked {
        crate::preset::ActivePanelPreset::new_stacked(kinds)
    }

    /// Create a [`Sidebar`](crate::preset::Sidebar) builder.
    pub fn sidebar(
        sidebar_kind: impl Into<Arc<str>>,
        content_kind: impl Into<Arc<str>>,
    ) -> crate::preset::Sidebar {
        crate::preset::Sidebar::new(sidebar_kind, content_kind)
    }

    /// Create a [`HolyGrail`](crate::preset::HolyGrail) builder.
    pub fn holy_grail(
        header: impl Into<Arc<str>>,
        footer: impl Into<Arc<str>>,
        left: impl Into<Arc<str>>,
        main: impl Into<Arc<str>>,
        right: impl Into<Arc<str>>,
    ) -> crate::preset::HolyGrail {
        crate::preset::HolyGrail::new(header, footer, left, main, right)
    }

    /// Create a [`Dashboard`](crate::preset::Dashboard) builder.
    pub fn dashboard(
        cards: impl IntoIterator<Item = (impl Into<Arc<str>>, impl Into<crate::strategy::CardSpan>)>,
    ) -> crate::preset::Dashboard {
        crate::preset::Dashboard::new(cards)
    }

    /// Create a [`Split`](crate::preset::Split) builder.
    pub fn split(first: impl Into<Arc<str>>, second: impl Into<Arc<str>>) -> crate::preset::Split {
        crate::preset::Split::new(first, second)
    }

    /// Create an adaptive layout that switches strategies at width breakpoints.
    pub fn adaptive(
        panels: impl IntoIterator<Item = impl Into<Arc<str>>>,
    ) -> crate::breakpoint::AdaptiveBuilder {
        let panels: Box<[Arc<str>]> = panels.into_iter().map(Into::into).collect();
        crate::breakpoint::AdaptiveBuilder::new(panels)
    }
}

fn require_preset_slots(kinds: &[&str], expected: usize, preset: &str) -> Result<(), String> {
    match kinds.len() == expected {
        true => Ok(()),
        false => Err(format!(
            "preset '{preset}' requires exactly {expected} panels, got {}",
            kinds.len()
        )),
    }
}

impl Layout {
    /// Parse a TOML configuration string into a `Layout`.
    #[cfg(feature = "toml")]
    pub fn from_toml(input: &str) -> Result<Self, crate::toml_parse::TomlError> {
        crate::toml_parse::parse(input)
    }

    /// Parse a TOML configuration string into a `LayoutRuntime`.
    ///
    /// Handles both single-strategy configs and adaptive configs with
    /// `[[layout.breakpoints]]`.
    #[cfg(feature = "toml")]
    pub fn from_toml_runtime(
        input: &str,
    ) -> Result<crate::runtime::LayoutRuntime, crate::toml_parse::TomlError> {
        crate::toml_parse::parse_runtime(input)
    }

    /// Read a TOML file from disk and parse it into a `Layout`.
    #[cfg(feature = "toml")]
    pub fn from_toml_file(
        path: impl AsRef<std::path::Path>,
    ) -> Result<Self, crate::toml_parse::TomlError> {
        let input = std::fs::read_to_string(path)?;
        crate::toml_parse::parse(&input)
    }

    /// Compile the layout tree into a Taffy tree ready for layout computation.
    pub fn compile(&self) -> Result<crate::compiler::CompileResult, PaneError> {
        compile(&self.tree)
    }
}

impl From<Layout> for LayoutTree {
    fn from(layout: Layout) -> Self {
        layout.tree
    }
}