bottom/canvas/components/
pipe_gauge.rs1use 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#[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 pub fn ratio(mut self, ratio: f64) -> Self {
56 self.ratio = ratio.clamp(0.0, 1.0);
57
58 self
59 }
60
61 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 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 pub fn label_style(mut self, label_style: Style) -> Self {
81 self.label_style = label_style;
82 self
83 }
84
85 pub fn gauge_style(mut self, style: Style) -> Self {
87 self.gauge_style = style;
88 self
89 }
90
91 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 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 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}