bottom/canvas/components/
pipe_gauge.rs

1use tui::{
2    buffer::Buffer,
3    layout::Rect,
4    style::Style,
5    text::Line,
6    widgets::{Block, Widget},
7};
8
9#[derive(Debug, Clone, Copy)]
10pub enum LabelLimit {
11    None,
12    #[expect(dead_code)]
13    Auto(u16),
14    Bars,
15    StartLabel,
16}
17
18impl Default for LabelLimit {
19    fn default() -> Self {
20        Self::None
21    }
22}
23
24/// A widget to measure something, using pipe characters ('|') as a unit.
25#[derive(Debug, Clone)]
26pub struct PipeGauge<'a> {
27    block: Option<Block<'a>>,
28    ratio: f64,
29    start_label: Option<Line<'a>>,
30    inner_label: Option<Line<'a>>,
31    label_style: Style,
32    gauge_style: Style,
33    hide_parts: LabelLimit,
34}
35
36impl Default for PipeGauge<'_> {
37    fn default() -> Self {
38        Self {
39            block: None,
40            ratio: 0.0,
41            start_label: None,
42            inner_label: None,
43            label_style: Style::default(),
44            gauge_style: Style::default(),
45            hide_parts: LabelLimit::default(),
46        }
47    }
48}
49
50impl<'a> PipeGauge<'a> {
51    /// The ratio, a value from 0.0 to 1.0 (any other greater or less will be
52    /// clamped) represents the portion of the pipe gauge to fill.
53    ///
54    /// Note: passing in NaN will potentially cause problems.
55    pub fn ratio(mut self, ratio: f64) -> Self {
56        self.ratio = ratio.clamp(0.0, 1.0);
57
58        self
59    }
60
61    /// The label displayed before the bar.
62    pub fn start_label<T>(mut self, start_label: T) -> Self
63    where
64        T: Into<Line<'a>>,
65    {
66        self.start_label = Some(start_label.into());
67        self
68    }
69
70    /// The label displayed inside the bar.
71    pub fn inner_label<T>(mut self, inner_label: T) -> Self
72    where
73        T: Into<Line<'a>>,
74    {
75        self.inner_label = Some(inner_label.into());
76        self
77    }
78
79    /// The style of the labels.
80    pub fn label_style(mut self, label_style: Style) -> Self {
81        self.label_style = label_style;
82        self
83    }
84
85    /// The style of the gauge itself.
86    pub fn gauge_style(mut self, style: Style) -> Self {
87        self.gauge_style = style;
88        self
89    }
90
91    /// Whether to hide parts of the gauge/label if the inner label wouldn't
92    /// fit.
93    pub fn hide_parts(mut self, hide_parts: LabelLimit) -> Self {
94        self.hide_parts = hide_parts;
95        self
96    }
97}
98
99impl Widget for PipeGauge<'_> {
100    fn render(mut self, area: Rect, buf: &mut Buffer) {
101        buf.set_style(area, self.label_style);
102        let gauge_area = match self.block.take() {
103            Some(b) => {
104                let inner_area = b.inner(area);
105                b.render(area, buf);
106                inner_area
107            }
108            None => area,
109        };
110
111        if gauge_area.height < 1 {
112            return;
113        }
114
115        let (col, row) = {
116            let inner_label_width = self
117                .inner_label
118                .as_ref()
119                .map(|l| l.width())
120                .unwrap_or_default();
121
122            let start_label_width = self
123                .start_label
124                .as_ref()
125                .map(|l| l.width())
126                .unwrap_or_default();
127
128            match self.hide_parts {
129                LabelLimit::StartLabel => {
130                    let inner_label = self.inner_label.unwrap_or_else(|| Line::from(""));
131                    let _ = buf.set_line(
132                        gauge_area.left(),
133                        gauge_area.top(),
134                        &inner_label,
135                        inner_label.width() as u16,
136                    );
137
138                    // Short circuit.
139                    return;
140                }
141                LabelLimit::Auto(_)
142                    if gauge_area.width < (inner_label_width + start_label_width + 1) as u16 =>
143                {
144                    let inner_label = self.inner_label.unwrap_or_else(|| Line::from(""));
145                    let _ = buf.set_line(
146                        gauge_area.left(),
147                        gauge_area.top(),
148                        &inner_label,
149                        inner_label.width() as u16,
150                    );
151
152                    // Short circuit.
153                    return;
154                }
155                _ => {
156                    let start_label = self.start_label.unwrap_or_else(|| Line::from(""));
157                    buf.set_line(
158                        gauge_area.left(),
159                        gauge_area.top(),
160                        &start_label,
161                        start_label.width() as u16,
162                    )
163                }
164            }
165        };
166
167        let end_label = self.inner_label.unwrap_or_else(|| Line::from(""));
168        match self.hide_parts {
169            LabelLimit::Bars => {
170                let _ = buf.set_line(
171                    gauge_area
172                        .right()
173                        .saturating_sub(end_label.width() as u16 + 1),
174                    row,
175                    &end_label,
176                    end_label.width() as u16,
177                );
178            }
179            LabelLimit::Auto(width_limit)
180                if gauge_area.right().saturating_sub(col) < width_limit =>
181            {
182                let _ = buf.set_line(
183                    gauge_area
184                        .right()
185                        .saturating_sub(end_label.width() as u16 + 1),
186                    row,
187                    &end_label,
188                    1,
189                );
190            }
191            LabelLimit::Auto(_) | LabelLimit::None => {
192                let (start, _) = buf.set_line(col, row, &Line::from("["), gauge_area.width);
193                if start >= gauge_area.right() {
194                    return;
195                }
196
197                let (end, _) = buf.set_line(
198                    (gauge_area.x + gauge_area.width).saturating_sub(1),
199                    row,
200                    &Line::from("]"),
201                    gauge_area.width,
202                );
203
204                let pipe_end =
205                    start + (f64::from(end.saturating_sub(start)) * self.ratio).floor() as u16;
206                for col in start..pipe_end {
207                    if let Some(cell) = buf.cell_mut((col, row)) {
208                        cell.set_symbol("|").set_style(Style {
209                            fg: self.gauge_style.fg,
210                            bg: None,
211                            add_modifier: self.gauge_style.add_modifier,
212                            sub_modifier: self.gauge_style.sub_modifier,
213                            underline_color: None,
214                        });
215                    }
216                }
217
218                if (end_label.width() as u16) < end.saturating_sub(start) {
219                    let gauge_end = gauge_area
220                        .right()
221                        .saturating_sub(end_label.width() as u16 + 1);
222                    buf.set_line(gauge_end, row, &end_label, end_label.width() as u16);
223                }
224            }
225            LabelLimit::StartLabel => unreachable!(),
226        }
227    }
228}