mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-21 20:23:11 +00:00
feat(LineGauge): allow LineGauge background styles (#565)
This PR deprecates `gauge_style` in favor of `filled_style` and `unfilled_style` which can have it's foreground and background styled. `cargo run --example=line_gauge --features=crossterm` https://github.com/ratatui-org/ratatui/assets/5149215/5fb2ce65-8607-478f-8be4-092e08612f5b Implements: <https://github.com/ratatui-org/ratatui/issues/424> Co-authored-by: Josh McKinney <joshka@users.noreply.github.com>
This commit is contained in:
parent
257db6257f
commit
eef1afe915
9 changed files with 333 additions and 46 deletions
|
@ -246,6 +246,11 @@ name = "gauge"
|
|||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "line_gauge"
|
||||
required-features = ["crossterm"]
|
||||
doc-scrape-examples = true
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
required-features = ["crossterm"]
|
||||
|
|
|
@ -164,6 +164,18 @@ cargo run --example=gauge --features=crossterm
|
|||
|
||||
![Gauge][gauge.gif]
|
||||
|
||||
## Line Gauge
|
||||
|
||||
Demonstrates the [`Line
|
||||
Gauge`](https://docs.rs/ratatui/latest/ratatui/widgets/struct.LineGauge.html) widget. Source:
|
||||
[line_gauge.rs](./line_gauge.rs).
|
||||
|
||||
```shell
|
||||
cargo run --example=line_gauge --features=crossterm
|
||||
```
|
||||
|
||||
![LineGauge][line_gauge.gif]
|
||||
|
||||
## Inline
|
||||
|
||||
Demonstrates how to use the
|
||||
|
@ -346,6 +358,7 @@ examples/vhs/generate.bash
|
|||
[inline.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/inline.gif?raw=true
|
||||
[layout.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/layout.gif?raw=true
|
||||
[list.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/list.gif?raw=true
|
||||
[line_gauge.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/line_gauge.gif?raw=true
|
||||
[modifiers.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/modifiers.gif?raw=true
|
||||
[panic.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/panic.gif?raw=true
|
||||
[paragraph.gif]: https://github.com/ratatui-org/ratatui/blob/images/examples/paragraph.gif?raw=true
|
||||
|
|
|
@ -76,7 +76,7 @@ fn draw_gauges(f: &mut Frame, app: &mut App, area: Rect) {
|
|||
|
||||
let line_gauge = LineGauge::default()
|
||||
.block(Block::new().title("LineGauge:"))
|
||||
.gauge_style(Style::default().fg(Color::Magenta))
|
||||
.filled_style(Style::default().fg(Color::Magenta))
|
||||
.line_set(if app.enhanced_graphics {
|
||||
symbols::line::THICK
|
||||
} else {
|
||||
|
|
|
@ -140,8 +140,8 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
|||
// cycle color hue based on the percent for a neat effect yellow -> red
|
||||
let hue = 90.0 - (percent as f32 * 0.6);
|
||||
let value = Okhsv::max_value();
|
||||
let fg = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let bg = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let filled_color = color_from_oklab(hue, Okhsv::max_saturation(), value);
|
||||
let unfilled_color = color_from_oklab(hue, Okhsv::max_saturation(), value * 0.5);
|
||||
let label = if percent < 100.0 {
|
||||
format!("Downloading: {percent}%")
|
||||
} else {
|
||||
|
@ -151,7 +151,8 @@ fn render_line_gauge(percent: f64, area: Rect, buf: &mut Buffer) {
|
|||
.ratio(percent / 100.0)
|
||||
.label(label)
|
||||
.style(Style::new().light_blue())
|
||||
.gauge_style(Style::new().fg(fg).bg(bg))
|
||||
.filled_style(Style::new().fg(filled_color))
|
||||
.unfilled_style(Style::new().fg(unfilled_color))
|
||||
.line_set(symbols::line::THICK)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
|
|
@ -248,7 +248,7 @@ fn ui(f: &mut Frame, downloads: &Downloads) {
|
|||
let done = NUM_DOWNLOADS - downloads.pending.len() - downloads.in_progress.len();
|
||||
#[allow(clippy::cast_precision_loss)]
|
||||
let progress = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Blue))
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.label(format!("{done}/{NUM_DOWNLOADS}"))
|
||||
.ratio(done as f64 / NUM_DOWNLOADS as f64);
|
||||
f.render_widget(progress, progress_area);
|
||||
|
|
215
examples/line_gauge.rs
Normal file
215
examples/line_gauge.rs
Normal file
|
@ -0,0 +1,215 @@
|
|||
//! # [Ratatui] Line Gauge example
|
||||
//!
|
||||
//! The latest version of this example is available in the [examples] folder in the repository.
|
||||
//!
|
||||
//! Please note that the examples are designed to be run against the `main` branch of the Github
|
||||
//! repository. This means that you may not be able to compile with the latest release version on
|
||||
//! crates.io, or the one that you have installed locally.
|
||||
//!
|
||||
//! See the [examples readme] for more information on finding examples that match the version of the
|
||||
//! library you are using.
|
||||
//!
|
||||
//! [Ratatui]: https://github.com/ratatui-org/ratatui
|
||||
//! [examples]: https://github.com/ratatui-org/ratatui/blob/main/examples
|
||||
//! [examples readme]: https://github.com/ratatui-org/ratatui/blob/main/examples/README.md
|
||||
|
||||
use std::{io::stdout, time::Duration};
|
||||
|
||||
use color_eyre::{config::HookBuilder, Result};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
ExecutableCommand,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
style::palette::tailwind,
|
||||
widgets::{block::Title, *},
|
||||
};
|
||||
|
||||
const CUSTOM_LABEL_COLOR: Color = tailwind::SLATE.c200;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
struct App {
|
||||
state: AppState,
|
||||
progress_columns: u16,
|
||||
progress: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
|
||||
enum AppState {
|
||||
#[default]
|
||||
Running,
|
||||
Started,
|
||||
Quitting,
|
||||
}
|
||||
|
||||
fn main() -> Result<()> {
|
||||
init_error_hooks()?;
|
||||
let terminal = init_terminal()?;
|
||||
App::default().run(terminal)?;
|
||||
restore_terminal()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn run(&mut self, mut terminal: Terminal<impl Backend>) -> Result<()> {
|
||||
while self.state != AppState::Quitting {
|
||||
self.draw(&mut terminal)?;
|
||||
self.handle_events()?;
|
||||
self.update(terminal.size()?.width);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, terminal: &mut Terminal<impl Backend>) -> Result<()> {
|
||||
terminal.draw(|f| f.render_widget(self, f.size()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self, terminal_width: u16) {
|
||||
if self.state != AppState::Started {
|
||||
return;
|
||||
}
|
||||
|
||||
self.progress_columns = (self.progress_columns + 1).clamp(0, terminal_width);
|
||||
self.progress = f64::from(self.progress_columns) / f64::from(terminal_width);
|
||||
}
|
||||
|
||||
fn handle_events(&mut self) -> Result<()> {
|
||||
let timeout = Duration::from_secs_f32(1.0 / 20.0);
|
||||
if event::poll(timeout)? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press {
|
||||
match key.code {
|
||||
KeyCode::Char(' ') | KeyCode::Enter => self.start(),
|
||||
KeyCode::Char('q') | KeyCode::Esc => self.quit(),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn start(&mut self) {
|
||||
self.state = AppState::Started;
|
||||
}
|
||||
|
||||
fn quit(&mut self) {
|
||||
self.state = AppState::Quitting;
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for &App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
use Constraint::{Length, Min, Ratio};
|
||||
let layout = Layout::vertical([Length(2), Min(0), Length(1)]);
|
||||
let [header_area, main_area, footer_area] = layout.areas(area);
|
||||
|
||||
let layout = Layout::vertical([Ratio(1, 3); 3]);
|
||||
let [gauge1_area, gauge2_area, gauge3_area] = layout.areas(main_area);
|
||||
|
||||
header().render(header_area, buf);
|
||||
footer().render(footer_area, buf);
|
||||
|
||||
self.render_gauge1(gauge1_area, buf);
|
||||
self.render_gauge2(gauge2_area, buf);
|
||||
self.render_gauge3(gauge3_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn header() -> impl Widget {
|
||||
Paragraph::new("Ratatui Line Gauge Example")
|
||||
.bold()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
}
|
||||
|
||||
fn footer() -> impl Widget {
|
||||
Paragraph::new("Press ENTER / SPACE to start")
|
||||
.alignment(Alignment::Center)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.bold()
|
||||
}
|
||||
|
||||
impl App {
|
||||
fn render_gauge1(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Blue / red only foreground");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(Style::default().fg(Color::Blue))
|
||||
.unfilled_style(Style::default().fg(Color::Red))
|
||||
.label("Foreground:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge2(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Blue / red only background");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(Style::default().fg(Color::Blue).bg(Color::Blue))
|
||||
.unfilled_style(Style::default().fg(Color::Red).bg(Color::Red))
|
||||
.label("Background:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
|
||||
fn render_gauge3(&self, area: Rect, buf: &mut Buffer) {
|
||||
let title = title_block("Fully styled with background");
|
||||
LineGauge::default()
|
||||
.block(title)
|
||||
.filled_style(
|
||||
Style::default()
|
||||
.fg(tailwind::BLUE.c400)
|
||||
.bg(tailwind::BLUE.c600),
|
||||
)
|
||||
.unfilled_style(
|
||||
Style::default()
|
||||
.fg(tailwind::RED.c400)
|
||||
.bg(tailwind::RED.c800),
|
||||
)
|
||||
.label("Both:")
|
||||
.ratio(self.progress)
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
fn title_block(title: &str) -> Block {
|
||||
let title = Title::from(title).alignment(Alignment::Center);
|
||||
Block::default()
|
||||
.title(title)
|
||||
.borders(Borders::NONE)
|
||||
.fg(CUSTOM_LABEL_COLOR)
|
||||
.padding(Padding::vertical(1))
|
||||
}
|
||||
|
||||
fn init_error_hooks() -> color_eyre::Result<()> {
|
||||
let (panic, error) = HookBuilder::default().into_hooks();
|
||||
let panic = panic.into_panic_hook();
|
||||
let error = error.into_eyre_hook();
|
||||
color_eyre::eyre::set_hook(Box::new(move |e| {
|
||||
let _ = restore_terminal();
|
||||
error(e)
|
||||
}))?;
|
||||
std::panic::set_hook(Box::new(move |info| {
|
||||
let _ = restore_terminal();
|
||||
panic(info);
|
||||
}));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_terminal() -> color_eyre::Result<Terminal<impl Backend>> {
|
||||
enable_raw_mode()?;
|
||||
stdout().execute(EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout());
|
||||
let terminal = Terminal::new(backend)?;
|
||||
Ok(terminal)
|
||||
}
|
||||
|
||||
fn restore_terminal() -> color_eyre::Result<()> {
|
||||
disable_raw_mode()?;
|
||||
stdout().execute(LeaveAlternateScreen)?;
|
||||
Ok(())
|
||||
}
|
14
examples/vhs/line_gauge.tape
Normal file
14
examples/vhs/line_gauge.tape
Normal file
|
@ -0,0 +1,14 @@
|
|||
# This is a vhs script. See https://github.com/charmbracelet/vhs for more info.
|
||||
# To run this script, install vhs and run `vhs ./examples/line_gauge.tape`
|
||||
Output "target/line_gauge.gif"
|
||||
Set Theme "Aardvark Blue"
|
||||
Set Width 1200
|
||||
Set Height 850
|
||||
Hide
|
||||
Type "cargo run --example=line_gauge --features=crossterm"
|
||||
Enter
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 2s
|
||||
Enter 1
|
||||
Sleep 15s
|
|
@ -5,8 +5,10 @@ use crate::{prelude::*, widgets::Block};
|
|||
/// A `Gauge` renders a bar filled according to the value given to [`Gauge::percent`] or
|
||||
/// [`Gauge::ratio`]. The bar width and height are defined by the [`Rect`] it is
|
||||
/// [rendered](Widget::render) in.
|
||||
///
|
||||
/// The associated label is always centered horizontally and vertically. If not set with
|
||||
/// [`Gauge::label`], the label is the percentage of the bar filled.
|
||||
///
|
||||
/// You might want to have a higher precision bar using [`Gauge::use_unicode`].
|
||||
///
|
||||
/// This can be useful to indicate the progression of a task, like a download.
|
||||
|
@ -235,14 +237,20 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
|||
|
||||
/// A compact widget to display a progress bar over a single thin line.
|
||||
///
|
||||
/// This can be useful to indicate the progression of a task, like a download.
|
||||
///
|
||||
/// A `LineGauge` renders a thin line filled according to the value given to [`LineGauge::ratio`].
|
||||
/// Unlike [`Gauge`], only the width can be defined by the [rendering](Widget::render) [`Rect`].
|
||||
/// The height is always 1.
|
||||
/// The associated label is always left-aligned. If not set with [`LineGauge::label`], the label
|
||||
/// is the percentage of the bar filled.
|
||||
/// Unlike [`Gauge`], only the width can be defined by the [rendering](Widget::render) [`Rect`]. The
|
||||
/// height is always 1.
|
||||
///
|
||||
/// The associated label is always left-aligned. If not set with [`LineGauge::label`], the label is
|
||||
/// the percentage of the bar filled.
|
||||
///
|
||||
/// You can also set the symbols used to draw the bar with [`LineGauge::line_set`].
|
||||
///
|
||||
/// This can be useful to indicate the progression of a task, like a download.
|
||||
/// To style the gauge line use [`LineGauge::filled_style`] and [`LineGauge::unfilled_style`] which
|
||||
/// let you pick a color for foreground (i.e. line) and background of the filled and unfilled part
|
||||
/// of gauge respectively.
|
||||
///
|
||||
/// # Examples:
|
||||
///
|
||||
|
@ -251,7 +259,7 @@ fn get_unicode_block<'a>(frac: f64) -> &'a str {
|
|||
///
|
||||
/// LineGauge::default()
|
||||
/// .block(Block::bordered().title("Progress"))
|
||||
/// .gauge_style(
|
||||
/// .filled_style(
|
||||
/// Style::default()
|
||||
/// .fg(Color::White)
|
||||
/// .bg(Color::Black)
|
||||
|
@ -271,7 +279,8 @@ pub struct LineGauge<'a> {
|
|||
label: Option<Line<'a>>,
|
||||
line_set: symbols::line::Set,
|
||||
style: Style,
|
||||
gauge_style: Style,
|
||||
filled_style: Style,
|
||||
unfilled_style: Style,
|
||||
}
|
||||
|
||||
impl<'a> LineGauge<'a> {
|
||||
|
@ -343,9 +352,40 @@ impl<'a> LineGauge<'a> {
|
|||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[deprecated(
|
||||
since = "0.27.0",
|
||||
note = "You should use `LineGauge::filled_style` instead."
|
||||
)]
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn gauge_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.gauge_style = style.into();
|
||||
let style: Style = style.into();
|
||||
|
||||
// maintain backward compatibility, which used the background color of the style as the
|
||||
// unfilled part of the gauge and the foreground color as the filled part of the gauge
|
||||
let filled_color = style.fg.unwrap_or(Color::Reset);
|
||||
let unfilled_color = style.bg.unwrap_or(Color::Reset);
|
||||
self.filled_style = style.fg(filled_color).bg(Color::Reset);
|
||||
self.unfilled_style = style.fg(unfilled_color).bg(Color::Reset);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of filled part of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn filled_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.filled_style = style.into();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the style of the unfilled part of the bar.
|
||||
///
|
||||
/// `style` accepts any type that is convertible to [`Style`] (e.g. [`Style`], [`Color`], or
|
||||
/// your own type that implements [`Into<Style>`]).
|
||||
#[must_use = "method moves the value of self and returns the modified value"]
|
||||
pub fn unfilled_style<S: Into<Style>>(mut self, style: S) -> Self {
|
||||
self.unfilled_style = style.into();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -379,26 +419,12 @@ impl WidgetRef for LineGauge<'_> {
|
|||
for col in start..end {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.fg,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.gauge_style.underline_color,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
.set_style(self.filled_style);
|
||||
}
|
||||
for col in end..gauge_area.right() {
|
||||
buf.get_mut(col, row)
|
||||
.set_symbol(self.line_set.horizontal)
|
||||
.set_style(Style {
|
||||
fg: self.gauge_style.bg,
|
||||
bg: None,
|
||||
#[cfg(feature = "underline-color")]
|
||||
underline_color: self.gauge_style.underline_color,
|
||||
add_modifier: self.gauge_style.add_modifier,
|
||||
sub_modifier: self.gauge_style.sub_modifier,
|
||||
});
|
||||
.set_style(self.unfilled_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -478,24 +504,36 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
#[allow(deprecated)]
|
||||
#[test]
|
||||
fn line_gauge_can_be_stylized_with_deprecated_gauge_style() {
|
||||
let gauge =
|
||||
LineGauge::default().gauge_style(Style::default().fg(Color::Red).bg(Color::Blue));
|
||||
|
||||
assert_eq!(
|
||||
gauge.filled_style,
|
||||
Style::default().fg(Color::Red).bg(Color::Reset)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
gauge.unfilled_style,
|
||||
Style::default().fg(Color::Blue).bg(Color::Reset)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn line_gauge_default() {
|
||||
// TODO: replace to `assert_eq!(LineGauge::default(), LineGauge::default())`
|
||||
// when `Eq` or `PartialEq` is implemented for `LineGauge`.
|
||||
assert_eq!(
|
||||
format!("{:?}", LineGauge::default()),
|
||||
format!(
|
||||
"{:?}",
|
||||
LineGauge {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
gauge_style: Style::default(),
|
||||
}
|
||||
),
|
||||
"LineGauge::default() should have correct default values."
|
||||
LineGauge::default(),
|
||||
LineGauge {
|
||||
block: None,
|
||||
ratio: 0.0,
|
||||
label: None,
|
||||
style: Style::default(),
|
||||
line_set: symbols::line::NORMAL,
|
||||
filled_style: Style::default(),
|
||||
unfilled_style: Style::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -180,7 +180,8 @@ fn widgets_line_gauge_renders() {
|
|||
terminal
|
||||
.draw(|f| {
|
||||
let gauge = LineGauge::default()
|
||||
.gauge_style(Style::default().fg(Color::Green).bg(Color::White))
|
||||
.filled_style(Style::default().fg(Color::Green))
|
||||
.unfilled_style(Style::default().fg(Color::White))
|
||||
.ratio(0.43);
|
||||
f.render_widget(
|
||||
gauge,
|
||||
|
@ -193,7 +194,7 @@ fn widgets_line_gauge_renders() {
|
|||
);
|
||||
let gauge = LineGauge::default()
|
||||
.block(Block::bordered().title("Gauge 2"))
|
||||
.gauge_style(Style::default().fg(Color::Green))
|
||||
.filled_style(Style::default().fg(Color::Green))
|
||||
.line_set(symbols::line::THICK)
|
||||
.ratio(0.211_313_934_313_1);
|
||||
f.render_widget(
|
||||
|
|
Loading…
Reference in a new issue