Skip to main content

blizz_ui/
layout_stack.rs

1//! Vertical layout compositor: tracks the current row and computes
2//! [`LayoutPanel`]s from alignment intent so callers declare *what*
3//! goes where, not *how* to position it.
4
5use std::io::{self, Write};
6
7use crate::layout_panel::LayoutPanel;
8use crate::renderer::{Component, Renderer};
9
10/// Horizontal alignment strategy for a component within the terminal.
11pub enum Align {
12  /// Centered using the pre-computed box geometry (column + inner width).
13  Box,
14  /// Centered for the given content width within the terminal.
15  Content(u16),
16  /// Full terminal width, column 0 — component handles its own centering.
17  Full,
18}
19
20/// A vertical layout stack that tracks the current row and produces
21/// panels for each component drawn.
22///
23/// Compositors create a stack at a starting row, then call [`draw`](Self::draw)
24/// for each component in order. The stack advances the row by the number
25/// of rows each component consumed.
26pub struct LayoutStack {
27  row: u16,
28  terminal_width: u16,
29  box_column: u16,
30  box_width: u16,
31}
32
33impl LayoutStack {
34  pub fn new(start_row: u16, terminal_width: u16, box_column: u16, box_width: u16) -> Self {
35    Self {
36      row: start_row,
37      terminal_width,
38      box_column,
39      box_width,
40    }
41  }
42
43  pub fn row(&self) -> u16 {
44    self.row
45  }
46
47  pub fn terminal_width(&self) -> u16 {
48    self.terminal_width
49  }
50
51  /// Draw a component at the current row with the given alignment,
52  /// then advance the row by the number of rows consumed.
53  #[cfg(not(tarpaulin_include))]
54  pub fn draw<W: Write>(
55    &mut self,
56    renderer: &mut Renderer<W>,
57    component: &impl Component,
58    align: Align,
59  ) -> io::Result<u16> {
60    let panel = self.panel(align);
61    let rows = renderer.draw(component, panel)?;
62    self.row = self.row.saturating_add(rows);
63    Ok(rows)
64  }
65
66  /// Advance the current row without drawing anything.
67  pub fn skip(&mut self, rows: u16) {
68    self.row = self.row.saturating_add(rows);
69  }
70
71  fn panel(&self, align: Align) -> LayoutPanel {
72    match align {
73      Align::Box => LayoutPanel {
74        row: self.row,
75        column: self.box_column,
76        width: self.box_width,
77      },
78      Align::Content(width) => LayoutPanel::centered(self.terminal_width, width, self.row),
79      Align::Full => LayoutPanel {
80        row: self.row,
81        column: 0,
82        width: self.terminal_width,
83      },
84    }
85  }
86}
87
88#[cfg(test)]
89mod tests {
90  use super::*;
91
92  #[test]
93  fn new_starts_at_given_row() {
94    let stack = LayoutStack::new(5, 80, 10, 30);
95    assert_eq!(stack.row(), 5);
96    assert_eq!(stack.terminal_width(), 80);
97  }
98
99  #[test]
100  fn skip_advances_row() {
101    let mut stack = LayoutStack::new(0, 80, 10, 30);
102    stack.skip(3);
103    assert_eq!(stack.row(), 3);
104    stack.skip(2);
105    assert_eq!(stack.row(), 5);
106  }
107
108  #[test]
109  fn panel_box_uses_precomputed_geometry() {
110    let stack = LayoutStack::new(10, 80, 15, 40);
111    let panel = stack.panel(Align::Box);
112    assert_eq!(panel.row, 10);
113    assert_eq!(panel.column, 15);
114    assert_eq!(panel.width, 40);
115  }
116
117  #[test]
118  fn panel_content_centers_width() {
119    let stack = LayoutStack::new(5, 80, 0, 0);
120    let panel = stack.panel(Align::Content(20));
121    assert_eq!(panel.row, 5);
122    assert_eq!(panel.column, 30); // (80 - 20) / 2
123    assert_eq!(panel.width, 20);
124  }
125
126  #[test]
127  fn panel_full_spans_terminal() {
128    let stack = LayoutStack::new(7, 120, 10, 30);
129    let panel = stack.panel(Align::Full);
130    assert_eq!(panel.row, 7);
131    assert_eq!(panel.column, 0);
132    assert_eq!(panel.width, 120);
133  }
134}