ratatui_widgets/
gauge.rs

1//! The [`Gauge`] widget is used to display a horizontal progress bar.
2use 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/// A widget to display a progress bar.
16///
17/// A `Gauge` renders a bar filled according to the value given to [`Gauge::percent`] or
18/// [`Gauge::ratio`]. The bar width and height are defined by the [`Rect`] it is
19/// [rendered](Widget::render) in.
20///
21/// The associated label is always centered horizontally and vertically. If not set with
22/// [`Gauge::label`], the label is the percentage of the bar filled.
23///
24/// You might want to have a higher precision bar using [`Gauge::use_unicode`].
25///
26/// This can be useful to indicate the progression of a task, like a download.
27///
28/// # Example
29///
30/// ```
31/// use ratatui::style::{Style, Stylize};
32/// use ratatui::widgets::{Block, Gauge};
33///
34/// Gauge::default()
35///     .block(Block::bordered().title("Progress"))
36///     .gauge_style(Style::new().white().on_black().italic())
37///     .percent(20);
38/// ```
39///
40/// # See also
41///
42/// - [`LineGauge`] for a thin progress bar
43#[expect(clippy::struct_field_names)] // gauge_style needs to be differentiated to style
44#[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    /// Surrounds the `Gauge` with a [`Block`].
56    ///
57    /// The gauge is rendered in the inner portion of the block once space for borders and padding
58    /// is reserved. Styles set on the block do **not** affect the bar itself.
59    #[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    /// Sets the bar progression from a percentage.
66    ///
67    /// # Panics
68    ///
69    /// This method panics if `percent` is **not** between 0 and 100 inclusively.
70    ///
71    /// # See also
72    ///
73    /// See [`Gauge::ratio`] to set from a float.
74    #[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    /// Sets the bar progression from a ratio (float).
85    ///
86    /// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
87    /// This is more easily seen as a floating point percentage (e.g. 42% = `0.42`).
88    ///
89    /// # Panics
90    ///
91    /// This method panics if `ratio` is **not** between 0 and 1 inclusively.
92    ///
93    /// # See also
94    ///
95    /// See [`Gauge::percent`] to set from a percentage.
96    #[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    /// Sets the label to display in the center of the bar.
107    ///
108    /// For a left-aligned label, see [`LineGauge`].
109    /// If the label is not defined, it is the percentage filled.
110    #[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    /// Sets the widget style.
120    ///
121    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
122    /// your own type that implements [`Into<Style>`]).
123    ///
124    /// This will style the block (if any non-styled) and background of the widget (everything
125    /// except the bar itself). [`Block`] style set with [`Gauge::block`] takes precedence.
126    #[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    /// Sets the style of the bar.
133    ///
134    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
135    /// your own type that implements [`Into<Style>`]).
136    #[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    /// Sets whether to use unicode characters to display the progress bar.
143    ///
144    /// This enables the use of
145    /// [unicode block characters](https://en.wikipedia.org/wiki/Block_Elements).
146    /// This is useful to display a higher precision bar (8 extra fractional parts per cell).
147    #[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        // compute label value and its position
178        // label is put at the center of the gauge_area
179        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        // the gauge will be filled proportionally to the ratio
186        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            // render the filled area (left to end)
194            for x in gauge_area.left()..end {
195                // Use full block for the filled part of the gauge and spaces for the part that is
196                // covered by the label. Note that the background and foreground colors are swapped
197                // for the label part, otherwise the gauge will be inverted
198                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        // render the label
215        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/// A compact widget to display a progress bar over a single thin line.
234///
235/// This can be useful to indicate the progression of a task, like a download.
236///
237/// A `LineGauge` renders a line filled with symbols defined by [`LineGauge::filled_symbol`] and
238/// [`LineGauge::unfilled_symbol`] according to the value given to [`LineGauge::ratio`].
239/// Unlike [`Gauge`], only the width can be defined by the [rendering](Widget::render) [`Rect`]. The
240/// height is always 1.
241///
242/// The associated label is always left-aligned. If not set with [`LineGauge::label`], the label is
243/// the percentage of the bar filled.
244///
245/// You can also set the symbols used to draw the bar with [`LineGauge::line_set`].
246///
247/// To style the gauge line use [`LineGauge::filled_style`] and [`LineGauge::unfilled_style`] which
248/// let you pick a color for foreground (i.e. line) and background of the filled and unfilled part
249/// of gauge respectively.
250///
251/// # Examples:
252///
253/// ```
254/// use ratatui::style::{Style, Stylize};
255/// use ratatui::symbols;
256/// use ratatui::widgets::{Block, LineGauge};
257///
258/// LineGauge::default()
259///     .block(Block::bordered().title("Progress"))
260///     .filled_style(Style::new().white().on_black().bold())
261///     .filled_symbol(symbols::line::THICK_HORIZONTAL)
262///     .ratio(0.4);
263/// ```
264///
265/// # See also
266///
267/// - [`Gauge`] for bigger, higher precision and more configurable progress bar
268#[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    /// Surrounds the `LineGauge` with a [`Block`].
297    #[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    /// Sets the bar progression from a ratio (float).
304    ///
305    /// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
306    /// This is more easily seen as a floating point percentage (e.g. 42% = `0.42`).
307    ///
308    /// # Panics
309    ///
310    /// This method panics if `ratio` is **not** between 0 and 1 inclusively.
311    #[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    /// Sets the characters to use for the line.
322    ///
323    /// # See also
324    ///
325    /// See [`symbols::line::Set`] for more information. Predefined sets are also available, see
326    /// [`NORMAL`](symbols::line::NORMAL), [`DOUBLE`](symbols::line::DOUBLE) and
327    /// [`THICK`](symbols::line::THICK).
328    #[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    /// Sets the symbol for the filled part of the gauge.
340    #[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    /// Sets the symbol for the unfilled part of the gauge.
347    #[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    /// Sets the label to display.
354    ///
355    /// With `LineGauge`, labels are only on the left, see [`Gauge`] for a centered label.
356    /// If the label is not defined, it is the percentage filled.
357    #[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    /// Sets the widget style.
367    ///
368    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
369    /// your own type that implements [`Into<Style>`]).
370    ///
371    /// This will style everything except the bar itself, so basically the block (if any) and
372    /// background.
373    #[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    /// Sets the style of the bar.
380    ///
381    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
382    /// your own type that implements [`Into<Style>`]).
383    #[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        // maintain backward compatibility, which used the background color of the style as the
389        // unfilled part of the gauge and the foreground color as the filled part of the gauge
390        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    /// Sets the style of filled part of the bar.
398    ///
399    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
400    /// your own type that implements [`Into<Style>`]).
401    #[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    /// Sets the style of the unfilled part of the bar.
408    ///
409    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
410    /// your own type that implements [`Into<Style>`]).
411    #[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        // This should not panic, even if the buffer is too small to render the gauge.
595        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        // This should not panic, even if the buffer is too small to render the line gauge.
604        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        // This should not panic, even if the buffer has zero size.
613        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        // This should not panic, even if the buffer has zero size.
621        line_gauge.render(buffer.area, &mut buffer);
622    }
623}