tui_bars/
valuebar.rs

1use std::borrow::Cow;
2
3use tui::{
4    buffer::Buffer,
5    layout::{Direction, Rect},
6    style::{Color, Style},
7    widgets::{Block, Widget},
8};
9
10/// A symmetrical gauge for a value
11#[derive(Debug, Clone)]
12pub struct ValueBar<'a> {
13    value: f32,
14    label: Cow<'a, str>,
15    direction: Direction,
16    style: Style,
17    block: Option<Block<'a>>,
18    range: f32,
19}
20
21impl<'a> Default for ValueBar<'a> {
22    fn default() -> Self {
23        Self {
24            value: 0.,
25            range: 1.,
26            direction: Direction::Horizontal,
27            label: "".into(),
28            style: Style::default(),
29            block: None,
30        }
31    }
32}
33
34impl<'a> ValueBar<'a> {
35    /// Set the value how much this bar should be filled. Should be between [`-range`..`range`]
36    pub fn value(mut self, value: f32) -> Self {
37        self.value = value;
38        self
39    }
40
41    /// The upper and lower bound of the gauge.
42    pub fn range(mut self, range: f32) -> Self {
43        self.range = range;
44        self
45    }
46
47    /// Show a label at the zero position of the bar. By default no label is shown.
48    /// If width of bar is too small, the label won't be rendered.
49    pub fn label<T>(mut self, label: T) -> Self
50    where
51        T: Into<Cow<'a, str>>,
52    {
53        self.label = label.into();
54        self
55    }
56
57    /// Set that this bar is filling horizontally (default) or vertically
58    pub fn direction(mut self, direction: Direction) -> Self {
59        self.direction = direction;
60        self
61    }
62
63    /// Surround this bar by a [Block]
64    pub fn block(mut self, block: Block<'a>) -> Self {
65        self.block = Some(block);
66        self
67    }
68
69    /// Apply a custom style to this bar
70    pub fn style(mut self, style: Style) -> Self {
71        self.style = style;
72        self
73    }
74
75    fn symbol(&self, p: i32) -> &str {
76        use Direction::*;
77        let negative = self.value < 0.;
78        match (p, negative, &self.direction) {
79            (..=-8, true, Horizontal) => "█",
80            (-7, true, Horizontal) => "🮋",
81            (-6, true, Horizontal) => "🮊",
82            (-5, true, Horizontal) => "🮉",
83            (-4, true, Horizontal) => "▐",
84            (-3, true, Horizontal) => "🮈",
85            (-2, true, Horizontal) => "🮇",
86            (-1, true, Horizontal) => "▕",
87            (0 | 1, false, Horizontal) => "▏",
88            (2, false, Horizontal) => "▎",
89            (3, false, Horizontal) => "▍",
90            (4, false, Horizontal) => "▌",
91            (5, false, Horizontal) => "▋",
92            (6, false, Horizontal) => "▊",
93            (7, false, Horizontal) => "▉",
94            (8.., false, Horizontal) => "█",
95            (..=-8, true, Vertical) => "█",
96            (-7, true, Vertical) => "🮆",
97            (-6, true, Vertical) => "🮅",
98            (-5, true, Vertical) => "🮄",
99            (-4, true, Vertical) => "▀",
100            (-3, true, Vertical) => "🮃",
101            (-2, true, Vertical) => "🮂",
102            (-1, true, Vertical) => "▔",
103            (0 | 1, false, Vertical) => "▁",
104            (2, false, Vertical) => "▂",
105            (3, false, Vertical) => "▃",
106            (4, false, Vertical) => "▄",
107            (5, false, Vertical) => "▅",
108            (6, false, Vertical) => "▆",
109            (7, false, Vertical) => "▇",
110            (8.., false, Vertical) => "█",
111            _ => " ",
112        }
113    }
114}
115
116impl<'a> Widget for ValueBar<'a> {
117    fn render(mut self, area: Rect, buffer: &mut Buffer) {
118        let area = match self.block.take() {
119            Some(block) => {
120                let inner = block.inner(area);
121                block.render(area, buffer);
122                inner
123            }
124            None => area,
125        };
126        let (length, width, start) = match self.direction {
127            Direction::Horizontal => (area.width, area.height, area.left()),
128            Direction::Vertical => (area.height, area.width, area.top()),
129        };
130        if width < 1 {
131            // Not enough space to render?
132            return;
133        }
134
135        let units_per_px = 2. * self.range / length as f32;
136        let center_row = area.top() + area.height.saturating_sub(1) / 2;
137        let center_col = start + length / 2;
138        let label_start =
139            (area.left() + area.width / 2).saturating_sub(self.label.len() as u16 / 2);
140        for y in area.top()..area.bottom() {
141            for x in area.left()..area.right() {
142                let px = units_per_px
143                    * match self.direction {
144                        Direction::Horizontal => x as f32 - center_col as f32,
145                        Direction::Vertical => center_row as f32 - y as f32,
146                    };
147                // println!("{center_row} - {y} * {units_per_px}");
148                let symbol = if px < 0. && self.value < 0. {
149                    self.symbol(((self.value - px) / units_per_px * 8. - 8.).round() as i32)
150                } else if px >= 0. && self.value >= 0. {
151                    self.symbol(((self.value - px) / units_per_px * 8.).round() as i32)
152                } else {
153                    " "
154                };
155
156                let cell = buffer.get_mut(x, y);
157                cell.set_style(self.style);
158                cell.set_symbol(symbol);
159
160                if y != center_row {
161                    continue;
162                }
163                if area.width < self.label.len() as u16 {
164                    // Not enough space to render label
165                    continue;
166                }
167                let idx = x
168                    .checked_sub(label_start)
169                    .and_then(|x| self.label.chars().nth(x as usize));
170                if let Some(c) = idx {
171                    cell.set_char(c);
172                    cell.set_style(if symbol == "█" {
173                        Style::default()
174                            .fg(Color::Reset)
175                            .bg(self.style.fg.unwrap_or(Color::Reset))
176                    } else {
177                        self.style
178                    });
179                }
180            }
181        }
182    }
183}