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}