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