Skip to main content

rich/
bar.rs

1//! Horizontal bars.
2//!
3//! Port of upstream `rich/bar.py`. A [`Bar`] draws a filled span `[begin, end]`
4//! within a range `[0, size]` across `width` cells, using eighth-block glyphs
5//! for sub-cell resolution at each edge.
6
7use crate::color::Color;
8use crate::console::{Console, ConsoleOptions};
9use crate::measure::Measurement;
10use crate::protocol::Renderable;
11use crate::segment::Segment;
12use crate::style::Style;
13
14const FULL_BLOCK: &str = "\u{2588}"; // █
15/// Right-aligned partial blocks for the *begin* edge (index = eighths).
16const BEGIN_BLOCK_ELEMENTS: [&str; 8] = [
17    "\u{2588}", "\u{2588}", "\u{2588}", "\u{2590}", "\u{2590}", "\u{2590}", "\u{2595}", "\u{2595}",
18];
19/// Left-aligned partial blocks for the *end* edge (index = eighths).
20const END_BLOCK_ELEMENTS: [&str; 8] = [
21    " ", "\u{258f}", "\u{258e}", "\u{258d}", "\u{258c}", "\u{258b}", "\u{258a}", "\u{2589}",
22];
23
24/// A horizontal bar spanning `[begin, end]` within `[0, size]`. Mirrors `rich.bar.Bar`.
25pub struct Bar {
26    size: f64,
27    begin: f64,
28    end: f64,
29    width: Option<usize>,
30    style: Style,
31}
32
33impl Bar {
34    /// A bar covering `[begin, end]` of a `[0, size]` range. As upstream,
35    /// `begin` is raised to 0 and `end` lowered to `size`.
36    pub fn new(size: f64, begin: f64, end: f64) -> Self {
37        Bar {
38            size,
39            begin: begin.max(0.0),
40            end: end.min(size),
41            width: None,
42            // Upstream default: `color="default"`, `bgcolor="default"`.
43            style: Style::new()
44                .with_color(Color::default_color())
45                .with_bgcolor(Color::default_color()),
46        }
47    }
48
49    /// The bar colour (upstream `color=`, default `"default"`).
50    pub fn color(mut self, color: Color) -> Self {
51        self.style = Style::from_color(Some(color), self.style.bgcolor().cloned());
52        self
53    }
54
55    /// The background colour (upstream `bgcolor=`, default `"default"`).
56    pub fn bgcolor(mut self, bgcolor: Color) -> Self {
57        self.style = Style::from_color(self.style.color().cloned(), Some(bgcolor));
58        self
59    }
60
61    /// Fix the bar width (otherwise it fills the available width).
62    pub fn width(mut self, width: usize) -> Self {
63        self.width = Some(width);
64        self
65    }
66}
67
68impl Renderable for Bar {
69    /// Port of `Bar.__rich_measure__`: its fixed width, else at least four
70    /// cells and up to the whole width.
71    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
72        match self.width {
73            Some(width) => Measurement::new(width, width),
74            None => Measurement::new(4, options.max_width),
75        }
76    }
77
78    fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
79        let width = self
80            .width
81            .unwrap_or(options.max_width)
82            .min(options.max_width);
83        let style = Some(self.style.clone());
84
85        if self.begin >= self.end {
86            return vec![Segment::new(" ".repeat(width), style)];
87        }
88
89        let prefix_complete_eighths = (width as f64 * 8.0 * self.begin / self.size) as usize;
90        let prefix_bar_count = prefix_complete_eighths / 8;
91        let prefix_eighths = prefix_complete_eighths % 8;
92
93        let body_complete_eighths = (width as f64 * 8.0 * self.end / self.size) as usize;
94        let body_bar_count = body_complete_eighths / 8;
95        let body_eighths = body_complete_eighths % 8;
96
97        let mut prefix = " ".repeat(prefix_bar_count);
98        if prefix_eighths != 0 {
99            prefix.push_str(BEGIN_BLOCK_ELEMENTS[prefix_eighths]);
100        }
101
102        let mut body = FULL_BLOCK.repeat(body_bar_count);
103        if body_eighths != 0 {
104            body.push_str(END_BLOCK_ELEMENTS[body_eighths]);
105        }
106
107        let body_len = body.chars().count();
108        let suffix = " ".repeat(width.saturating_sub(body_len));
109
110        // Overlay: prefix, then the body from `len(prefix)` onward, then the suffix.
111        let prefix_len = prefix.chars().count();
112        let body_tail: String = body.chars().skip(prefix_len).collect();
113        let line = format!("{prefix}{body_tail}{suffix}");
114
115        vec![Segment::new(line, style)]
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122    use crate::color::ColorSystem;
123
124    fn render(begin: f64, end: f64) -> String {
125        let console = Console::builder()
126            .force_terminal(true)
127            .color_system(Some(ColorSystem::Truecolor))
128            .width(20)
129            .build();
130        console.render_to_string(&Bar::new(100.0, begin, end).width(20))
131    }
132
133    #[test]
134    fn full_bar() {
135        assert_eq!(
136            render(0.0, 100.0),
137            format!("\x1b[39;49m{}\x1b[0m", FULL_BLOCK.repeat(20))
138        );
139    }
140
141    #[test]
142    fn half_bar() {
143        assert_eq!(
144            render(0.0, 50.0),
145            format!("\x1b[39;49m{}          \x1b[0m", FULL_BLOCK.repeat(10))
146        );
147    }
148
149    #[test]
150    fn partial_edge_uses_eighth_block() {
151        // end=33% of width 20 → 6 full blocks + a 4/8 left block (▌).
152        assert_eq!(render(0.0, 33.0), "\x1b[39;49m██████▌             \x1b[0m");
153    }
154}