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.
35    pub fn new(size: f64, begin: f64, end: f64) -> Self {
36        Bar {
37            size,
38            begin,
39            end,
40            width: None,
41            // Upstream default: `color="default"`, `bgcolor="default"`.
42            style: Style::new()
43                .with_color(Color::default_color())
44                .with_bgcolor(Color::default_color()),
45        }
46    }
47
48    /// The bar colour (upstream `color=`, default `"default"`).
49    pub fn color(mut self, color: Color) -> Self {
50        self.style = Style::from_color(Some(color), self.style.bgcolor().cloned());
51        self
52    }
53
54    /// The background colour (upstream `bgcolor=`, default `"default"`).
55    pub fn bgcolor(mut self, bgcolor: Color) -> Self {
56        self.style = Style::from_color(self.style.color().cloned(), Some(bgcolor));
57        self
58    }
59
60    /// Fix the bar width (otherwise it fills the available width).
61    pub fn width(mut self, width: usize) -> Self {
62        self.width = Some(width);
63        self
64    }
65}
66
67impl Renderable for Bar {
68    /// Port of `Bar.__rich_measure__`: its fixed width, else at least four
69    /// cells and up to the whole width.
70    fn measure(&self, _console: &Console, options: &ConsoleOptions) -> Measurement {
71        match self.width {
72            Some(width) => Measurement::new(width, width),
73            None => Measurement::new(4, options.max_width),
74        }
75    }
76
77    fn rich_render(&self, _console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
78        let width = self
79            .width
80            .unwrap_or(options.max_width)
81            .min(options.max_width);
82        let style = Some(self.style.clone());
83
84        if self.begin >= self.end {
85            return vec![Segment::new(" ".repeat(width), style)];
86        }
87
88        let prefix_complete_eighths = (width as f64 * 8.0 * self.begin / self.size) as usize;
89        let prefix_bar_count = prefix_complete_eighths / 8;
90        let prefix_eighths = prefix_complete_eighths % 8;
91
92        let body_complete_eighths = (width as f64 * 8.0 * self.end / self.size) as usize;
93        let body_bar_count = body_complete_eighths / 8;
94        let body_eighths = body_complete_eighths % 8;
95
96        let mut prefix = " ".repeat(prefix_bar_count);
97        if prefix_eighths != 0 {
98            prefix.push_str(BEGIN_BLOCK_ELEMENTS[prefix_eighths]);
99        }
100
101        let mut body = FULL_BLOCK.repeat(body_bar_count);
102        if body_eighths != 0 {
103            body.push_str(END_BLOCK_ELEMENTS[body_eighths]);
104        }
105
106        let body_len = body.chars().count();
107        let suffix = " ".repeat(width.saturating_sub(body_len));
108
109        // Overlay: prefix, then the body from `len(prefix)` onward, then the suffix.
110        let prefix_len = prefix.chars().count();
111        let body_tail: String = body.chars().skip(prefix_len).collect();
112        let line = format!("{prefix}{body_tail}{suffix}");
113
114        vec![Segment::new(line, style)]
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::color::ColorSystem;
122
123    fn render(begin: f64, end: f64) -> String {
124        let console = Console::builder()
125            .force_terminal(true)
126            .color_system(Some(ColorSystem::Truecolor))
127            .width(20)
128            .build();
129        console.render_to_string(&Bar::new(100.0, begin, end).width(20))
130    }
131
132    #[test]
133    fn full_bar() {
134        assert_eq!(
135            render(0.0, 100.0),
136            format!("\x1b[39;49m{}\x1b[0m", FULL_BLOCK.repeat(20))
137        );
138    }
139
140    #[test]
141    fn half_bar() {
142        assert_eq!(
143            render(0.0, 50.0),
144            format!("\x1b[39;49m{}          \x1b[0m", FULL_BLOCK.repeat(10))
145        );
146    }
147
148    #[test]
149    fn partial_edge_uses_eighth_block() {
150        // end=33% of width 20 → 6 full blocks + a 4/8 left block (▌).
151        assert_eq!(render(0.0, 33.0), "\x1b[39;49m██████▌             \x1b[0m");
152    }
153}