scrin 0.1.79

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::widgets::Widget;

#[derive(Debug, Clone)]
pub struct Bar {
    pub label: String,
    pub value: u64,
    pub color: Color,
}

impl Bar {
    pub fn new(label: &str, value: u64) -> Self {
        Self {
            label: label.to_string(),
            value,
            color: Color::rgb(88, 166, 255),
        }
    }

    pub fn with_color(mut self, color: Color) -> Self {
        self.color = color;
        self
    }
}

#[derive(Debug, Clone)]
pub struct BarChart {
    pub bars: Vec<Bar>,
    pub max: Option<u64>,
    pub bar_width: u16,
    pub gap: u16,
    pub show_labels: bool,
    pub direction: BarDirection,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BarDirection {
    Vertical,
    Horizontal,
}

impl BarChart {
    pub fn new() -> Self {
        Self {
            bars: Vec::new(),
            max: None,
            bar_width: 1,
            gap: 1,
            show_labels: true,
            direction: BarDirection::Vertical,
        }
    }

    pub fn with_bars(mut self, bars: Vec<Bar>) -> Self {
        self.bars = bars;
        self
    }

    pub fn with_direction(mut self, dir: BarDirection) -> Self {
        self.direction = dir;
        self
    }
}

impl Widget for BarChart {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if self.bars.is_empty() {
            return;
        }
        let max = self
            .max
            .unwrap_or_else(|| self.bars.iter().map(|b| b.value).max().unwrap_or(1));

        match self.direction {
            BarDirection::Vertical => self.render_vertical(buffer, area, max),
            BarDirection::Horizontal => self.render_horizontal(buffer, area, max),
        }
    }
}

impl BarChart {
    fn render_vertical(&self, buffer: &mut Buffer, area: Rect, max: u64) {
        let bar_area_height = if self.show_labels {
            area.height.saturating_sub(2) as usize
        } else {
            area.height as usize
        };

        for (i, bar) in self.bars.iter().enumerate() {
            let x = (area.x as usize) + i * (self.bar_width as usize + self.gap as usize);
            if x >= area.right() as usize {
                break;
            }
            let fill = if max == 0 {
                0
            } else {
                ((bar.value as f64 / max as f64) * bar_area_height as f64) as usize
            };

            for dy in 0..bar_area_height {
                let y = (area.y as usize) + bar_area_height - 1 - dy;
                if y < area.bottom() as usize {
                    let ch = if dy < fill { '' } else { '' };
                    let fg = if dy < fill {
                        bar.color
                    } else {
                        bar.color.dim(0.3)
                    };
                    buffer.set(
                        x,
                        y,
                        crate::core::buffer::Cell {
                            ch,
                            fg,
                            bg: None,
                            bold: false,
                            italic: false,
                            underlined: false,
                        },
                    );
                }
            }

            if self.show_labels {
                let label_y = area.bottom() as usize - 1;
                let label_display: String =
                    bar.label.chars().take(self.bar_width as usize).collect();
                buffer.set_str(x, label_y, &label_display, bar.color.dim(0.5), None);
            }
        }
    }

    fn render_horizontal(&self, buffer: &mut Buffer, area: Rect, max: u64) {
        let bar_area_width = if self.show_labels {
            area.width.saturating_sub(12) as usize
        } else {
            area.width as usize
        };

        for (i, bar) in self.bars.iter().enumerate() {
            let y = (area.y as usize) + i;
            if y >= area.bottom() as usize {
                break;
            }

            if self.show_labels {
                let label_display: String = bar.label.chars().take(10).collect();
                buffer.set_str(area.x as usize, y, &label_display, bar.color.dim(0.5), None);
            }

            let fill = if max == 0 {
                0
            } else {
                ((bar.value as f64 / max as f64) * bar_area_width as f64) as usize
            };

            let x_offset = if self.show_labels {
                area.x + 11
            } else {
                area.x
            };
            for dx in 0..bar_area_width {
                let x = (x_offset as usize) + dx;
                if x >= area.right() as usize {
                    break;
                }
                let ch = if dx < fill { '' } else { '' };
                let fg = if dx < fill {
                    bar.color
                } else {
                    bar.color.dim(0.3)
                };
                buffer.set(
                    x,
                    y,
                    crate::core::buffer::Cell {
                        ch,
                        fg,
                        bg: None,
                        bold: false,
                        italic: false,
                        underlined: false,
                    },
                );
            }
        }
    }
}

impl Default for BarChart {
    fn default() -> Self {
        Self::new()
    }
}