1use crate::{
2 buffer::Buffer,
3 layout::Rect,
4 style::{Color, Style, Styled},
5 symbols::{self},
6 text::{Line, Span},
7 widgets::{block::BlockExt, Block, Widget, WidgetRef},
8};
9
10#[allow(clippy::struct_field_names)] #[derive(Debug, Default, Clone, PartialEq)]
42pub struct Gauge<'a> {
43 block: Option<Block<'a>>,
44 ratio: f64,
45 label: Option<Span<'a>>,
46 use_unicode: bool,
47 style: Style,
48 gauge_style: Style,
49}
50
51impl<'a> Gauge<'a> {
52 #[must_use = "method moves the value of self and returns the modified value"]
57 pub fn block(mut self, block: Block<'a>) -> Self {
58 self.block = Some(block);
59 self
60 }
61
62 #[must_use = "method moves the value of self and returns the modified value"]
72 pub fn percent(mut self, percent: u16) -> Self {
73 assert!(
74 percent <= 100,
75 "Percentage should be between 0 and 100 inclusively."
76 );
77 self.ratio = f64::from(percent) / 100.0;
78 self
79 }
80
81 #[must_use = "method moves the value of self and returns the modified value"]
94 pub fn ratio(mut self, ratio: f64) -> Self {
95 assert!(
96 (0.0..=1.0).contains(&ratio),
97 "Ratio should be between 0 and 1 inclusively."
98 );
99 self.ratio = ratio;
100 self
101 }
102
103 #[must_use = "method moves the value of self and returns the modified value"]
108 pub fn label<T>(mut self, label: T) -> Self
109 where
110 T: Into<Span<'a>>,
111 {
112 self.label = Some(label.into());
113 self
114 }
115
116 #[must_use = "method moves the value of self and returns the modified value"]
124 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
125 self.style = style.into();
126 self
127 }
128
129 #[must_use = "method moves the value of self and returns the modified value"]
134 pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
135 self.gauge_style = style.into();
136 self
137 }
138
139 #[must_use = "method moves the value of self and returns the modified value"]
145 pub const fn use_unicode(mut self, unicode: bool) -> Self {
146 self.use_unicode = unicode;
147 self
148 }
149}
150
151impl Widget for Gauge<'_> {
152 fn render(self, area: Rect, buf: &mut Buffer) {
153 self.render_ref(area, buf);
154 }
155}
156
157impl WidgetRef for Gauge<'_> {
158 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
159 buf.set_style(area, self.style);
160 self.block.render_ref(area, buf);
161 let inner = self.block.inner_if_some(area);
162 self.render_gauge(inner, buf);
163 }
164}
165
166impl Gauge<'_> {
167 fn render_gauge(&self, gauge_area: Rect, buf: &mut Buffer) {
168 if gauge_area.is_empty() {
169 return;
170 }
171
172 buf.set_style(gauge_area, self.gauge_style);
173
174 let default_label = Span::raw(format!("{}%", f64::round(self.ratio * 100.0)));
177 let label = self.label.as_ref().unwrap_or(&default_label);
178 let clamped_label_width = gauge_area.width.min(label.width() as u16);
179 let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
180 let label_row = gauge_area.top() + gauge_area.height / 2;
181
182 let filled_width = f64::from(gauge_area.width) * self.ratio;
184 let end = if self.use_unicode {
185 gauge_area.left() + filled_width.floor() as u16
186 } else {
187 gauge_area.left() + filled_width.round() as u16
188 };
189 for y in gauge_area.top()..gauge_area.bottom() {
190 for x in gauge_area.left()..end {
192 if x < label_col || x > label_col + clamped_label_width || y != label_row {
196 buf[(x, y)]
197 .set_symbol(symbols::block::FULL)
198 .set_fg(self.gauge_style.fg.unwrap_or(Color::Reset))
199 .set_bg(self.gauge_style.bg.unwrap_or(Color::Reset));
200 } else {
201 buf[(x, y)]
202 .set_symbol(" ")
203 .set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
204 .set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
205 }
206 }
207 if self.use_unicode && self.ratio < 1.0 {
208 buf[(end, y)].set_symbol(get_unicode_block(filled_width % 1.0));
209 }
210 }
211 buf.set_span(label_col, label_row, label, clamped_label_width);
213 }
214}
215
216fn get_unicode_block<'a>(frac: f64) -> &'a str {
217 match (frac * 8.0).round() as u16 {
218 1 => symbols::block::ONE_EIGHTH,
219 2 => symbols::block::ONE_QUARTER,
220 3 => symbols::block::THREE_EIGHTHS,
221 4 => symbols::block::HALF,
222 5 => symbols::block::FIVE_EIGHTHS,
223 6 => symbols::block::THREE_QUARTERS,
224 7 => symbols::block::SEVEN_EIGHTHS,
225 8 => symbols::block::FULL,
226 _ => " ",
227 }
228}
229
230#[derive(Debug, Default, Clone, PartialEq)]
267pub struct LineGauge<'a> {
268 block: Option<Block<'a>>,
269 ratio: f64,
270 label: Option<Line<'a>>,
271 line_set: symbols::line::Set,
272 style: Style,
273 filled_style: Style,
274 unfilled_style: Style,
275}
276
277impl<'a> LineGauge<'a> {
278 #[must_use = "method moves the value of self and returns the modified value"]
280 pub fn block(mut self, block: Block<'a>) -> Self {
281 self.block = Some(block);
282 self
283 }
284
285 #[must_use = "method moves the value of self and returns the modified value"]
294 pub fn ratio(mut self, ratio: f64) -> Self {
295 assert!(
296 (0.0..=1.0).contains(&ratio),
297 "Ratio should be between 0 and 1 inclusively."
298 );
299 self.ratio = ratio;
300 self
301 }
302
303 #[must_use = "method moves the value of self and returns the modified value"]
311 pub const fn line_set(mut self, set: symbols::line::Set) -> Self {
312 self.line_set = set;
313 self
314 }
315
316 #[must_use = "method moves the value of self and returns the modified value"]
321 pub fn label<T>(mut self, label: T) -> Self
322 where
323 T: Into<Line<'a>>,
324 {
325 self.label = Some(label.into());
326 self
327 }
328
329 #[must_use = "method moves the value of self and returns the modified value"]
337 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
338 self.style = style.into();
339 self
340 }
341
342 #[deprecated(
347 since = "0.27.0",
348 note = "You should use `LineGauge::filled_style` instead."
349 )]
350 #[must_use = "method moves the value of self and returns the modified value"]
351 pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
352 let style: Style = style.into();
353
354 let filled_color = style.fg.unwrap_or(Color::Reset);
357 let unfilled_color = style.bg.unwrap_or(Color::Reset);
358 self.filled_style = style.fg(filled_color).bg(Color::Reset);
359 self.unfilled_style = style.fg(unfilled_color).bg(Color::Reset);
360 self
361 }
362
363 #[must_use = "method moves the value of self and returns the modified value"]
368 pub fn filled_style<S: Into<Style>>(mut self, style: S) -> Self {
369 self.filled_style = style.into();
370 self
371 }
372
373 #[must_use = "method moves the value of self and returns the modified value"]
378 pub fn unfilled_style<S: Into<Style>>(mut self, style: S) -> Self {
379 self.unfilled_style = style.into();
380 self
381 }
382}
383
384impl Widget for LineGauge<'_> {
385 fn render(self, area: Rect, buf: &mut Buffer) {
386 self.render_ref(area, buf);
387 }
388}
389
390impl WidgetRef for LineGauge<'_> {
391 fn render_ref(&self, area: Rect, buf: &mut Buffer) {
392 buf.set_style(area, self.style);
393 self.block.render_ref(area, buf);
394 let gauge_area = self.block.inner_if_some(area);
395 if gauge_area.is_empty() {
396 return;
397 }
398
399 let ratio = self.ratio;
400 let default_label = Line::from(format!("{:.0}%", ratio * 100.0));
401 let label = self.label.as_ref().unwrap_or(&default_label);
402 let (col, row) = buf.set_line(gauge_area.left(), gauge_area.top(), label, gauge_area.width);
403 let start = col + 1;
404 if start >= gauge_area.right() {
405 return;
406 }
407
408 let end = start
409 + (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
410 for col in start..end {
411 buf[(col, row)]
412 .set_symbol(self.line_set.horizontal)
413 .set_style(self.filled_style);
414 }
415 for col in end..gauge_area.right() {
416 buf[(col, row)]
417 .set_symbol(self.line_set.horizontal)
418 .set_style(self.unfilled_style);
419 }
420 }
421}
422
423impl<'a> Styled for Gauge<'a> {
424 type Item = Self;
425
426 fn style(&self) -> Style {
427 self.style
428 }
429
430 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
431 self.style(style)
432 }
433}
434
435impl<'a> Styled for LineGauge<'a> {
436 type Item = Self;
437
438 fn style(&self) -> Style {
439 self.style
440 }
441
442 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
443 self.style(style)
444 }
445}
446
447#[cfg(test)]
448mod tests {
449 use super::*;
450 use crate::{
451 style::{Color, Modifier, Style, Stylize},
452 symbols,
453 };
454 #[test]
455 #[should_panic = "Percentage should be between 0 and 100 inclusively"]
456 fn gauge_invalid_percentage() {
457 let _ = Gauge::default().percent(110);
458 }
459
460 #[test]
461 #[should_panic = "Ratio should be between 0 and 1 inclusively"]
462 fn gauge_invalid_ratio_upper_bound() {
463 let _ = Gauge::default().ratio(1.1);
464 }
465
466 #[test]
467 #[should_panic = "Ratio should be between 0 and 1 inclusively"]
468 fn gauge_invalid_ratio_lower_bound() {
469 let _ = Gauge::default().ratio(-0.5);
470 }
471
472 #[test]
473 fn gauge_can_be_stylized() {
474 assert_eq!(
475 Gauge::default().black().on_white().bold().not_dim().style,
476 Style::default()
477 .fg(Color::Black)
478 .bg(Color::White)
479 .add_modifier(Modifier::BOLD)
480 .remove_modifier(Modifier::DIM)
481 );
482 }
483
484 #[test]
485 fn line_gauge_can_be_stylized() {
486 assert_eq!(
487 LineGauge::default()
488 .black()
489 .on_white()
490 .bold()
491 .not_dim()
492 .style,
493 Style::default()
494 .fg(Color::Black)
495 .bg(Color::White)
496 .add_modifier(Modifier::BOLD)
497 .remove_modifier(Modifier::DIM)
498 );
499 }
500
501 #[allow(deprecated)]
502 #[test]
503 fn line_gauge_can_be_stylized_with_deprecated_gauge_style() {
504 let gauge =
505 LineGauge::default().gauge_style(Style::default().fg(Color::Red).bg(Color::Blue));
506
507 assert_eq!(
508 gauge.filled_style,
509 Style::default().fg(Color::Red).bg(Color::Reset)
510 );
511
512 assert_eq!(
513 gauge.unfilled_style,
514 Style::default().fg(Color::Blue).bg(Color::Reset)
515 );
516 }
517
518 #[test]
519 fn line_gauge_default() {
520 assert_eq!(
521 LineGauge::default(),
522 LineGauge {
523 block: None,
524 ratio: 0.0,
525 label: None,
526 style: Style::default(),
527 line_set: symbols::line::NORMAL,
528 filled_style: Style::default(),
529 unfilled_style: Style::default()
530 }
531 );
532 }
533}