blizz-ui 3.0.0-dev.19

Self-rendering terminal UI components for the blizz wizard
Documentation
//! Composites question text, selector or text-entry chrome, hint, timer bar,
//! and optional cursor positioning for the blizz setup wizard prompt layer.
//!
//! Layout is driven by a [`LayoutStack`] — each component declares its
//! alignment intent and the stack handles vertical positioning.

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

use crate::Renderer;
use crate::components::hint::HintComponent;
use crate::components::selector::SelectorComponent;
use crate::components::text::TextComponent;
use crate::components::text_entry::TextEntryComponent;
use crate::components::timer_bar::TimerBarComponent;
use crate::layout_stack::{Align, LayoutStack};
use crate::prompt;

/// References to wizard prompt widgets for [`render_prompt_layer`].
#[derive(Clone, Copy)]
pub struct WizardPromptRefs<'a> {
  pub question: &'a TextComponent,
  pub entry: &'a TextEntryComponent,
  pub selector: &'a SelectorComponent,
  pub hint: &'a HintComponent,
  pub timer_bar: &'a TimerBarComponent,
}

#[cfg(not(tarpaulin_include))]
pub fn render_prompt_layer<W: Write>(
  renderer: &mut Renderer<W>,
  refs: WizardPromptRefs<'_>,
) -> io::Result<()> {
  let terminal_width = renderer.ctx().terminal_size.width;
  let layout = match refs.entry.layout() {
    Some(l) => l,
    None => return Ok(()),
  };

  let mut stack = LayoutStack::new(
    layout.question_row,
    terminal_width,
    layout.box_column,
    layout.inner_width,
  );

  let question_width = refs.question.text.chars().count() as u16;
  stack.draw(renderer, refs.question, Align::Content(question_width))?;

  if refs.selector.visible {
    stack.draw(renderer, refs.selector, Align::Full)?;
    stack.skip(1);
    return draw_footer(&mut stack, renderer, refs.hint, refs.timer_bar);
  }

  if refs.entry.visible && refs.entry.open > 0.0 {
    stack.draw(renderer, refs.entry, Align::Box)?;
  } else {
    let box_height = layout.hint_row.saturating_sub(layout.box_top_row);
    stack.skip(box_height);
  }

  draw_footer(&mut stack, renderer, refs.hint, refs.timer_bar)?;

  if refs.entry.is_fully_ready()
    && let Some(col) = refs.entry.cursor_column(terminal_width)
  {
    prompt::queue_cursor(&mut renderer.writer, layout, col)?;
  }

  Ok(())
}

#[cfg(not(tarpaulin_include))]
fn draw_footer<W: Write>(
  stack: &mut LayoutStack,
  renderer: &mut Renderer<W>,
  hint: &HintComponent,
  timer_bar: &TimerBarComponent,
) -> io::Result<()> {
  let hint_width = hint.text.chars().count() as u16;
  stack.draw(renderer, hint, Align::Content(hint_width))?;

  let seg = TimerBarComponent::segment_count(timer_bar.progress, stack.terminal_width());
  stack.draw(renderer, timer_bar, Align::Content(seg as u16))?;
  Ok(())
}