feat: add a new RatatuiMascot widget (#1584)

Move the Mascot from Demo2 into a new widget.
Make the Rat grey and adjust the other colors.

```rust
frame.render_widget(RatatuiMascot::default(), frame.area());
```
This commit is contained in:
n4n5 2024-12-26 13:32:38 +01:00 committed by GitHub
parent 904b0aa723
commit 50ba96518f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 252 additions and 110 deletions

View File

@ -1,47 +1,13 @@
use itertools::Itertools;
use ratatui::{
buffer::Buffer,
layout::{Alignment, Constraint, Layout, Margin, Rect},
widgets::{Block, Borders, Clear, Padding, Paragraph, Widget, Wrap},
widgets::{
Block, Borders, Clear, MascotEyeColor, Padding, Paragraph, RatatuiMascot, Widget, Wrap,
},
};
use crate::{RgbSwatch, THEME};
const RATATUI_LOGO: [&str; 32] = [
" ███ ",
" ██████ ",
" ███████ ",
" ████████ ",
" █████████ ",
" ██████████ ",
" ████████████ ",
" █████████████ ",
" █████████████ ██████",
" ███████████ ████████",
" █████ ███████████ ",
" ███ ██ee████████ ",
" █ █████████████ ",
" ████ █████████████ ",
" █████████████████ ",
" ████████████████ ",
" ████████████████ ",
" ███ ██████████ ",
" ██ █████████ ",
" █xx█ █████████ ",
" █xxxx█ ██████████ ",
" █xx█xxx█ █████████ ",
" █xx██xxxx█ ████████ ",
" █xxxxxxxxxx█ ██████████ ",
" █xxxxxxxxxxxx█ ██████████ ",
" █xxxxxxx██xxxxx█ █████████ ",
" █xxxxxxxxx██xxxxx█ ████ ███ ",
" █xxxxxxxxxxxxxxxxxx█ ██ ███ ",
"█xxxxxxxxxxxxxxxxxxxx█ █ ███ ",
"█xxxxxxxxxxxxxxxxxxxxx█ ███ ",
" █xxxxxxxxxxxxxxxxxxxxx█ ███ ",
" █xxxxxxxxxxxxxxxxxxxxx█ █ ",
];
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct AboutTab {
row_index: usize,
@ -61,9 +27,20 @@ impl Widget for AboutTab {
fn render(self, area: Rect, buf: &mut Buffer) {
RgbSwatch.render(area, buf);
let horizontal = Layout::horizontal([Constraint::Length(34), Constraint::Min(0)]);
let [description, logo] = horizontal.areas(area);
let [logo_area, description] = horizontal.areas(area);
render_crate_description(description, buf);
render_logo(self.row_index, logo, buf);
let eye_state = if self.row_index % 2 == 0 {
MascotEyeColor::Default
} else {
MascotEyeColor::Red
};
RatatuiMascot::default().set_eye(eye_state).render(
logo_area.inner(Margin {
vertical: 0,
horizontal: 2,
}),
buf,
);
}
}
@ -96,70 +73,3 @@ fn render_crate_description(area: Rect, buf: &mut Buffer) {
.scroll((0, 0))
.render(area, buf);
}
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const.
///
/// The logo is rendered in three colors, one for the rat, one for the terminal, and one for the
/// rat's eye. The eye color alternates between two colors based on the selected row.
#[allow(clippy::cast_possible_truncation)]
pub fn render_logo(selected_row: usize, area: Rect, buf: &mut Buffer) {
let eye_color = if selected_row % 2 == 0 {
THEME.logo.rat_eye
} else {
THEME.logo.rat_eye_alt
};
let area = area.inner(Margin {
vertical: 0,
horizontal: 2,
});
for (y, (line1, line2)) in RATATUI_LOGO.iter().tuples().enumerate() {
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
let x = area.left() + x as u16;
let y = area.top() + y as u16;
let cell = &mut buf[(x, y)];
let rat_color = THEME.logo.rat;
let term_color = THEME.logo.term;
match (ch1, ch2) {
('█', '█') => {
cell.set_char('█');
cell.fg = rat_color;
cell.bg = rat_color;
}
('█', ' ') => {
cell.set_char('▀');
cell.fg = rat_color;
}
(' ', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
}
('█', 'x') => {
cell.set_char('▀');
cell.fg = rat_color;
cell.bg = term_color;
}
('x', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
cell.bg = term_color;
}
('x', 'x') => {
cell.set_char(' ');
cell.fg = term_color;
cell.bg = term_color;
}
('█', 'e') => {
cell.set_char('▀');
cell.fg = rat_color;
cell.bg = eye_color;
}
('e', '█') => {
cell.set_char('▄');
cell.fg = rat_color;
cell.bg = eye_color;
}
(_, _) => {}
};
}
}
}

View File

@ -22,10 +22,8 @@ pub struct KeyBinding {
}
pub struct Logo {
pub rat: Color,
pub rat_eye: Color,
pub rat_eye_alt: Color,
pub term: Color,
}
pub struct Email {
@ -77,10 +75,8 @@ pub const THEME: Theme = Theme {
description: Style::new().fg(LIGHT_GRAY).bg(DARK_BLUE),
description_title: Style::new().fg(LIGHT_GRAY).add_modifier(Modifier::BOLD),
logo: Logo {
rat: WHITE,
rat_eye: BLACK,
rat_eye_alt: RED,
term: BLACK,
},
key_binding: KeyBinding {
key: Style::new().fg(BLACK).bg(DARK_GRAY),

View File

@ -44,6 +44,7 @@ cargo add ratatui-widgets
- [`LineGauge`]: displays progress as a line.
- [`List`]: displays a list of items and allows selection.
- [`RatatuiLogo`]: displays the Ratatui logo.
- [`RatatuiMascot`]: displays the Ratatui mascot.
- [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
- [`Scrollbar`]: displays a scrollbar.
- [`Sparkline`]: displays a single dataset as a sparkline.
@ -60,6 +61,7 @@ cargo add ratatui-widgets
[`LineGauge`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/gauge/struct.LineGauge.html
[`List`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/list/struct.List.html
[`RatatuiLogo`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/logo/struct.RatatuiLogo.html
[`RatatuiMascot`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/mascot/struct.RatatuiMascot.html
[`Paragraph`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/paragraph/struct.Paragraph.html
[`Scrollbar`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/scrollbar/struct.Scrollbar.html
[`Sparkline`]: https://docs.rs/ratatui-widgets/latest/ratatui_widgets/sparkline/struct.Sparkline.html

View File

@ -43,6 +43,7 @@
//! - [`LineGauge`]: displays progress as a line.
//! - [`List`]: displays a list of items and allows selection.
//! - [`RatatuiLogo`]: displays the Ratatui logo.
//! - [`RatatuiMascot`]: displays the Ratatui mascot.
//! - [`Paragraph`]: displays a paragraph of optionally styled and wrapped text.
//! - [`Scrollbar`]: displays a scrollbar.
//! - [`Sparkline`]: displays a single dataset as a sparkline.
@ -59,6 +60,7 @@
//! [`LineGauge`]: crate::gauge::LineGauge
//! [`List`]: crate::list::List
//! [`RatatuiLogo`]: crate::logo::RatatuiLogo
//! [`RatatuiMascot`]: crate::mascot::RatatuiMascot
//! [`Paragraph`]: crate::paragraph::Paragraph
//! [`Scrollbar`]: crate::scrollbar::Scrollbar
//! [`Sparkline`]: crate::sparkline::Sparkline
@ -86,6 +88,7 @@ pub mod clear;
pub mod gauge;
pub mod list;
pub mod logo;
pub mod mascot;
pub mod paragraph;
pub mod scrollbar;
pub mod sparkline;

View File

@ -0,0 +1,228 @@
//! A Ratatui mascot widget
//!
//! The mascot takes 32x16 cells and is rendered using half block characters.
use itertools::Itertools;
use ratatui_core::{buffer::Buffer, layout::Rect, style::Color, widgets::Widget}; // tuples();
const RATATUI_MASCOT: &str = indoc::indoc! {"
hhh
hhhhhh
hhhhhhh
hhhhhhhh
hhhhhhhhh
hhhhhhhhhh
hhhhhhhhhhhh
hhhhhhhhhhhhh
hhhhhhhhhhhhh
hhhhhhhhhhh
hhhhh
hhh ee
h
"
};
const EMPTY: char = ' ';
const RAT: char = '█';
const HAT: char = 'h';
const EYE: char = 'e';
const TERM: char = '░';
const TERM_BORDER: char = '▒';
const TERM_CURSOR: char = '▓';
/// State for the mascot's eye
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MascotEyeColor {
/// The default eye color
#[default]
Default,
/// The red eye color
Red,
}
/// A widget that renders the Ratatui mascot
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RatatuiMascot {
eye_state: MascotEyeColor,
/// The color of the rat
rat_color: Color,
/// The color of the rat's eye
rat_eye_color: Color,
/// The color of the rat's eye when blinking
rat_eye_blink: Color,
/// The color of the rat's hat
hat_color: Color,
/// The color of the terminal
term_color: Color,
/// The color of the terminal border
term_border_color: Color,
/// The color of the terminal cursor
term_cursor_color: Color,
}
impl Default for RatatuiMascot {
fn default() -> Self {
Self {
rat_color: Color::Indexed(252), // light_gray #d0d0d0
hat_color: Color::Indexed(231), // white #ffffff
rat_eye_color: Color::Indexed(236), // dark_charcoal #303030
rat_eye_blink: Color::Indexed(196), // red #ff0000
term_color: Color::Indexed(232), // vampire_black #080808
term_border_color: Color::Indexed(237), // gray #808080
term_cursor_color: Color::Indexed(248), // dark_gray #a8a8a8
eye_state: MascotEyeColor::Default,
}
}
}
impl RatatuiMascot {
/// Create a new Ratatui mascot widget
pub fn new() -> Self {
Self {
..Default::default()
}
}
/// Set the eye state (open / blinking)
#[must_use]
pub const fn set_eye(self, rat_eye: MascotEyeColor) -> Self {
Self {
eye_state: rat_eye,
..self
}
}
const fn color_for(&self, c: char) -> Option<Color> {
match c {
RAT => Some(self.rat_color),
HAT => Some(self.hat_color),
EYE => Some(match self.eye_state {
MascotEyeColor::Default => self.rat_eye_color,
MascotEyeColor::Red => self.rat_eye_blink,
}),
TERM => Some(self.term_color),
TERM_CURSOR => Some(self.term_cursor_color),
TERM_BORDER => Some(self.term_border_color),
_ => None,
}
}
}
impl Widget for RatatuiMascot {
/// Use half block characters to render a logo based on the `RATATUI_LOGO` const.
///
/// The logo colors are hardcorded in the widget.
/// The eye color depends on whether it's open / blinking
fn render(self, area: Rect, buf: &mut Buffer) {
for (y, (line1, line2)) in RATATUI_MASCOT.lines().tuples().enumerate() {
for (x, (ch1, ch2)) in line1.chars().zip(line2.chars()).enumerate() {
let x = area.left() + x as u16;
let y = area.top() + y as u16;
let cell = &mut buf[(x, y)];
// given two cells which make up the top and bottom of the character,
// Foreground color should be the non-space, non-terminal
let (fg, bg) = match (ch1, ch2) {
(EMPTY, EMPTY) => (None, None),
(c, EMPTY) | (EMPTY, c) => (self.color_for(c), None),
(TERM, TERM_BORDER) => (self.color_for(TERM_BORDER), self.color_for(TERM)),
(TERM, c) | (c, TERM) => (self.color_for(c), self.color_for(TERM)),
(c1, c2) => (self.color_for(c1), self.color_for(c2)),
};
// symbol should make the empty space or terminal bg as the empty part of the block
let symbol = match (ch1, ch2) {
(EMPTY, EMPTY) => None,
(TERM, TERM) => Some(EMPTY),
(_, EMPTY | TERM) => Some('▀'),
(EMPTY | TERM, _) => Some('▄'),
(c, d) if c == d => Some('█'),
(_, _) => Some('▀'),
};
if let Some(fg) = fg {
cell.fg = fg;
}
if let Some(bg) = bg {
cell.bg = bg;
}
if let Some(symb) = symbol {
cell.set_char(symb);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_mascot() {
let mascot = RatatuiMascot::new();
assert_eq!(mascot.eye_state, MascotEyeColor::Default);
}
#[test]
fn set_eye_color() {
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 16));
let mascot = RatatuiMascot::new().set_eye(MascotEyeColor::Red);
mascot.render(buf.area, &mut buf);
assert_eq!(mascot.eye_state, MascotEyeColor::Red);
assert_eq!(buf[(21, 5)].bg, Color::Indexed(196));
}
#[test]
fn render_mascot() {
let mascot = RatatuiMascot::new();
let mut buf = Buffer::empty(Rect::new(0, 0, 32, 16));
mascot.render(buf.area, &mut buf);
assert_eq!(buf.area.as_size(), (32, 16).into());
assert_eq!(buf[(21, 5)].bg, Color::Indexed(236));
assert_eq!(
buf.content
.iter()
.map(ratatui_core::buffer::Cell::symbol)
.collect::<String>(),
Buffer::with_lines([
" ▄▄███ ",
" ▄███████ ",
" ▄█████████ ",
" ████████████ ",
" ▀███████████▀ ▄▄██████",
" ▀███▀▄█▀▀████████ ",
" ▄▄▄▄▀▄████████████ ",
" ████████████████ ",
" ▀███▀██████████ ",
" ▄▀▀▄ █████████ ",
" ▄▀ ▄ ▀▄▀█████████ ",
" ▄▀ ▀▀ ▀▄▀███████ ",
" ▄▀ ▄▄ ▀▄▀█████████ ",
" ▄▀ ▀▀ ▀▄▀██▀ ███ ",
"█ ▀▄▀ ▄██ ",
" ▀▄ ▀▄▀█ ",
])
.content
.iter()
.map(ratatui_core::buffer::Cell::symbol)
.collect::<String>()
);
}
}

View File

@ -25,6 +25,8 @@
//! - [`Sparkline`]: display a single data set as a sparkline.
//! - [`Table`]: displays multiple rows and columns in a grid and allows selection.
//! - [`Tabs`]: displays a tab bar and allows selection.
//! - [`RatatuiLogo`]: displays the Ratatui logo.
//! - [`RatatuiMascot`]: displays the Ratatui mascot.
//!
//! [`Canvas`]: crate::widgets::canvas::Canvas
@ -43,6 +45,7 @@ pub use ratatui_widgets::{
gauge::{Gauge, LineGauge},
list::{List, ListDirection, ListItem, ListState},
logo::{RatatuiLogo, Size as RatatuiLogoSize},
mascot::{MascotEyeColor, RatatuiMascot},
paragraph::{Paragraph, Wrap},
scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState},
sparkline::{RenderDirection, Sparkline, SparklineBar},