fix(gauge): fix gauge widget colors (#572)

The background colors of the gauge had a workaround for the issue we had
with VHS / TTYD rendering the background color of the gauge. This
workaround is no longer necessary in the updated versions of VHS / TTYD.

Fixes https://github.com/ratatui-org/ratatui/issues/501
This commit is contained in:
Josh McKinney 2023-10-19 04:29:53 -07:00 committed by GitHub
parent 8d507c43fa
commit 0c52ff431a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 177 additions and 169 deletions

View file

@ -1,17 +1,32 @@
use std::{ use std::{
error::Error, io::{self, stdout, Stdout},
io, rc::Rc,
time::{Duration, Instant}, time::Duration,
}; };
use anyhow::Result;
use crossterm::{ use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode}, event::{self, Event, KeyCode, KeyEventKind},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
}; };
use ratatui::{prelude::*, widgets::*}; use ratatui::{
prelude::*,
widgets::{block::Title, *},
};
fn main() -> Result<()> {
App::run()
}
struct App { struct App {
term: Term,
should_quit: bool,
state: AppState,
}
#[derive(Debug, Default, Clone, Copy)]
struct AppState {
progress1: u16, progress1: u16,
progress2: u16, progress2: u16,
progress3: f64, progress3: f64,
@ -19,138 +34,154 @@ struct App {
} }
impl App { impl App {
fn new() -> App { fn run() -> Result<()> {
App { // run at ~10 fps minus the time it takes to draw
progress1: 0, let timeout = Duration::from_secs_f32(1.0 / 10.0);
progress2: 0, let mut app = Self::start()?;
progress3: 0.45, while !app.should_quit {
progress4: 0, app.update();
app.draw()?;
app.handle_events(timeout)?;
} }
app.stop()?;
Ok(())
} }
fn on_tick(&mut self) { fn start() -> Result<Self> {
self.progress1 += 1; Ok(App {
if self.progress1 > 100 { term: Term::start()?,
self.progress1 = 0; should_quit: false,
} state: AppState {
self.progress2 += 2; progress1: 0,
if self.progress2 > 100 { progress2: 0,
self.progress2 = 0; progress3: 0.0,
} progress4: 0,
self.progress3 += 0.001; },
if self.progress3 > 1.0 { })
self.progress3 = 0.0;
}
self.progress4 += 1;
if self.progress4 > 100 {
self.progress4 = 0;
}
}
}
fn main() -> Result<(), Box<dyn Error>> {
// setup terminal
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
// create app and run it
let tick_rate = Duration::from_millis(250);
let app = App::new();
let res = run_app(&mut terminal, app, tick_rate);
// restore terminal
disable_raw_mode()?;
execute!(
terminal.backend_mut(),
LeaveAlternateScreen,
DisableMouseCapture
)?;
terminal.show_cursor()?;
if let Err(err) = res {
println!("{err:?}");
} }
Ok(()) fn stop(&mut self) -> Result<()> {
} Term::stop()?;
Ok(())
}
fn run_app<B: Backend>( fn update(&mut self) {
terminal: &mut Terminal<B>, self.state.progress1 = (self.state.progress1 + 4).min(100);
mut app: App, self.state.progress2 = (self.state.progress2 + 3).min(100);
tick_rate: Duration, self.state.progress3 = (self.state.progress3 + 0.02).min(1.0);
) -> io::Result<()> { self.state.progress4 = (self.state.progress4 + 1).min(100);
let mut last_tick = Instant::now(); }
loop {
terminal.draw(|f| ui(f, &app))?;
let timeout = tick_rate fn draw(&mut self) -> Result<()> {
.checked_sub(last_tick.elapsed()) self.term.draw(|frame| {
.unwrap_or_else(|| Duration::from_secs(0)); let state = self.state;
if crossterm::event::poll(timeout)? { let layout = Self::equal_layout(frame);
Self::render_gauge1(state.progress1, frame, layout[0]);
Self::render_gauge2(state.progress2, frame, layout[1]);
Self::render_gauge3(state.progress3, frame, layout[2]);
Self::render_gauge4(state.progress4, frame, layout[3]);
})?;
Ok(())
}
fn handle_events(&mut self, timeout: Duration) -> io::Result<()> {
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
if let KeyCode::Char('q') = key.code { if key.kind == KeyEventKind::Press {
return Ok(()); if let KeyCode::Char('q') = key.code {
self.should_quit = true;
}
} }
} }
} }
if last_tick.elapsed() >= tick_rate { Ok(())
app.on_tick(); }
last_tick = Instant::now();
} fn equal_layout(frame: &Frame) -> Rc<[Rect]> {
Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
Constraint::Percentage(25),
])
.split(frame.size())
}
fn render_gauge1(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress");
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().light_red())
.percent(progress);
frame.render_widget(gauge, area);
}
fn render_gauge2(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and custom label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().blue().on_light_blue())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn render_gauge3(progress: f64, frame: &mut Frame, area: Rect) {
let title =
Self::title_block("Gauge with ratio progress, custom label with style, and unicode");
let label = Span::styled(
format!("{:.2}%", progress * 100.0),
Style::new().red().italic().bold(),
);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(progress)
.label(label)
.use_unicode(true);
frame.render_widget(gauge, area);
}
fn render_gauge4(progress: u16, frame: &mut Frame, area: Rect) {
let title = Self::title_block("Gauge with percentage progress and label");
let label = format!("{}/100", progress);
let gauge = Gauge::default()
.block(title)
.gauge_style(Style::new().green().italic())
.percent(progress)
.label(label);
frame.render_widget(gauge, area);
}
fn title_block(title: &str) -> Block {
let title = Title::from(title).alignment(Alignment::Center);
Block::default().title(title).borders(Borders::TOP)
} }
} }
fn ui(f: &mut Frame, app: &App) { struct Term {
let chunks = Layout::default() terminal: Terminal<CrosstermBackend<Stdout>>,
.direction(Direction::Vertical) }
.constraints([
Constraint::Percentage(25), impl Term {
Constraint::Percentage(25), pub fn start() -> io::Result<Term> {
Constraint::Percentage(25), stdout().execute(EnterAlternateScreen)?;
Constraint::Percentage(25), enable_raw_mode()?;
]) let terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
.split(f.size()); Ok(Self { terminal })
}
let gauge = Gauge::default()
.block(Block::default().title("Gauge1").borders(Borders::ALL)) pub fn stop() -> io::Result<()> {
.gauge_style(Style::default().fg(Color::Yellow)) disable_raw_mode()?;
.percent(app.progress1); stdout().execute(LeaveAlternateScreen)?;
f.render_widget(gauge, chunks[0]); Ok(())
}
let label = format!("{}/100", app.progress2);
let gauge = Gauge::default() fn draw(&mut self, frame: impl FnOnce(&mut Frame)) -> Result<()> {
.block(Block::default().title("Gauge2").borders(Borders::ALL)) self.terminal.draw(frame)?;
.gauge_style(Style::default().fg(Color::Magenta).bg(Color::Green)) Ok(())
.percent(app.progress2) }
.label(label);
f.render_widget(gauge, chunks[1]);
let label = Span::styled(
format!("{:.2}%", app.progress3 * 100.0),
Style::default()
.fg(Color::Red)
.add_modifier(Modifier::ITALIC | Modifier::BOLD),
);
let gauge = Gauge::default()
.block(Block::default().title("Gauge3").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Yellow))
.ratio(app.progress3)
.label(label)
.use_unicode(true);
f.render_widget(gauge, chunks[2]);
let label = format!("{}/100", app.progress4);
let gauge = Gauge::default()
.block(Block::default().title("Gauge4").borders(Borders::ALL))
.gauge_style(
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::ITALIC),
)
.percent(app.progress4)
.label(label);
f.render_widget(gauge, chunks[3]);
} }

View file

@ -3,7 +3,7 @@
Output "target/gauge.gif" Output "target/gauge.gif"
Set Theme "Aardvark Blue" Set Theme "Aardvark Blue"
Set Width 1200 Set Width 1200
Set Height 600 Set Height 550
Hide Hide
Type "cargo run --example=gauge --features=crossterm" Type "cargo run --example=gauge --features=crossterm"
Enter Enter

View file

@ -185,14 +185,14 @@ impl<'a> Widget for Gauge<'a> {
// render the filled area (left to end) // render the filled area (left to end)
for x in gauge_area.left()..end { for x in gauge_area.left()..end {
let cell = buf.get_mut(x, y); let cell = buf.get_mut(x, y);
if self.use_unicode { // Use full block for the filled part of the gauge and spaces for the part that is
// covered by the label. Note that the background and foreground colors are swapped
// for the label part, otherwise the gauge will be inverted
if x < label_col || x > label_col + clamped_label_width || y != label_row {
cell.set_symbol(symbols::block::FULL) cell.set_symbol(symbols::block::FULL)
.set_fg(self.gauge_style.fg.unwrap_or(Color::Reset)) .set_fg(self.gauge_style.fg.unwrap_or(Color::Reset))
.set_bg(self.gauge_style.bg.unwrap_or(Color::Reset)); .set_bg(self.gauge_style.bg.unwrap_or(Color::Reset));
} else { } else {
// spaces are needed to apply the background styling.
// note that the background and foreground colors are swapped
// otherwise the gauge will be inverted
cell.set_symbol(" ") cell.set_symbol(" ")
.set_fg(self.gauge_style.bg.unwrap_or(Color::Reset)) .set_fg(self.gauge_style.bg.unwrap_or(Color::Reset))
.set_bg(self.gauge_style.fg.unwrap_or(Color::Reset)); .set_bg(self.gauge_style.fg.unwrap_or(Color::Reset));

View file

@ -2,7 +2,7 @@ use ratatui::{
backend::TestBackend, backend::TestBackend,
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Direction, Layout, Rect}, layout::{Constraint, Direction, Layout, Rect},
style::{Color, Modifier, Style}, style::{Color, Modifier, Style, Stylize},
symbols, symbols,
text::Span, text::Span,
widgets::{Block, Borders, Gauge, LineGauge}, widgets::{Block, Borders, Gauge, LineGauge},
@ -47,32 +47,12 @@ fn widgets_gauge_renders() {
" ", " ",
" ", " ",
]); ]);
expected.set_style(Rect::new(3, 3, 34, 1), Style::new().red().on_blue());
for i in 3..17 { expected.set_style(Rect::new(3, 6, 15, 1), Style::new().red().on_blue());
expected // Note that filled part of the gauge only covers the 5 and the 1, not the % symbol
.get_mut(i, 3) expected.set_style(Rect::new(18, 6, 2, 1), Style::new().blue().on_red());
.set_bg(Color::Blue) expected.set_style(Rect::new(20, 6, 17, 1), Style::new().red().on_blue());
.set_fg(Color::Red);
}
for i in 17..37 {
expected
.get_mut(i, 3)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
for i in 3..20 {
expected
.get_mut(i, 6)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
for i in 20..37 {
expected
.get_mut(i, 6)
.set_bg(Color::Blue)
.set_fg(Color::Red);
}
terminal.backend().assert_buffer(&expected); terminal.backend().assert_buffer(&expected);
} }
@ -106,10 +86,10 @@ fn widgets_gauge_renders_no_unicode() {
" ", " ",
" ", " ",
" ┌Percentage────────────────────────┐ ", " ┌Percentage────────────────────────┐ ",
" 43% │ ", "███████████████43% │ ",
" └──────────────────────────────────┘ ", " └──────────────────────────────────┘ ",
" ┌Ratio─────────────────────────────┐ ", " ┌Ratio─────────────────────────────┐ ",
" 21% │ ", "███████ 21% │ ",
" └──────────────────────────────────┘ ", " └──────────────────────────────────┘ ",
" ", " ",
" ", " ",
@ -143,9 +123,9 @@ fn widgets_gauge_applies_styles() {
.unwrap(); .unwrap();
let mut expected = Buffer::with_lines(vec![ let mut expected = Buffer::with_lines(vec![
"┌Test──────┐", "┌Test──────┐",
" ", "████",
" 43% │", "███43% │",
" ", "████",
"└──────────┘", "└──────────┘",
]); ]);
// title // title
@ -156,13 +136,10 @@ fn widgets_gauge_applies_styles() {
Style::default().fg(Color::Blue).bg(Color::Red), Style::default().fg(Color::Blue).bg(Color::Red),
); );
// filled area // filled area
for y in 1..4 { expected.set_style(
expected.set_style( Rect::new(1, 1, 4, 3),
Rect::new(1, y, 4, 1), Style::default().fg(Color::Blue).bg(Color::Red),
// filled style is invert of gauge_style );
Style::default().fg(Color::Red).bg(Color::Blue),
);
}
// label (foreground and modifier from label style) // label (foreground and modifier from label style)
expected.set_style( expected.set_style(
Rect::new(4, 2, 1, 1), Rect::new(4, 2, 1, 1),