lv-tui 0.2.0

A reactive TUI framework for Rust, inspired by Textual and React
Documentation
use crate::component::{Component, EventCx, MeasureCx};
use crate::event::Event;
use crate::geom::{Rect, Size};
use crate::layout::Constraint;
use crate::render::RenderCx;
use crate::style::Style;

const BLOCKS: &[&str] = &[" ", "", "", "", "", "", "", "", ""];

/// A progress bar using Unicode 8-segment block characters.
///
/// Displays a bar of `width` cells filled to `ratio` (0.0–1.0). Purely visual —
/// not focusable, no keyboard interaction. Combine with `#[reactive]` and
/// [`Event::Tick`] for animated progress.
pub struct ProgressBar {
    ratio: f64,
    width: u16,
    style: Style,
    track_style: Style,
}

impl ProgressBar {
    /// Creates a new progress bar at 0%, 20 cells wide.
    pub fn new() -> Self {
        Self {
            ratio: 0.0,
            width: 20,
            style: Style::default().fg(crate::style::Color::Cyan),
            track_style: Style::default().fg(crate::style::Color::Gray),
        }
    }

    /// Builder: sets the fill ratio (0.0–1.0).
    pub fn ratio(mut self, ratio: f64) -> Self {
        self.ratio = ratio.clamp(0.0, 1.0);
        self
    }

    /// Builder: sets the bar width in character cells.
    pub fn width(mut self, width: u16) -> Self {
        self.width = width;
        self
    }

    /// Builder: sets the filled-portion style.
    pub fn style(mut self, style: Style) -> Self {
        self.style = style;
        self
    }

    /// Builder: sets the unfilled-portion (track) style.
    pub fn track_style(mut self, style: Style) -> Self {
        self.track_style = style;
        self
    }

    /// Sets the fill ratio and marks paint dirty.
    pub fn set_ratio(&mut self, ratio: f64, cx: &mut EventCx) {
        let r = ratio.clamp(0.0, 1.0);
        if (self.ratio - r).abs() > f64::EPSILON {
            self.ratio = r;
            cx.invalidate_paint();
        }
    }
}

impl Component for ProgressBar {
    fn render(&self, cx: &mut RenderCx) {
        let filled = (self.ratio * self.width as f64) as u16;
        let whole = filled.min(self.width);
        let frac = ((self.ratio * self.width as f64) - whole as f64) * 8.0;
        let frac_idx = (frac as usize).min(BLOCKS.len() - 1);

        // Filled portion
        if whole > 0 {
            cx.set_style(self.style.clone());
            cx.text("".repeat(whole as usize));
        }

        // Fractional character + track
        if whole < self.width {
            if frac_idx > 0 {
                cx.set_style(self.style.clone());
                cx.text(BLOCKS[frac_idx]);
            }
            let frac_used = if frac_idx > 0 { 1 } else { 0 };
            let remaining = self.width.saturating_sub(whole).saturating_sub(frac_used);
            if remaining > 0 {
                cx.set_style(self.track_style.clone());
                cx.text("".repeat(remaining as usize));
            }
        }

        // Ensure cursor advances to end
        cx.set_style(self.track_style.clone());
        cx.line("");
    }

    fn measure(&self, _constraint: Constraint, _cx: &mut MeasureCx) -> Size {
        Size { width: self.width, height: 1 }
    }

    fn event(&mut self, _event: &Event, _cx: &mut EventCx) {}
    fn layout(&mut self, _rect: Rect, _cx: &mut crate::component::LayoutCx) {}
    fn focusable(&self) -> bool { false }
    fn style(&self) -> Style { self.style.clone() }
}