1use alloc::format;
3
4use ratatui_core::buffer::Buffer;
5use ratatui_core::layout::Rect;
6use ratatui_core::style::{Color, Style, Styled};
7use ratatui_core::symbols;
8use ratatui_core::text::{Line, Span};
9use ratatui_core::widgets::Widget;
10
11use crate::block::{Block, BlockExt};
12#[cfg(not(feature = "std"))]
13use crate::polyfills::F64Polyfills;
14
15#[expect(clippy::struct_field_names)] #[derive(Debug, Default, Clone, PartialEq)]
45pub struct Gauge<'a> {
46 block: Option<Block<'a>>,
47 ratio: f64,
48 label: Option<Span<'a>>,
49 use_unicode: bool,
50 style: Style,
51 gauge_style: Style,
52}
53
54impl<'a> Gauge<'a> {
55 #[must_use = "method moves the value of self and returns the modified value"]
60 pub fn block(mut self, block: Block<'a>) -> Self {
61 self.block = Some(block);
62 self
63 }
64
65 #[must_use = "method moves the value of self and returns the modified value"]
75 pub fn percent(mut self, percent: u16) -> Self {
76 assert!(
77 percent <= 100,
78 "Percentage should be between 0 and 100 inclusively."
79 );
80 self.ratio = f64::from(percent) / 100.0;
81 self
82 }
83
84 #[must_use = "method moves the value of self and returns the modified value"]
97 pub fn ratio(mut self, ratio: f64) -> Self {
98 assert!(
99 (0.0..=1.0).contains(&ratio),
100 "Ratio should be between 0 and 1 inclusively."
101 );
102 self.ratio = ratio;
103 self
104 }
105
106 #[must_use = "method moves the value of self and returns the modified value"]
111 pub fn label<T>(mut self, label: T) -> Self
112 where
113 T: Into<Span<'a>>,
114 {
115 self.label = Some(label.into());
116 self
117 }
118
119 #[must_use = "method moves the value of self and returns the modified value"]
127 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
128 self.style = style.into();
129 self
130 }
131
132 #[must_use = "method moves the value of self and returns the modified value"]
137 pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
138 self.gauge_style = style.into();
139 self
140 }
141
142 #[must_use = "method moves the value of self and returns the modified value"]
148 pub const fn use_unicode(mut self, unicode: bool) -> Self {
149 self.use_unicode = unicode;
150 self
151 }
152}
153
154impl Widget for Gauge<'_> {
155 fn render(self, area: Rect, buf: &mut Buffer) {
156 Widget::render(&self, area, buf);
157 }
158}
159
160impl Widget for &Gauge<'_> {
161 fn render(self, area: Rect, buf: &mut Buffer) {
162 buf.set_style(area, self.style);
163 self.block.as_ref().render(area, buf);
164 let inner = self.block.inner_if_some(area);
165 self.render_gauge(inner, buf);
166 }
167}
168
169impl Gauge<'_> {
170 fn render_gauge(&self, gauge_area: Rect, buf: &mut Buffer) {
171 if gauge_area.is_empty() {
172 return;
173 }
174
175 buf.set_style(gauge_area, self.gauge_style);
176
177 let default_label = Span::raw(format!("{}%", f64::round(self.ratio * 100.0)));
180 let label = self.label.as_ref().unwrap_or(&default_label);
181 let clamped_label_width = gauge_area.width.min(label.width() as u16);
182 let label_col = gauge_area.left() + (gauge_area.width - clamped_label_width) / 2;
183 let label_row = gauge_area.top() + gauge_area.height / 2;
184
185 let filled_width = f64::from(gauge_area.width) * self.ratio;
187 let end = if self.use_unicode {
188 gauge_area.left() + filled_width.floor() as u16
189 } else {
190 gauge_area.left() + filled_width.round() as u16
191 };
192 for y in gauge_area.top()..gauge_area.bottom() {
193 for x in gauge_area.left()..end {
195 if x < label_col || x > label_col + clamped_label_width || y != label_row {
199 buf[(x, y)]
200 .set_symbol(symbols::block::FULL)
201 .set_fg(self.gauge_style.fg.unwrap_or(Color::Reset))
202 .set_bg(self.gauge_style.bg.unwrap_or(Color::Reset));
203 } else {
204 buf[(x, y)]
205 .set_symbol(" ")
206 .set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
207 .set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));
208 }
209 }
210 if self.use_unicode && self.ratio < 1.0 {
211 buf[(end, y)].set_symbol(get_unicode_block(filled_width % 1.0));
212 }
213 }
214 buf.set_span(label_col, label_row, label, clamped_label_width);
216 }
217}
218
219fn get_unicode_block<'a>(frac: f64) -> &'a str {
220 match (frac * 8.0).round() as u16 {
221 1 => symbols::block::ONE_EIGHTH,
222 2 => symbols::block::ONE_QUARTER,
223 3 => symbols::block::THREE_EIGHTHS,
224 4 => symbols::block::HALF,
225 5 => symbols::block::FIVE_EIGHTHS,
226 6 => symbols::block::THREE_QUARTERS,
227 7 => symbols::block::SEVEN_EIGHTHS,
228 8 => symbols::block::FULL,
229 _ => " ",
230 }
231}
232
233#[derive(Debug, Clone, PartialEq)]
269pub struct LineGauge<'a> {
270 block: Option<Block<'a>>,
271 ratio: f64,
272 label: Option<Line<'a>>,
273 style: Style,
274 filled_symbol: &'a str,
275 unfilled_symbol: &'a str,
276 filled_style: Style,
277 unfilled_style: Style,
278}
279
280impl Default for LineGauge<'_> {
281 fn default() -> Self {
282 Self {
283 block: None,
284 ratio: 0.0,
285 label: None,
286 style: Style::default(),
287 filled_symbol: symbols::line::HORIZONTAL,
288 unfilled_symbol: symbols::line::HORIZONTAL,
289 filled_style: Style::default(),
290 unfilled_style: Style::default(),
291 }
292 }
293}
294
295impl<'a> LineGauge<'a> {
296 #[must_use = "method moves the value of self and returns the modified value"]
298 pub fn block(mut self, block: Block<'a>) -> Self {
299 self.block = Some(block);
300 self
301 }
302
303 #[must_use = "method moves the value of self and returns the modified value"]
312 pub fn ratio(mut self, ratio: f64) -> Self {
313 assert!(
314 (0.0..=1.0).contains(&ratio),
315 "Ratio should be between 0 and 1 inclusively."
316 );
317 self.ratio = ratio;
318 self
319 }
320
321 #[must_use = "method moves the value of self and returns the modified value"]
329 #[deprecated(
330 since = "0.30.0",
331 note = "use `filled_symbol()` and `unfilled_symbol()` instead"
332 )]
333 pub const fn line_set(mut self, set: symbols::line::Set<'a>) -> Self {
334 self.filled_symbol = set.horizontal;
335 self.unfilled_symbol = set.horizontal;
336 self
337 }
338
339 #[must_use = "method moves the value of self and returns the modified value"]
341 pub const fn filled_symbol(mut self, symbol: &'a str) -> Self {
342 self.filled_symbol = symbol;
343 self
344 }
345
346 #[must_use = "method moves the value of self and returns the modified value"]
348 pub const fn unfilled_symbol(mut self, symbol: &'a str) -> Self {
349 self.unfilled_symbol = symbol;
350 self
351 }
352
353 #[must_use = "method moves the value of self and returns the modified value"]
358 pub fn label<T>(mut self, label: T) -> Self
359 where
360 T: Into<Line<'a>>,
361 {
362 self.label = Some(label.into());
363 self
364 }
365
366 #[must_use = "method moves the value of self and returns the modified value"]
374 pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
375 self.style = style.into();
376 self
377 }
378
379 #[deprecated(since = "0.27.0", note = "use `filled_style()` instead")]
384 #[must_use = "method moves the value of self and returns the modified value"]
385 pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
386 let style: Style = style.into();
387
388 let filled_color = style.fg.unwrap_or(Color::Reset);
391 let unfilled_color = style.bg.unwrap_or(Color::Reset);
392 self.filled_style = style.fg(filled_color).bg(Color::Reset);
393 self.unfilled_style = style.fg(unfilled_color).bg(Color::Reset);
394 self
395 }
396
397 #[must_use = "method moves the value of self and returns the modified value"]
402 pub fn filled_style<S: Into<Style>>(mut self, style: S) -> Self {
403 self.filled_style = style.into();
404 self
405 }
406
407 #[must_use = "method moves the value of self and returns the modified value"]
412 pub fn unfilled_style<S: Into<Style>>(mut self, style: S) -> Self {
413 self.unfilled_style = style.into();
414 self
415 }
416}
417
418impl Widget for LineGauge<'_> {
419 fn render(self, area: Rect, buf: &mut Buffer) {
420 Widget::render(&self, area, buf);
421 }
422}
423
424impl Widget for &LineGauge<'_> {
425 fn render(self, area: Rect, buf: &mut Buffer) {
426 buf.set_style(area, self.style);
427 self.block.as_ref().render(area, buf);
428 let gauge_area = self.block.inner_if_some(area);
429 if gauge_area.is_empty() {
430 return;
431 }
432
433 let ratio = self.ratio;
434 let default_label = Line::from(format!("{:3.0}%", ratio * 100.0));
435 let label = self.label.as_ref().unwrap_or(&default_label);
436 let (col, row) = buf.set_line(gauge_area.left(), gauge_area.top(), label, gauge_area.width);
437 let start = col + 1;
438 if start >= gauge_area.right() {
439 return;
440 }
441
442 let end = start
443 + (f64::from(gauge_area.right().saturating_sub(start)) * self.ratio).floor() as u16;
444 for col in start..end {
445 buf[(col, row)]
446 .set_symbol(self.filled_symbol)
447 .set_style(self.filled_style);
448 }
449 for col in end..gauge_area.right() {
450 buf[(col, row)]
451 .set_symbol(self.unfilled_symbol)
452 .set_style(self.unfilled_style);
453 }
454 }
455}
456
457impl Styled for Gauge<'_> {
458 type Item = Self;
459
460 fn style(&self) -> Style {
461 self.style
462 }
463
464 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
465 self.style(style)
466 }
467}
468
469impl Styled for LineGauge<'_> {
470 type Item = Self;
471
472 fn style(&self) -> Style {
473 self.style
474 }
475
476 fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
477 self.style(style)
478 }
479}
480
481#[cfg(test)]
482mod tests {
483 use ratatui_core::style::{Color, Modifier, Style, Stylize};
484 use ratatui_core::symbols;
485
486 use super::*;
487
488 #[test]
489 #[should_panic = "Percentage should be between 0 and 100 inclusively"]
490 fn gauge_invalid_percentage() {
491 let _ = Gauge::default().percent(110);
492 }
493
494 #[test]
495 #[should_panic = "Ratio should be between 0 and 1 inclusively"]
496 fn gauge_invalid_ratio_upper_bound() {
497 let _ = Gauge::default().ratio(1.1);
498 }
499
500 #[test]
501 #[should_panic = "Ratio should be between 0 and 1 inclusively"]
502 fn gauge_invalid_ratio_lower_bound() {
503 let _ = Gauge::default().ratio(-0.5);
504 }
505
506 #[test]
507 fn gauge_can_be_stylized() {
508 assert_eq!(
509 Gauge::default().black().on_white().bold().not_dim().style,
510 Style::default()
511 .fg(Color::Black)
512 .bg(Color::White)
513 .add_modifier(Modifier::BOLD)
514 .remove_modifier(Modifier::DIM)
515 );
516 }
517
518 #[test]
519 fn line_gauge_can_be_stylized() {
520 assert_eq!(
521 LineGauge::default()
522 .black()
523 .on_white()
524 .bold()
525 .not_dim()
526 .style,
527 Style::default()
528 .fg(Color::Black)
529 .bg(Color::White)
530 .add_modifier(Modifier::BOLD)
531 .remove_modifier(Modifier::DIM)
532 );
533 }
534
535 #[expect(deprecated)]
536 #[test]
537 fn line_gauge_can_be_stylized_with_deprecated_gauge_style() {
538 let gauge =
539 LineGauge::default().gauge_style(Style::default().fg(Color::Red).bg(Color::Blue));
540
541 assert_eq!(
542 gauge.filled_style,
543 Style::default().fg(Color::Red).bg(Color::Reset)
544 );
545
546 assert_eq!(
547 gauge.unfilled_style,
548 Style::default().fg(Color::Blue).bg(Color::Reset)
549 );
550 }
551
552 #[test]
553 fn line_gauge_set_filled_symbol() {
554 assert_eq!(LineGauge::default().filled_symbol("▰").filled_symbol, "▰");
555 }
556
557 #[test]
558 fn line_gauge_set_unfilled_symbol() {
559 assert_eq!(
560 LineGauge::default().unfilled_symbol("▱").unfilled_symbol,
561 "▱"
562 );
563 }
564
565 #[expect(deprecated)]
566 #[test]
567 fn line_gauge_deprecated_line_set() {
568 let gauge = LineGauge::default().line_set(symbols::line::DOUBLE);
569 assert_eq!(gauge.filled_symbol, symbols::line::DOUBLE.horizontal);
570 assert_eq!(gauge.unfilled_symbol, symbols::line::DOUBLE.horizontal);
571 }
572
573 #[test]
574 fn line_gauge_default() {
575 assert_eq!(
576 LineGauge::default(),
577 LineGauge {
578 block: None,
579 ratio: 0.0,
580 label: None,
581 style: Style::default(),
582 filled_symbol: symbols::line::HORIZONTAL,
583 unfilled_symbol: symbols::line::HORIZONTAL,
584 filled_style: Style::default(),
585 unfilled_style: Style::default()
586 }
587 );
588 }
589
590 #[test]
591 fn render_in_minimal_buffer_gauge() {
592 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
593 let gauge = Gauge::default().percent(50);
594 gauge.render(buffer.area, &mut buffer);
596 assert_eq!(buffer, Buffer::with_lines(["5"]));
597 }
598
599 #[test]
600 fn render_in_minimal_buffer_line_gauge() {
601 let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
602 let line_gauge = LineGauge::default().ratio(0.5);
603 line_gauge.render(buffer.area, &mut buffer);
605 assert_eq!(buffer, Buffer::with_lines([" "]));
606 }
607
608 #[test]
609 fn render_in_zero_size_buffer_gauge() {
610 let mut buffer = Buffer::empty(Rect::ZERO);
611 let gauge = Gauge::default().percent(50);
612 gauge.render(buffer.area, &mut buffer);
614 }
615
616 #[test]
617 fn render_in_zero_size_buffer_line_gauge() {
618 let mut buffer = Buffer::empty(Rect::ZERO);
619 let line_gauge = LineGauge::default().ratio(0.5);
620 line_gauge.render(buffer.area, &mut buffer);
622 }
623}