ratatui/src/widgets/sparkline.rs
tieway59 dd22e721e3
chore: Correct "builder methods" in docs and add must_use on widgets setters (#655)
Fixes #650

This PR corrects the "builder methods" expressing to simple `setters`
(see #650 #655), and gives a clearer diagnostic notice on setters `must_use`.

`#[must_use = "method moves the value of self and returns the modified value"]`

Details:

    docs: Correct wording in docs from builder methods

    Add `must_use` on layout setters

    chore: add `must_use` on widgets fluent methods

        This commit ignored `table.rs` because it is included in other PRs.

    test(gauge): fix test
2023-12-06 15:39:52 +01:00

322 lines
9.8 KiB
Rust

#![warn(missing_docs)]
use std::cmp::min;
use strum::{Display, EnumString};
use crate::{
buffer::Buffer,
layout::Rect,
style::{Style, Styled},
symbols,
widgets::{Block, Widget},
};
/// Widget to render a sparkline over one or more lines.
///
/// You can create a `Sparkline` using [`Sparkline::default`].
///
/// `Sparkline` can be styled either using [`Sparkline::style`] or preferably using the methods
/// provided by the [`Stylize`](crate::style::Stylize) trait.
///
/// # Setter methods
///
/// - [`Sparkline::block`] wraps the sparkline in a [`Block`]
/// - [`Sparkline::data`] defines the dataset, you'll almost always want to use it
/// - [`Sparkline::max`] sets the maximum value of bars
/// - [`Sparkline::direction`] sets the render direction
///
/// # Examples
///
/// ```
/// use ratatui::{prelude::*, widgets::*};
///
/// Sparkline::default()
/// .block(Block::default().title("Sparkline").borders(Borders::ALL))
/// .data(&[0, 2, 3, 4, 1, 4, 10])
/// .max(5)
/// .direction(RenderDirection::RightToLeft)
/// .style(Style::default().red().on_white());
/// ```
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct Sparkline<'a> {
/// A block to wrap the widget in
block: Option<Block<'a>>,
/// Widget style
style: Style,
/// A slice of the data to display
data: &'a [u64],
/// The maximum value to take to compute the maximum bar height (if nothing is specified, the
/// widget uses the max of the dataset)
max: Option<u64>,
/// A set of bar symbols used to represent the give data
bar_set: symbols::bar::Set,
// The direction to render the sparkine, either from left to right, or from right to left
direction: RenderDirection,
}
/// Defines the direction in which sparkline will be rendered.
///
/// See [`Sparkline::direction`].
#[derive(Debug, Default, Display, EnumString, Clone, Copy, Eq, PartialEq, Hash)]
pub enum RenderDirection {
/// The first value is on the left, going to the right
#[default]
LeftToRight,
/// The first value is on the right, going to the left
RightToLeft,
}
impl<'a> Default for Sparkline<'a> {
fn default() -> Sparkline<'a> {
Sparkline {
block: None,
style: Style::default(),
data: &[],
max: None,
bar_set: symbols::bar::NINE_LEVELS,
direction: RenderDirection::LeftToRight,
}
}
}
impl<'a> Sparkline<'a> {
/// Wraps the sparkline with the given `block`.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Sparkline<'a> {
self.block = Some(block);
self
}
/// Sets the style of the entire widget.
///
/// The foreground corresponds to the bars while the background is everything else.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style(mut self, style: Style) -> Sparkline<'a> {
self.style = style;
self
}
/// Sets the dataset for the sparkline.
///
/// # Example
///
/// ```
/// # use ratatui::{prelude::*, widgets::*};
/// # fn ui(frame: &mut Frame) {
/// # let area = Rect::default();
/// let sparkline = Sparkline::default().data(&[1, 2, 3]);
/// frame.render_widget(sparkline, area);
/// # }
/// ```
#[must_use = "method moves the value of self and returns the modified value"]
pub fn data(mut self, data: &'a [u64]) -> Sparkline<'a> {
self.data = data;
self
}
/// Sets the maximum value of bars.
///
/// Every bar will be scaled accordingly. If no max is given, this will be the max in the
/// dataset.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn max(mut self, max: u64) -> Sparkline<'a> {
self.max = Some(max);
self
}
/// Sets the characters used to display the bars.
///
/// Can be [`symbols::bar::THREE_LEVELS`], [`symbols::bar::NINE_LEVELS`] (default) or a custom
/// [`Set`](symbols::bar::Set).
#[must_use = "method moves the value of self and returns the modified value"]
pub fn bar_set(mut self, bar_set: symbols::bar::Set) -> Sparkline<'a> {
self.bar_set = bar_set;
self
}
/// Sets the direction of the sparkline.
///
/// [`RenderDirection::LeftToRight`] by default.
#[must_use = "method moves the value of self and returns the modified value"]
pub fn direction(mut self, direction: RenderDirection) -> Sparkline<'a> {
self.direction = direction;
self
}
}
impl<'a> Styled for Sparkline<'a> {
type Item = Sparkline<'a>;
fn style(&self) -> Style {
self.style
}
fn set_style(self, style: Style) -> Self::Item {
self.style(style)
}
}
impl<'a> Widget for Sparkline<'a> {
fn render(mut self, area: Rect, buf: &mut Buffer) {
let spark_area = match self.block.take() {
Some(b) => {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
}
None => area,
};
if spark_area.height < 1 {
return;
}
let max = match self.max {
Some(v) => v,
None => *self.data.iter().max().unwrap_or(&1u64),
};
let max_index = min(spark_area.width as usize, self.data.len());
let mut data = self
.data
.iter()
.take(max_index)
.map(|e| {
if max == 0 {
0
} else {
e * u64::from(spark_area.height) * 8 / max
}
})
.collect::<Vec<u64>>();
for j in (0..spark_area.height).rev() {
for (i, d) in data.iter_mut().enumerate() {
let symbol = match *d {
0 => self.bar_set.empty,
1 => self.bar_set.one_eighth,
2 => self.bar_set.one_quarter,
3 => self.bar_set.three_eighths,
4 => self.bar_set.half,
5 => self.bar_set.five_eighths,
6 => self.bar_set.three_quarters,
7 => self.bar_set.seven_eighths,
_ => self.bar_set.full,
};
let x = match self.direction {
RenderDirection::LeftToRight => spark_area.left() + i as u16,
RenderDirection::RightToLeft => spark_area.right() - i as u16 - 1,
};
buf.get_mut(x, spark_area.top() + j)
.set_symbol(symbol)
.set_style(self.style);
if *d > 8 {
*d -= 8;
} else {
*d = 0;
}
}
}
}
}
#[cfg(test)]
mod tests {
use strum::ParseError;
use super::*;
use crate::{
assert_buffer_eq,
buffer::Cell,
style::{Color, Modifier, Stylize},
};
#[test]
fn render_direction_to_string() {
assert_eq!(RenderDirection::LeftToRight.to_string(), "LeftToRight");
assert_eq!(RenderDirection::RightToLeft.to_string(), "RightToLeft");
}
#[test]
fn render_direction_from_str() {
assert_eq!(
"LeftToRight".parse::<RenderDirection>(),
Ok(RenderDirection::LeftToRight)
);
assert_eq!(
"RightToLeft".parse::<RenderDirection>(),
Ok(RenderDirection::RightToLeft)
);
assert_eq!(
"".parse::<RenderDirection>(),
Err(ParseError::VariantNotFound)
);
}
// Helper function to render a sparkline to a buffer with a given width
// filled with x symbols to make it easier to assert on the result
fn render(widget: Sparkline, width: u16) -> Buffer {
let area = Rect::new(0, 0, width, 1);
let mut cell = Cell::default();
cell.set_symbol("x");
let mut buffer = Buffer::filled(area, &cell);
widget.render(area, &mut buffer);
buffer
}
#[test]
fn it_does_not_panic_if_max_is_zero() {
let widget = Sparkline::default().data(&[0, 0, 0]);
let buffer = render(widget, 6);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
}
#[test]
fn it_does_not_panic_if_max_is_set_to_zero() {
let widget = Sparkline::default().data(&[0, 1, 2]).max(0);
let buffer = render(widget, 6);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" xxx"]));
}
#[test]
fn it_draws() {
let widget = Sparkline::default().data(&[0, 1, 2, 3, 4, 5, 6, 7, 8]);
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_renders_left_to_right() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::LeftToRight);
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec![" ▁▂▃▄▅▆▇█xxx"]));
}
#[test]
fn it_renders_right_to_left() {
let widget = Sparkline::default()
.data(&[0, 1, 2, 3, 4, 5, 6, 7, 8])
.direction(RenderDirection::RightToLeft);
let buffer = render(widget, 12);
assert_buffer_eq!(buffer, Buffer::with_lines(vec!["xxx█▇▆▅▄▃▂▁ "]));
}
#[test]
fn can_be_stylized() {
assert_eq!(
Sparkline::default()
.black()
.on_white()
.bold()
.not_dim()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::DIM)
)
}
}