From 2805dddf0527584da9c7865ff6a78a9c74731187 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Sat, 5 Oct 2024 17:35:43 -0700 Subject: [PATCH] feat(logo): Add a Ratatui logo widget This is a simple logo widget that can be used to render the Ratatui logo in the terminal. It is used in the `examples/ratatui-logo.rs` example, and may be used in your applications' help or about screens. ```rust use ratatui::{Frame, widgets::RatatuiLogo}; fn draw(frame: &mut Frame) { frame.render_widget(RatatuiLogo::tiny(), frame.area()); } ``` --- Cargo.toml | 1 + examples/ratatui-logo.rs | 79 +++++------ examples/vhs/ratatui-logo.tape | 2 + src/widgets.rs | 2 + src/widgets/logo.rs | 236 +++++++++++++++++++++++++++++++++ 5 files changed, 273 insertions(+), 47 deletions(-) create mode 100644 src/widgets/logo.rs diff --git a/Cargo.toml b/Cargo.toml index 8db15036..d4b322f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ cassowary = "0.3" compact_str = "0.8.0" crossterm = { version = "0.28.1", optional = true } document-features = { version = "0.2.7", optional = true } +indoc = "2" instability = "0.3.1" itertools = "0.13" lru = "0.12.0" diff --git a/examples/ratatui-logo.rs b/examples/ratatui-logo.rs index 675662e0..bf521812 100644 --- a/examples/ratatui-logo.rs +++ b/examples/ratatui-logo.rs @@ -13,57 +13,42 @@ //! [examples]: https://github.com/ratatui/ratatui/blob/main/examples //! [examples readme]: https://github.com/ratatui/ratatui/blob/main/examples/README.md -use std::{ - io::{self}, - thread::sleep, - time::Duration, +use std::env::args; + +use color_eyre::Result; +use crossterm::event::{self, Event}; +use ratatui::{ + layout::{Constraint, Layout}, + widgets::{RatatuiLogo, RatatuiLogoSize}, + DefaultTerminal, TerminalOptions, Viewport, }; -use indoc::indoc; -use itertools::izip; -use ratatui::{widgets::Paragraph, TerminalOptions, Viewport}; - -/// A fun example of using half block characters to draw a logo -#[allow(clippy::many_single_char_names)] -fn logo() -> String { - let r = indoc! {" - ▄▄▄ - █▄▄▀ - █ █ - "}; - let a = indoc! {" - ▄▄ - █▄▄█ - █ █ - "}; - let t = indoc! {" - ▄▄▄ - █ - █ - "}; - let u = indoc! {" - ▄ ▄ - █ █ - ▀▄▄▀ - "}; - let i = indoc! {" - ▄ - █ - █ - "}; - izip!(r.lines(), a.lines(), t.lines(), u.lines(), i.lines()) - .map(|(r, a, t, u, i)| format!("{r:5}{a:5}{t:4}{a:5}{t:4}{u:5}{i:5}")) - .collect::>() - .join("\n") -} - -fn main() -> io::Result<()> { - let mut terminal = ratatui::init_with_options(TerminalOptions { +fn main() -> Result<()> { + color_eyre::install()?; + let terminal = ratatui::init_with_options(TerminalOptions { viewport: Viewport::Inline(3), }); - terminal.draw(|frame| frame.render_widget(Paragraph::new(logo()), frame.area()))?; - sleep(Duration::from_secs(5)); + let size = match args().nth(1).as_deref() { + Some("small") => RatatuiLogoSize::Small, + Some("tiny") => RatatuiLogoSize::Tiny, + _ => RatatuiLogoSize::default(), + }; + let result = run(terminal, size); ratatui::restore(); println!(); - Ok(()) + result +} + +fn run(mut terminal: DefaultTerminal, size: RatatuiLogoSize) -> Result<()> { + loop { + terminal.draw(|frame| { + use Constraint::{Fill, Length}; + let [top, bottom] = Layout::vertical([Length(1), Fill(1)]).areas(frame.area()); + frame.render_widget("Powered by", top); + frame.render_widget(RatatuiLogo::new(size), bottom); + })?; + if matches!(event::read()?, Event::Key(_)) { + break Ok(()); + } + } } diff --git a/examples/vhs/ratatui-logo.tape b/examples/vhs/ratatui-logo.tape index b508ed81..d520bf90 100644 --- a/examples/vhs/ratatui-logo.tape +++ b/examples/vhs/ratatui-logo.tape @@ -10,3 +10,5 @@ Enter Sleep 2s Show Sleep 2s +Hide +Escape diff --git a/src/widgets.rs b/src/widgets.rs index ae0a330c..28afc893 100644 --- a/src/widgets.rs +++ b/src/widgets.rs @@ -31,6 +31,7 @@ mod chart; mod clear; mod gauge; mod list; +mod logo; mod paragraph; mod reflow; mod scrollbar; @@ -46,6 +47,7 @@ pub use self::{ clear::Clear, gauge::{Gauge, LineGauge}, list::{List, ListDirection, ListItem, ListState}, + logo::{RatatuiLogo, Size as RatatuiLogoSize}, paragraph::{Paragraph, Wrap}, scrollbar::{ScrollDirection, Scrollbar, ScrollbarOrientation, ScrollbarState}, sparkline::{RenderDirection, Sparkline}, diff --git a/src/widgets/logo.rs b/src/widgets/logo.rs new file mode 100644 index 00000000..2f3212fa --- /dev/null +++ b/src/widgets/logo.rs @@ -0,0 +1,236 @@ +use indoc::indoc; + +use crate::{buffer::Buffer, layout::Rect, text::Text, widgets::Widget}; + +/// A widget that renders the Ratatui logo +/// +/// The Ratatui logo takes up two lines of text and comes in two sizes: `Tiny` and `Small`. This may +/// be used in an application's help or about screen to show that it is powered by Ratatui. +/// +/// # Examples +/// +/// The [Ratatui-logo] example demonstrates how to use the `RatatuiLogo` widget. This can be run by +/// cloning the Ratatui repository and then running the following command with an optional size +/// argument: +/// +/// ```shell +/// cargo run --example ratatui-logo [size] +/// ``` +/// +/// [Ratatui-logo]: https://github.com/ratatui/ratatui/blob/main/examples/ratatui-logo.rs +/// +/// ## Tiny (default, 2x15 characters) +/// +/// ``` +/// use ratatui::widgets::RatatuiLogo; +/// +/// # fn draw(frame: &mut ratatui::Frame) { +/// frame.render_widget(RatatuiLogo::tiny(), frame.area()); +/// # } +/// ``` +/// +/// Renders: +/// +/// ```text +/// ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌ +/// ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌ +/// ``` +/// +/// ## Small (2x27 characters) +/// +/// ``` +/// use ratatui::widgets::RatatuiLogo; +/// +/// # fn draw(frame: &mut ratatui::Frame) { +/// frame.render_widget(RatatuiLogo::small(), frame.area()); +/// # } +/// ``` +/// +/// Renders: +/// +/// ```text +/// █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █ +/// █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █ +/// ``` +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +pub struct RatatuiLogo { + size: Size, +} + +/// The size of the logo +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)] +#[non_exhaustive] +pub enum Size { + /// A tiny logo + /// + /// The default size of the logo (2x15 characters) + /// + /// ```text + /// ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌ + /// ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌ + /// ``` + #[default] + Tiny, + /// A small logo + /// + /// A slightly larger version of the logo (2x27 characters) + /// + /// ```text + /// █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █ + /// █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █ + /// ``` + Small, +} + +impl RatatuiLogo { + /// Create a new Ratatui logo widget + /// + /// # Examples + /// + /// ``` + /// use ratatui::widgets::{RatatuiLogo, RatatuiLogoSize}; + /// + /// let logo = RatatuiLogo::new(RatatuiLogoSize::Tiny); + /// ``` + pub const fn new(size: Size) -> Self { + Self { size } + } + + /// Set the size of the logo + /// + /// # Examples + /// + /// ``` + /// use ratatui::widgets::{RatatuiLogo, RatatuiLogoSize}; + /// + /// let logo = RatatuiLogo::default().size(RatatuiLogoSize::Small); + /// ``` + #[must_use] + pub const fn size(self, size: Size) -> Self { + let _ = self; + Self { size } + } + + /// Create a new Ratatui logo widget with a tiny size + /// + /// # Examples + /// + /// ``` + /// use ratatui::widgets::RatatuiLogo; + /// + /// let logo = RatatuiLogo::tiny(); + /// ``` + pub const fn tiny() -> Self { + Self::new(Size::Tiny) + } + + /// Create a new Ratatui logo widget with a small size + /// + /// # Examples + /// + /// ``` + /// use ratatui::widgets::RatatuiLogo; + /// + /// let logo = RatatuiLogo::small(); + /// ``` + pub const fn small() -> Self { + Self::new(Size::Small) + } +} + +impl Widget for RatatuiLogo { + fn render(self, area: Rect, buf: &mut Buffer) { + let logo = self.size.as_str(); + Text::raw(logo).render(area, buf); + } +} + +impl Size { + const fn as_str(self) -> &'static str { + match self { + Self::Tiny => Self::tiny(), + Self::Small => Self::small(), + } + } + + const fn tiny() -> &'static str { + indoc! {" + ▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌ + ▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌ + "} + } + + const fn small() -> &'static str { + indoc! {" + █▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █ + █▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █ + "} + } +} + +#[cfg(test)] +mod tests { + use rstest::rstest; + + use super::*; + + #[rstest] + #[case::tiny(Size::Tiny)] + #[case::small(Size::Small)] + fn new_size(#[case] size: Size) { + let logo = RatatuiLogo::new(size); + assert_eq!(logo.size, size); + } + + #[test] + fn default_logo_is_tiny() { + let logo = RatatuiLogo::default(); + assert_eq!(logo.size, Size::Tiny); + } + + #[test] + fn set_logo_size_to_small() { + let logo = RatatuiLogo::default().size(Size::Small); + assert_eq!(logo.size, Size::Small); + } + + #[test] + fn tiny_logo_constant() { + let logo = RatatuiLogo::tiny(); + assert_eq!(logo.size, Size::Tiny); + } + + #[test] + fn small_logo_constant() { + let logo = RatatuiLogo::small(); + assert_eq!(logo.size, Size::Small); + } + + #[test] + #[rustfmt::skip] + fn render_tiny() { + let mut buf = Buffer::empty(Rect::new(0, 0, 15, 2)); + RatatuiLogo::tiny().render(buf.area, &mut buf); + assert_eq!( + buf, + Buffer::with_lines([ + "▛▚▗▀▖▜▘▞▚▝▛▐ ▌▌", + "▛▚▐▀▌▐ ▛▜ ▌▝▄▘▌", + ]) + ); + } + + #[test] + #[rustfmt::skip] + fn render_small() { + let mut buf = Buffer::empty(Rect::new(0, 0, 27, 2)); + RatatuiLogo::small().render(buf.area, &mut buf); + assert_eq!( + buf, + Buffer::with_lines([ + "█▀▀▄ ▄▀▀▄▝▜▛▘▄▀▀▄▝▜▛▘█ █ █", + "█▀▀▄ █▀▀█ ▐▌ █▀▀█ ▐▌ ▀▄▄▀ █", + ]) + ); + } +}