mirror of
https://github.com/ratatui/ratatui.git
synced 2025-09-27 04:50:46 +00:00
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:
parent
904b0aa723
commit
50ba96518f
@ -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;
|
||||
}
|
||||
(_, _) => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
228
ratatui-widgets/src/mascot.rs
Normal file
228
ratatui-widgets/src/mascot.rs
Normal 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>()
|
||||
);
|
||||
}
|
||||
}
|
@ -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},
|
||||
|
Loading…
x
Reference in New Issue
Block a user