ratatui/widgets/
gauge.rs

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/// A widget to display a progress bar.
11///
12/// A `Gauge` renders a bar filled according to the value given to [`Gauge::percent`] or
13/// [`Gauge::ratio`]. The bar width and height are defined by the [`Rect`] it is
14/// [rendered](Widget::render) in.
15///
16/// The associated label is always centered horizontally and vertically. If not set with
17/// [`Gauge::label`], the label is the percentage of the bar filled.
18///
19/// You might want to have a higher precision bar using [`Gauge::use_unicode`].
20///
21/// This can be useful to indicate the progression of a task, like a download.
22///
23/// # Example
24///
25/// ```
26/// use ratatui::{
27///     style::{Style, Stylize},
28///     widgets::{Block, Gauge},
29/// };
30///
31/// Gauge::default()
32///     .block(Block::bordered().title("Progress"))
33///     .gauge_style(Style::new().white().on_black().italic())
34///     .percent(20);
35/// ```
36///
37/// # See also
38///
39/// - [`LineGauge`] for a thin progress bar
40#[allow(clippy::struct_field_names)] // gauge_style needs to be differentiated to style
41#[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    /// Surrounds the `Gauge` with a [`Block`].
53    ///
54    /// The gauge is rendered in the inner portion of the block once space for borders and padding
55    /// is reserved. Styles set on the block do **not** affect the bar itself.
56    #[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    /// Sets the bar progression from a percentage.
63    ///
64    /// # Panics
65    ///
66    /// This method panics if `percent` is **not** between 0 and 100 inclusively.
67    ///
68    /// # See also
69    ///
70    /// See [`Gauge::ratio`] to set from a float.
71    #[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    /// Sets the bar progression from a ratio (float).
82    ///
83    /// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
84    /// This is more easily seen as a floating point percentage (e.g. 42% = `0.42`).
85    ///
86    /// # Panics
87    ///
88    /// This method panics if `ratio` is **not** between 0 and 1 inclusively.
89    ///
90    /// # See also
91    ///
92    /// See [`Gauge::percent`] to set from a percentage.
93    #[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    /// Sets the label to display in the center of the bar.
104    ///
105    /// For a left-aligned label, see [`LineGauge`].
106    /// If the label is not defined, it is the percentage filled.
107    #[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    /// Sets the widget style.
117    ///
118    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
119    /// your own type that implements [`Into<Style>`]).
120    ///
121    /// This will style the block (if any non-styled) and background of the widget (everything
122    /// except the bar itself). [`Block`] style set with [`Gauge::block`] takes precedence.
123    #[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    /// Sets the style of the bar.
130    ///
131    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
132    /// your own type that implements [`Into<Style>`]).
133    #[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    /// Sets whether to use unicode characters to display the progress bar.
140    ///
141    /// This enables the use of
142    /// [unicode block characters](https://en.wikipedia.org/wiki/Block_Elements).
143    /// This is useful to display a higher precision bar (8 extra fractional parts per cell).
144    #[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        // compute label value and its position
175        // label is put at the center of the gauge_area
176        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        // the gauge will be filled proportionally to the ratio
183        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            // render the filled area (left to end)
191            for x in gauge_area.left()..end {
192                // Use full block for the filled part of the gauge and spaces for the part that is
193                // covered by the label. Note that the background and foreground colors are swapped
194                // for the label part, otherwise the gauge will be inverted
195                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        // render the label
212        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/// A compact widget to display a progress bar over a single thin line.
231///
232/// This can be useful to indicate the progression of a task, like a download.
233///
234/// A `LineGauge` renders a thin line filled according to the value given to [`LineGauge::ratio`].
235/// Unlike [`Gauge`], only the width can be defined by the [rendering](Widget::render) [`Rect`]. The
236/// height is always 1.
237///
238/// The associated label is always left-aligned. If not set with [`LineGauge::label`], the label is
239/// the percentage of the bar filled.
240///
241/// You can also set the symbols used to draw the bar with [`LineGauge::line_set`].
242///
243/// To style the gauge line use [`LineGauge::filled_style`] and [`LineGauge::unfilled_style`] which
244/// let you pick a color for foreground (i.e. line) and background of the filled and unfilled part
245/// of gauge respectively.
246///
247/// # Examples:
248///
249/// ```
250/// use ratatui::{
251///     style::{Style, Stylize},
252///     symbols,
253///     widgets::{Block, LineGauge},
254/// };
255///
256/// LineGauge::default()
257///     .block(Block::bordered().title("Progress"))
258///     .filled_style(Style::new().white().on_black().bold())
259///     .line_set(symbols::line::THICK)
260///     .ratio(0.4);
261/// ```
262///
263/// # See also
264///
265/// - [`Gauge`] for bigger, higher precision and more configurable progress bar
266#[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    /// Surrounds the `LineGauge` with a [`Block`].
279    #[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    /// Sets the bar progression from a ratio (float).
286    ///
287    /// `ratio` is the ratio between filled bar over empty bar (i.e. `3/4` completion is `0.75`).
288    /// This is more easily seen as a floating point percentage (e.g. 42% = `0.42`).
289    ///
290    /// # Panics
291    ///
292    /// This method panics if `ratio` is **not** between 0 and 1 inclusively.
293    #[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    /// Sets the characters to use for the line.
304    ///
305    /// # See also
306    ///
307    /// See [`symbols::line::Set`] for more information. Predefined sets are also available, see
308    /// [`NORMAL`](symbols::line::NORMAL), [`DOUBLE`](symbols::line::DOUBLE) and
309    /// [`THICK`](symbols::line::THICK).
310    #[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    /// Sets the label to display.
317    ///
318    /// With `LineGauge`, labels are only on the left, see [`Gauge`] for a centered label.
319    /// If the label is not defined, it is the percentage filled.
320    #[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    /// Sets the widget style.
330    ///
331    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
332    /// your own type that implements [`Into<Style>`]).
333    ///
334    /// This will style everything except the bar itself, so basically the block (if any) and
335    /// background.
336    #[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    /// Sets the style of the bar.
343    ///
344    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
345    /// your own type that implements [`Into<Style>`]).
346    #[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        // maintain backward compatibility, which used the background color of the style as the
355        // unfilled part of the gauge and the foreground color as the filled part of the gauge
356        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    /// Sets the style of filled part of the bar.
364    ///
365    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
366    /// your own type that implements [`Into<Style>`]).
367    #[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    /// Sets the style of the unfilled part of the bar.
374    ///
375    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
376    /// your own type that implements [`Into<Style>`]).
377    #[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}