Skip to main content

blizz_ui/
renderer.rs

1use std::io;
2use std::io::Write;
3
4use crossterm::{
5  cursor::{Hide, MoveTo},
6  queue,
7  terminal::{Clear, ClearType},
8};
9use rand::Rng;
10
11use crate::layout_panel::LayoutPanel;
12use crate::render_context::RenderContext;
13
14/// Bundles writer, RNG, per-frame context, and current panel for rendering.
15///
16/// Created once at wizard startup. Per-frame context is updated via
17/// [`frame()`](Renderer::frame), which scopes each render pass.
18/// The active [`LayoutPanel`] is set by [`draw()`](Renderer::draw).
19pub struct Renderer<W: Write> {
20  pub writer: W,
21  pub rng: Box<dyn Rng>,
22  ctx: RenderContext,
23  panel: LayoutPanel,
24}
25
26impl<W: Write> Renderer<W> {
27  pub fn new(writer: W, rng: Box<dyn Rng>, ctx: RenderContext) -> Self {
28    Self {
29      writer,
30      rng,
31      ctx,
32      panel: LayoutPanel {
33        row: 0,
34        column: 0,
35        width: 0,
36      },
37    }
38  }
39
40  pub fn ctx(&self) -> &RenderContext {
41    &self.ctx
42  }
43
44  pub fn panel(&self) -> LayoutPanel {
45    self.panel
46  }
47
48  /// Scope a render frame: update the context, clear the screen, run `f`,
49  /// then flush.
50  ///
51  /// Analogous to the shellops `with_cursor` pattern — scoped mutation
52  /// with a single point of context update.
53  #[cfg(not(tarpaulin_include))]
54  pub fn frame<F>(&mut self, ctx: RenderContext, f: F) -> io::Result<()>
55  where
56    F: FnOnce(&mut Self) -> io::Result<()>,
57  {
58    self.ctx = ctx;
59    queue!(self.writer, Hide, Clear(ClearType::All))?;
60    f(self)?;
61    self.writer.flush()
62  }
63
64  /// Set the active panel and render a component.
65  #[cfg(not(tarpaulin_include))]
66  pub fn draw(&mut self, component: &impl Component, panel: LayoutPanel) -> io::Result<u16> {
67    self.panel = panel;
68    component.render(self)
69  }
70
71  /// Destructure the renderer for a component render at the active panel.
72  ///
73  /// Positions the cursor, then passes writer, panel, and RNG to the
74  /// closure. The panel is read from the renderer (set by [`draw`](Renderer::draw)).
75  #[cfg(not(tarpaulin_include))]
76  pub fn with_panel<F>(&mut self, f: F) -> io::Result<u16>
77  where
78    F: FnOnce(&mut W, LayoutPanel, &mut Box<dyn Rng>) -> io::Result<u16>,
79  {
80    let panel = self.panel;
81    queue!(self.writer, MoveTo(panel.column, panel.row))?;
82    f(&mut self.writer, panel, &mut self.rng)
83  }
84}
85
86/// A self-rendering UI element.
87///
88/// Components read their panel from the [`Renderer`] (set by
89/// [`Renderer::draw`]). Returns the number of terminal rows consumed,
90/// so compositors can stack components.
91pub trait Component {
92  fn render<W: Write>(&self, renderer: &mut Renderer<W>) -> io::Result<u16>;
93}
94
95#[cfg(test)]
96mod tests {
97  use crate::test_helpers::test_renderer;
98
99  #[test]
100  fn ctx_returns_terminal_size_and_elapsed() {
101    let renderer = test_renderer();
102    let ctx = renderer.ctx();
103    assert_eq!(ctx.terminal_size.width, 80);
104    assert_eq!(ctx.terminal_size.height, 24);
105  }
106
107  #[test]
108  fn panel_defaults_to_zero() {
109    let renderer = test_renderer();
110    let panel = renderer.panel();
111    assert_eq!(panel.row, 0);
112    assert_eq!(panel.column, 0);
113    assert_eq!(panel.width, 0);
114  }
115}