ratatui_widgets/barchart/
bar.rs

1use alloc::string::{String, ToString};
2
3use ratatui_core::buffer::Buffer;
4use ratatui_core::layout::Rect;
5use ratatui_core::style::{Style, Styled};
6use ratatui_core::text::Line;
7use ratatui_core::widgets::Widget;
8use unicode_width::UnicodeWidthStr;
9
10/// A bar to be shown by the [`BarChart`](super::BarChart) widget.
11///
12/// Here is an explanation of a `Bar`'s components.
13/// ```plain
14/// ███                          ┐
15/// █2█  <- text_value or value  │ bar
16/// foo  <- label                ┘
17/// ```
18/// Note that every element can be styled individually.
19///
20/// # Example
21///
22/// The following example creates a bar with the label "Bar 1", a value "10",
23/// red background and a white value foreground.
24/// ```
25/// use ratatui::style::{Style, Stylize};
26/// use ratatui::widgets::Bar;
27///
28/// Bar::with_label("Bar 1", 10)
29///     .red()
30///     .value_style(Style::new().red().on_white())
31///     .text_value("10°C");
32/// ```
33#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
34pub struct Bar<'a> {
35    /// Value to display on the bar (computed when the data is passed to the widget)
36    pub(super) value: u64,
37    /// optional label to be printed under the bar
38    pub(super) label: Option<Line<'a>>,
39    /// style for the bar
40    pub(super) style: Style,
41    /// style of the value printed at the bottom of the bar.
42    pub(super) value_style: Style,
43    /// optional `text_value` to be shown on the bar instead of the actual value
44    pub(super) text_value: Option<String>,
45}
46
47impl<'a> Bar<'a> {
48    /// Creates a new `Bar` with the given value.
49    ///
50    /// # Examples
51    ///
52    /// ```
53    /// use ratatui::widgets::Bar;
54    ///
55    /// let bar = Bar::new(42);
56    /// ```
57    pub const fn new(value: u64) -> Self {
58        Self {
59            value,
60            label: None,
61            style: Style::new(),
62            value_style: Style::new(),
63            text_value: None,
64        }
65    }
66
67    /// Creates a new `Bar` with the given `label` and value.
68    ///
69    /// a `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
70    ///
71    /// # Examples
72    ///
73    /// ```
74    /// use ratatui::widgets::Bar;
75    ///
76    /// let bar = Bar::with_label("Label", 42);
77    /// ```
78    pub fn with_label<T: Into<Line<'a>>>(label: T, value: u64) -> Self {
79        Self {
80            value,
81            label: Some(label.into()),
82            style: Style::new(),
83            value_style: Style::new(),
84            text_value: None,
85        }
86    }
87
88    /// Set the value of this bar.
89    ///
90    /// The value will be displayed inside the bar.
91    ///
92    /// # See also
93    ///
94    /// - [`Bar::value_style`] to style the value.
95    /// - [`Bar::text_value`] to set the displayed value.
96    #[must_use = "method moves the value of self and returns the modified value"]
97    pub const fn value(mut self, value: u64) -> Self {
98        self.value = value;
99        self
100    }
101
102    /// Set the label of the bar.
103    ///
104    /// `label` can be a [`&str`], [`String`] or anything that can be converted into [`Line`].
105    ///
106    /// # Examples
107    ///
108    /// From [`&str`] and [`String`]:
109    ///
110    /// ```rust
111    /// use ratatui::widgets::Bar;
112    ///
113    /// Bar::default().label("label");
114    /// Bar::default().label(String::from("label"));
115    /// ```
116    ///
117    /// From a [`Line`] with red foreground color:
118    ///
119    /// ```rust
120    /// use ratatui::style::Stylize;
121    /// use ratatui::text::Line;
122    /// use ratatui::widgets::Bar;
123    ///
124    /// Bar::default().label(Line::from("Line").red());
125    /// ```
126    ///
127    /// For [`Vertical`](ratatui_core::layout::Direction::Vertical) bars,
128    /// display the label **under** the bar.
129    /// For [`Horizontal`](ratatui_core::layout::Direction::Horizontal) bars,
130    /// display the label **in** the bar.
131    /// See [`BarChart::direction`](crate::barchart::BarChart::direction) to set the direction.
132    #[must_use = "method moves the value of self and returns the modified value"]
133    pub fn label<T: Into<Line<'a>>>(mut self, label: T) -> Self {
134        self.label = Some(label.into());
135        self
136    }
137
138    /// Set the style of the bar.
139    ///
140    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
141    /// your own type that implements [`Into<Style>`]).
142    ///
143    /// This will apply to every non-styled element. It can be seen and used as a default value.
144    ///
145    /// [`Color`]: ratatui_core::style::Color
146    #[must_use = "method moves the value of self and returns the modified value"]
147    pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
148        self.style = style.into();
149        self
150    }
151
152    /// Set the style of the value.
153    ///
154    /// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
155    /// your own type that implements [`Into<Style>`]).
156    ///
157    /// # See also
158    ///
159    /// [`Bar::value`] to set the value.
160    ///
161    /// [`Color`]: ratatui_core::style::Color
162    #[must_use = "method moves the value of self and returns the modified value"]
163    pub fn value_style<S: Into<Style>>(mut self, style: S) -> Self {
164        self.value_style = style.into();
165        self
166    }
167
168    /// Set the text value printed in the bar.
169    ///
170    /// `text_value` can be a [`&str`], `Number` or anything that can be converted into [`String`].
171    ///
172    /// If `text_value` is not set, then the [`ToString`] representation of `value` will be shown on
173    /// the bar.
174    ///
175    /// # Examples
176    ///
177    /// From [`&str`] and [`String`]:
178    ///
179    /// ```
180    /// use ratatui::widgets::Bar;
181    ///
182    /// Bar::default().text_value("label");
183    /// Bar::default().text_value(String::from("label"));
184    /// ```
185    ///
186    /// # See also
187    ///
188    /// [`Bar::value`] to set the value.
189    #[must_use = "method moves the value of self and returns the modified value"]
190    pub fn text_value<T: Into<String>>(mut self, text_value: T) -> Self {
191        self.text_value = Some(text_value.into());
192        self
193    }
194
195    /// Render the value of the bar.
196    ///
197    /// [`text_value`](Bar::text_value) is used if set, otherwise the value is converted to string.
198    /// The value is rendered using `value_style`. If the value width is greater than the
199    /// bar width, then the value is split into 2 parts. the first part is rendered in the bar
200    /// using `value_style`. The second part is rendered outside the bar using `bar_style`
201    pub(super) fn render_value_with_different_styles(
202        &self,
203        buf: &mut Buffer,
204        area: Rect,
205        bar_length: usize,
206        default_value_style: Style,
207        bar_style: Style,
208    ) {
209        let value = self.value.to_string();
210        let text = self.text_value.as_ref().unwrap_or(&value);
211
212        if !text.is_empty() {
213            let style = default_value_style.patch(self.value_style);
214            // Since the value may be longer than the bar itself, we need to use 2 different styles
215            // while rendering. Render the first part with the default value style
216            buf.set_stringn(area.x, area.y, text, bar_length, style);
217            // render the second part with the bar_style
218            if text.len() > bar_length {
219                // Find the last character boundary at or before bar_length
220                let bar_length = text
221                    .char_indices()
222                    .take_while(|(i, _)| *i < bar_length)
223                    .last()
224                    .map_or(0, |(i, c)| i + c.len_utf8());
225
226                let (first, second) = text.split_at(bar_length);
227
228                let style = bar_style.patch(self.style);
229                buf.set_stringn(
230                    area.x + first.len() as u16,
231                    area.y,
232                    second,
233                    area.width as usize - first.len(),
234                    style,
235                );
236            }
237        }
238    }
239
240    pub(super) fn render_value(
241        &self,
242        buf: &mut Buffer,
243        max_width: u16,
244        x: u16,
245        y: u16,
246        default_value_style: Style,
247        ticks: u64,
248    ) {
249        if self.value != 0 {
250            const TICKS_PER_LINE: u64 = 8;
251            let value = self.value.to_string();
252            let value_label = self.text_value.as_ref().unwrap_or(&value);
253            let width = value_label.width() as u16;
254            // if we have enough space or the ticks are greater equal than 1 cell (8)
255            // then print the value
256            if width < max_width || (width == max_width && ticks >= TICKS_PER_LINE) {
257                buf.set_string(
258                    x + (max_width.saturating_sub(value_label.len() as u16) >> 1),
259                    y,
260                    value_label,
261                    default_value_style.patch(self.value_style),
262                );
263            }
264        }
265    }
266
267    pub(super) fn render_label(
268        &self,
269        buf: &mut Buffer,
270        max_width: u16,
271        x: u16,
272        y: u16,
273        default_label_style: Style,
274    ) {
275        // center the label. Necessary to do it this way as we don't want to set the style
276        // of the whole area, just the label area
277        let width = self
278            .label
279            .as_ref()
280            .map_or(0, Line::width)
281            .min(max_width as usize) as u16;
282        let area = Rect {
283            x: x + (max_width.saturating_sub(width)) / 2,
284            y,
285            width,
286            height: 1,
287        };
288        buf.set_style(area, default_label_style);
289        if let Some(label) = &self.label {
290            label.render(area, buf);
291        }
292    }
293}
294
295impl Styled for Bar<'_> {
296    type Item = Self;
297
298    fn style(&self) -> Style {
299        self.style
300    }
301
302    fn set_style<S: Into<Style>>(mut self, style: S) -> Self::Item {
303        self.style = style.into();
304        self
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use ratatui_core::style::{Color, Modifier, Style, Stylize};
311
312    use super::*;
313
314    #[test]
315    fn test_bar_new() {
316        let bar = Bar::new(42).label(Line::from("Label"));
317        assert_eq!(bar.label, Some(Line::from("Label")));
318        assert_eq!(bar.value, 42);
319    }
320
321    #[test]
322    fn test_bar_with_label() {
323        let bar = Bar::with_label("Label", 42);
324        assert_eq!(bar.label, Some(Line::from("Label")));
325        assert_eq!(bar.value, 42);
326    }
327
328    #[test]
329    fn test_bar_stylized() {
330        let bar = Bar::default().red().bold();
331        assert_eq!(
332            bar.style,
333            Style::default().fg(Color::Red).add_modifier(Modifier::BOLD)
334        );
335    }
336}