panes 0.19.0

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

use crate::error::{MutationError, PaneError, TreeError};
use crate::node::PanelId;
use crate::panel::fixed;
use crate::sequence::PanelSequence;
use crate::strategy::CardSpan;
use crate::tree::LayoutTree;
use crate::viewport::ViewportState;

use super::StrategyKind;
use super::build::{build_tree_for_strategy, populate_sequence_by_kinds};
use super::focus::try_apply_focus;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// After a rebuild (which hardcodes active=0), reset viewport focus to index 0
/// then transition to the desired target panel.
fn focus_after_rebuild(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    target: PanelId,
) {
    viewport.focus = sequence.get(0);
    let _ = try_apply_focus(strategy, tree, sequence, viewport, target);
}

// ---------------------------------------------------------------------------
// apply_add
// ---------------------------------------------------------------------------

/// Add a panel at a specific sequence index.
///
/// Slotted: restores a collapsed slot (index ignored).
/// All others: inserts the kind at `index`, rebuilds the tree via
/// `build_tree_for_strategy`, and applies focus to the new panel.
pub fn apply_add(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    kind: Arc<str>,
    index: usize,
) -> Result<PanelId, PaneError> {
    match strategy {
        StrategyKind::Slotted { .. } => add_slotted(tree, viewport),
        _ => add_via_rebuild(strategy, tree, sequence, viewport, kind, index),
    }
}

fn add_via_rebuild(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    kind: Arc<str>,
    index: usize,
) -> Result<PanelId, PaneError> {
    let mut kinds: Vec<Arc<str>> = sequence
        .iter()
        .filter_map(|pid| tree.panel_kind_arc(pid).ok())
        .collect();
    let clamped = index.min(kinds.len());
    kinds.insert(clamped, kind);
    rebuild_tree_and_sequence(tree, sequence, &kinds, |kinds| {
        build_tree_for_strategy(strategy, kinds)
    })?;
    let new_pid = sequence
        .get(clamped)
        .ok_or(PaneError::InvalidTree(TreeError::EmptyAfterRebuild))?;
    focus_after_rebuild(strategy, tree, sequence, viewport, new_pid);
    Ok(new_pid)
}

fn add_slotted(tree: &mut LayoutTree, viewport: &mut ViewportState) -> Result<PanelId, PaneError> {
    let pid = viewport
        .collapsed
        .iter()
        .next()
        .copied()
        .ok_or(PaneError::InvalidMutation(MutationError::NoCollapsedSlots))?;
    let saved = viewport
        .saved_constraints
        .remove(&pid)
        .ok_or(PaneError::InvalidMutation(
            MutationError::SlotNoSavedConstraints,
        ))?;
    tree.set_constraints(pid, saved)?;
    viewport.collapsed.remove(&pid);
    viewport.focus = Some(pid);
    Ok(pid)
}

// ---------------------------------------------------------------------------
// apply_remove
// ---------------------------------------------------------------------------

/// Remove a panel. Returns the new focus panel.
pub fn apply_remove(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    pid: PanelId,
) -> Result<Option<PanelId>, PaneError> {
    match strategy {
        StrategyKind::Slotted { .. } => remove_slotted(tree, sequence, viewport, pid),
        _ => remove_via_rebuild(strategy, tree, sequence, viewport, pid),
    }
}

fn remove_slotted(
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    pid: PanelId,
) -> Result<Option<PanelId>, PaneError> {
    let current = tree.panel_constraints(pid)?;
    viewport.saved_constraints.insert(pid, current);
    tree.set_constraints(pid, fixed(0.0))?;
    viewport.collapsed.insert(pid);
    // Panel may not be in sequence (e.g. decoration panels), fall back to index 0.
    let removed_idx = sequence.remove(pid).unwrap_or(0);
    let new_focus = sequence.neighbor_after_removal(removed_idx);
    viewport.focus = new_focus;
    Ok(new_focus)
}

fn remove_via_rebuild(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    pid: PanelId,
) -> Result<Option<PanelId>, PaneError> {
    // Panel may not be in sequence (e.g. decoration panels), fall back to index 0.
    let removed_idx = sequence.remove(pid).unwrap_or(0);
    match sequence.is_empty() {
        true => {
            viewport.focus = None;
            return Ok(None);
        }
        false => {}
    }
    let kinds = collect_kinds_from_sequence(tree, sequence);
    rebuild_tree_and_sequence(tree, sequence, &kinds, |kinds| {
        build_tree_for_strategy(strategy, kinds)
    })?;
    let focus_idx = removed_idx.min(sequence.len().saturating_sub(1));
    let new_focus = sequence.get(focus_idx);
    if let Some(pid) = new_focus {
        focus_after_rebuild(strategy, tree, sequence, viewport, pid);
    }
    Ok(new_focus)
}

// ---------------------------------------------------------------------------
// apply_move
// ---------------------------------------------------------------------------

/// Move a panel to a new sequence index.
pub fn apply_move(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    pid: PanelId,
    new_index: usize,
) -> Result<PanelId, PaneError> {
    match strategy.supports_move() {
        false => {
            return Err(PaneError::InvalidMutation(MutationError::MoveNotSupported));
        }
        true => {}
    }

    match new_index >= sequence.len() {
        true => return Err(PaneError::SequenceOutOfBounds(new_index, sequence.len())),
        false => {}
    }

    sequence
        .move_to(pid, new_index)
        .ok_or(PaneError::PanelNotFound(pid))?;

    rebuild_from_sequence(strategy, tree, sequence)?;

    let moved_pid = sequence
        .get(new_index)
        .ok_or_else(|| PaneError::SequenceOutOfBounds(new_index, sequence.len()))?;
    focus_after_rebuild(strategy, tree, sequence, viewport, moved_pid);
    Ok(moved_pid)
}

fn rebuild_from_sequence(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
) -> Result<(), PaneError> {
    let kinds = collect_kinds_from_sequence(tree, sequence);
    match strategy {
        StrategyKind::Slotted { .. } => return Ok(()),
        _ => {}
    }
    rebuild_tree_and_sequence(tree, sequence, &kinds, |kinds| {
        build_tree_for_strategy(strategy, kinds)
    })
}

/// Rebuild the tree from a kinds list and repopulate the sequence.
///
/// Full tree rebuild on every mutation is a deliberate design trade-off:
/// strategy trees are structurally determined by kind order, so in-place
/// patching would duplicate each strategy's topology logic.
fn rebuild_tree_and_sequence(
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    kinds: &[Arc<str>],
    builder: impl FnOnce(&[Arc<str>]) -> Result<LayoutTree, PaneError>,
) -> Result<(), PaneError> {
    *tree = builder(kinds)?;
    let mut new_seq = PanelSequence::default();
    populate_sequence_by_kinds(tree, kinds, &mut new_seq);
    *sequence = new_seq;
    Ok(())
}

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

/// Collect panel kinds from the sequence, preserving order.
/// Skips panels missing from the tree (only possible via `tree_mut()` corruption).
pub(crate) fn collect_kinds_from_sequence(
    tree: &LayoutTree,
    sequence: &PanelSequence,
) -> Box<[Arc<str>]> {
    sequence
        .iter()
        .filter_map(|pid| tree.panel_kind_arc(pid).ok())
        .collect()
}

// ---------------------------------------------------------------------------
// apply_set_card_span
// ---------------------------------------------------------------------------

/// Change a dashboard card's column span and rebuild the grid tree.
///
/// Returns the updated `StrategyKind` so the caller can replace `self.strategy`.
pub fn apply_set_card_span(
    strategy: &StrategyKind,
    tree: &mut LayoutTree,
    sequence: &mut PanelSequence,
    viewport: &mut ViewportState,
    pid: PanelId,
    span: CardSpan,
) -> Result<StrategyKind, PaneError> {
    let seq_idx = sequence
        .index_of(pid)
        .ok_or(PaneError::PanelNotFound(pid))?;

    let new_strategy = build_updated_strategy(strategy, seq_idx, span)?;

    let kinds = collect_kinds_from_sequence(tree, sequence);
    rebuild_tree_and_sequence(tree, sequence, &kinds, |kinds| {
        build_tree_for_strategy(&new_strategy, kinds)
    })?;

    focus_after_rebuild(&new_strategy, tree, sequence, viewport, pid);

    Ok(new_strategy)
}

fn update_spans(old: &[CardSpan], index: usize, span: CardSpan) -> Arc<[CardSpan]> {
    let len = old.len().max(index + 1);
    let mut new_spans: Vec<CardSpan> = Vec::with_capacity(len);
    new_spans.extend_from_slice(old);
    new_spans.resize(len, CardSpan::Columns(1));
    new_spans[index] = span;
    new_spans.into()
}

fn build_updated_strategy(
    strategy: &StrategyKind,
    index: usize,
    span: CardSpan,
) -> Result<StrategyKind, PaneError> {
    match strategy {
        StrategyKind::Dashboard {
            columns,
            gap,
            spans,
            auto_rows,
        } => Ok(StrategyKind::Dashboard {
            columns: *columns,
            gap: *gap,
            spans: update_spans(spans, index, span),
            auto_rows: *auto_rows,
        }),
        _ => Err(PaneError::InvalidMutation(MutationError::SpanNotSupported)),
    }
}