mirror of
https://github.com/ratatui-org/ratatui
synced 2024-11-10 15:14:27 +00:00
docs(examples): make custom widget example into a button (#539)
The widget also now supports mouse
This commit is contained in:
parent
c0991cc576
commit
61af0d9906
2 changed files with 231 additions and 25 deletions
|
@ -1,27 +1,121 @@
|
|||
use std::{error::Error, io};
|
||||
use std::{error::Error, io, ops::ControlFlow, time::Duration};
|
||||
|
||||
use crossterm::{
|
||||
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode},
|
||||
event::{
|
||||
self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, MouseButton, MouseEvent,
|
||||
MouseEventKind,
|
||||
},
|
||||
execute,
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
};
|
||||
use ratatui::{prelude::*, widgets::*};
|
||||
|
||||
#[derive(Default)]
|
||||
struct Label<'a> {
|
||||
text: &'a str,
|
||||
/// A custom widget that renders a button with a label, theme and state.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Button<'a> {
|
||||
label: Line<'a>,
|
||||
theme: Theme,
|
||||
state: State,
|
||||
}
|
||||
|
||||
impl<'a> Widget for Label<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
buf.set_string(area.left(), area.top(), self.text, Style::default());
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum State {
|
||||
Normal,
|
||||
Selected,
|
||||
Active,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
struct Theme {
|
||||
text: Color,
|
||||
background: Color,
|
||||
highlight: Color,
|
||||
shadow: Color,
|
||||
}
|
||||
|
||||
const BLUE: Theme = Theme {
|
||||
text: Color::Rgb(16, 24, 48),
|
||||
background: Color::Rgb(48, 72, 144),
|
||||
highlight: Color::Rgb(64, 96, 192),
|
||||
shadow: Color::Rgb(32, 48, 96),
|
||||
};
|
||||
|
||||
const RED: Theme = Theme {
|
||||
text: Color::Rgb(48, 16, 16),
|
||||
background: Color::Rgb(144, 48, 48),
|
||||
highlight: Color::Rgb(192, 64, 64),
|
||||
shadow: Color::Rgb(96, 32, 32),
|
||||
};
|
||||
|
||||
const GREEN: Theme = Theme {
|
||||
text: Color::Rgb(16, 48, 16),
|
||||
background: Color::Rgb(48, 144, 48),
|
||||
highlight: Color::Rgb(64, 192, 64),
|
||||
shadow: Color::Rgb(32, 96, 32),
|
||||
};
|
||||
|
||||
/// A button with a label that can be themed.
|
||||
impl<'a> Button<'a> {
|
||||
pub fn new<T: Into<Line<'a>>>(label: T) -> Button<'a> {
|
||||
Button {
|
||||
label: label.into(),
|
||||
theme: BLUE,
|
||||
state: State::Normal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn theme(mut self, theme: Theme) -> Button<'a> {
|
||||
self.theme = theme;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: State) -> Button<'a> {
|
||||
self.state = state;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Label<'a> {
|
||||
fn text(mut self, text: &'a str) -> Label<'a> {
|
||||
self.text = text;
|
||||
self
|
||||
impl<'a> Widget for Button<'a> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let (background, text, shadow, highlight) = self.colors();
|
||||
buf.set_style(area, Style::new().bg(background).fg(text));
|
||||
|
||||
// render top line if there's enough space
|
||||
if area.height > 2 {
|
||||
buf.set_string(
|
||||
area.x,
|
||||
area.y,
|
||||
"▔".repeat(area.width as usize),
|
||||
Style::new().fg(highlight).bg(background),
|
||||
);
|
||||
}
|
||||
// render bottom line if there's enough space
|
||||
if area.height > 1 {
|
||||
buf.set_string(
|
||||
area.x,
|
||||
area.y + area.height - 1,
|
||||
"▁".repeat(area.width as usize),
|
||||
Style::new().fg(shadow).bg(background),
|
||||
);
|
||||
}
|
||||
// render label centered
|
||||
buf.set_line(
|
||||
area.x + (area.width.saturating_sub(self.label.width() as u16)) / 2,
|
||||
area.y + (area.height.saturating_sub(1)) / 2,
|
||||
&self.label,
|
||||
area.width,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
impl Button<'_> {
|
||||
fn colors(&self) -> (Color, Color, Color, Color) {
|
||||
let theme = self.theme;
|
||||
match self.state {
|
||||
State::Normal => (theme.background, theme.text, theme.shadow, theme.highlight),
|
||||
State::Selected => (theme.highlight, theme.text, theme.shadow, theme.highlight),
|
||||
State::Active => (theme.background, theme.text, theme.highlight, theme.shadow),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,19 +147,122 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||
}
|
||||
|
||||
fn run_app<B: Backend>(terminal: &mut Terminal<B>) -> io::Result<()> {
|
||||
let mut selected_button: usize = 0;
|
||||
let button_states = &mut [State::Selected, State::Normal, State::Normal];
|
||||
loop {
|
||||
terminal.draw(ui)?;
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if let KeyCode::Char('q') = key.code {
|
||||
return Ok(());
|
||||
terminal.draw(|frame| ui(frame, button_states))?;
|
||||
if !event::poll(Duration::from_millis(100))? {
|
||||
continue;
|
||||
}
|
||||
match event::read()? {
|
||||
Event::Key(key) => {
|
||||
if key.kind != event::KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
if handle_key_event(key, button_states, &mut selected_button).is_break() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event::Mouse(mouse) => handle_mouse_event(mouse, button_states, &mut selected_button),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ui(f: &mut Frame) {
|
||||
let size = f.size();
|
||||
let label = Label::default().text("Test");
|
||||
f.render_widget(label, size);
|
||||
fn ui(frame: &mut Frame, states: &[State; 3]) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints(vec![
|
||||
Constraint::Length(1),
|
||||
Constraint::Max(3),
|
||||
Constraint::Length(1),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
])
|
||||
.split(frame.size());
|
||||
frame.render_widget(
|
||||
Paragraph::new("Custom Widget Example (mouse enabled)"),
|
||||
layout[0],
|
||||
);
|
||||
render_buttons(frame, layout[1], states);
|
||||
frame.render_widget(
|
||||
Paragraph::new("←/→: select, Space: toggle, q: quit"),
|
||||
layout[2],
|
||||
);
|
||||
}
|
||||
|
||||
fn render_buttons(frame: &mut Frame<'_>, area: Rect, states: &[State; 3]) {
|
||||
let layout = Layout::default()
|
||||
.direction(Direction::Horizontal)
|
||||
.constraints(vec![
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Length(15),
|
||||
Constraint::Min(0), // ignore remaining space
|
||||
])
|
||||
.split(area);
|
||||
frame.render_widget(Button::new("Red").theme(RED).state(states[0]), layout[0]);
|
||||
frame.render_widget(
|
||||
Button::new("Green").theme(GREEN).state(states[1]),
|
||||
layout[1],
|
||||
);
|
||||
frame.render_widget(Button::new("Blue").theme(BLUE).state(states[2]), layout[2]);
|
||||
}
|
||||
|
||||
fn handle_key_event(
|
||||
key: event::KeyEvent,
|
||||
button_states: &mut [State; 3],
|
||||
selected_button: &mut usize,
|
||||
) -> ControlFlow<()> {
|
||||
match key.code {
|
||||
KeyCode::Char('q') => return ControlFlow::Break(()),
|
||||
KeyCode::Left => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_sub(1);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
KeyCode::Right => {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
*selected_button = selected_button.saturating_add(1).min(2);
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
KeyCode::Char(' ') => {
|
||||
if button_states[*selected_button] == State::Active {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
} else {
|
||||
button_states[*selected_button] = State::Active;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
ControlFlow::Continue(())
|
||||
}
|
||||
|
||||
fn handle_mouse_event(
|
||||
mouse: MouseEvent,
|
||||
button_states: &mut [State; 3],
|
||||
selected_button: &mut usize,
|
||||
) {
|
||||
match mouse.kind {
|
||||
MouseEventKind::Moved => {
|
||||
let old_selected_button = *selected_button;
|
||||
*selected_button = match mouse.column {
|
||||
x if x < 15 => 0,
|
||||
x if x < 30 => 1,
|
||||
_ => 2,
|
||||
};
|
||||
if old_selected_button != *selected_button {
|
||||
button_states[old_selected_button] = State::Normal;
|
||||
button_states[*selected_button] = State::Selected;
|
||||
}
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
if button_states[*selected_button] == State::Active {
|
||||
button_states[*selected_button] = State::Normal;
|
||||
} else {
|
||||
button_states[*selected_button] = State::Active;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,11 +2,20 @@
|
|||
# To run this script, install vhs and run `vhs ./examples/custom_widget.tape`
|
||||
Output "target/custom_widget.gif"
|
||||
Set Theme "OceanicMaterial"
|
||||
Set Width 1200
|
||||
Set Height 200
|
||||
Set Width 760
|
||||
Set Height 260
|
||||
Hide
|
||||
Type "cargo run --example=custom_widget --features=crossterm"
|
||||
Enter
|
||||
Sleep 1s
|
||||
Sleep 2s
|
||||
Show
|
||||
Sleep 5s
|
||||
Sleep 1s
|
||||
Set TypingSpeed 0.7s
|
||||
Right
|
||||
Right
|
||||
Space
|
||||
Space
|
||||
Left
|
||||
Space
|
||||
Left
|
||||
Space
|
Loading…
Reference in a new issue