mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-25 14:10:31 +00:00
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:
parent
8d507c43fa
commit
0c52ff431a
4 changed files with 177 additions and 169 deletions
|
@ -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]);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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));
|
||||||
|
|
|
@ -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),
|
||||||
|
|
Loading…
Reference in a new issue