blizz-ui 3.0.0-dev.18

Self-rendering terminal UI components for the blizz wizard
Documentation
//! Vertical layout compositor: tracks the current row and computes
//! [`LayoutPanel`]s from alignment intent so callers declare *what*
//! goes where, not *how* to position it.

use std::io::{self, Write};

use crate::layout_panel::LayoutPanel;
use crate::renderer::{Component, Renderer};

/// Horizontal alignment strategy for a component within the terminal.
pub enum Align {
  /// Centered using the pre-computed box geometry (column + inner width).
  Box,
  /// Centered for the given content width within the terminal.
  Content(u16),
  /// Full terminal width, column 0 — component handles its own centering.
  Full,
}

/// A vertical layout stack that tracks the current row and produces
/// panels for each component drawn.
///
/// Compositors create a stack at a starting row, then call [`draw`](Self::draw)
/// for each component in order. The stack advances the row by the number
/// of rows each component consumed.
pub struct LayoutStack {
  row: u16,
  terminal_width: u16,
  box_column: u16,
  box_width: u16,
}

impl LayoutStack {
  pub fn new(start_row: u16, terminal_width: u16, box_column: u16, box_width: u16) -> Self {
    Self {
      row: start_row,
      terminal_width,
      box_column,
      box_width,
    }
  }

  pub fn row(&self) -> u16 {
    self.row
  }

  pub fn terminal_width(&self) -> u16 {
    self.terminal_width
  }

  /// Draw a component at the current row with the given alignment,
  /// then advance the row by the number of rows consumed.
  #[cfg(not(tarpaulin_include))]
  pub fn draw<W: Write>(
    &mut self,
    renderer: &mut Renderer<W>,
    component: &impl Component,
    align: Align,
  ) -> io::Result<u16> {
    let panel = self.panel(align);
    let rows = renderer.draw(component, panel)?;
    self.row = self.row.saturating_add(rows);
    Ok(rows)
  }

  /// Advance the current row without drawing anything.
  pub fn skip(&mut self, rows: u16) {
    self.row = self.row.saturating_add(rows);
  }

  fn panel(&self, align: Align) -> LayoutPanel {
    match align {
      Align::Box => LayoutPanel {
        row: self.row,
        column: self.box_column,
        width: self.box_width,
      },
      Align::Content(width) => LayoutPanel::centered(self.terminal_width, width, self.row),
      Align::Full => LayoutPanel {
        row: self.row,
        column: 0,
        width: self.terminal_width,
      },
    }
  }
}

#[cfg(test)]
mod tests {
  use super::*;

  #[test]
  fn new_starts_at_given_row() {
    let stack = LayoutStack::new(5, 80, 10, 30);
    assert_eq!(stack.row(), 5);
    assert_eq!(stack.terminal_width(), 80);
  }

  #[test]
  fn skip_advances_row() {
    let mut stack = LayoutStack::new(0, 80, 10, 30);
    stack.skip(3);
    assert_eq!(stack.row(), 3);
    stack.skip(2);
    assert_eq!(stack.row(), 5);
  }

  #[test]
  fn panel_box_uses_precomputed_geometry() {
    let stack = LayoutStack::new(10, 80, 15, 40);
    let panel = stack.panel(Align::Box);
    assert_eq!(panel.row, 10);
    assert_eq!(panel.column, 15);
    assert_eq!(panel.width, 40);
  }

  #[test]
  fn panel_content_centers_width() {
    let stack = LayoutStack::new(5, 80, 0, 0);
    let panel = stack.panel(Align::Content(20));
    assert_eq!(panel.row, 5);
    assert_eq!(panel.column, 30); // (80 - 20) / 2
    assert_eq!(panel.width, 20);
  }

  #[test]
  fn panel_full_spans_terminal() {
    let stack = LayoutStack::new(7, 120, 10, 30);
    let panel = stack.panel(Align::Full);
    assert_eq!(panel.row, 7);
    assert_eq!(panel.column, 0);
    assert_eq!(panel.width, 120);
  }
}