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