Skip to main content

tui_cards/
lib.rs

1//! A [Ratatui] widget to render charming playing cards in the terminal. Part of the [tui-widgets]
2//! suite by [Joshka].
3//!
4//! ![demo](https://vhs.charm.sh/vhs-34mhPM1Juk2XnnLTGpOtE9.gif)
5//!
6//! [![Crate badge]][Crate]
7//! [![Docs Badge]][Docs]
8//! [![Deps Badge]][Dependency Status]
9//! [![License Badge]][License]
10//! [![Coverage Badge]][Coverage]
11//! [![Discord Badge]][Ratatui Discord]
12//!
13//! [GitHub Repository] · [API Docs] · [Examples] · [Changelog] · [Contributing]
14//!
15//! # Usage
16//!
17//! Create a `Card` and render it directly in a frame.
18//!
19//! ```no_run
20//! use tui_cards::{Card, Rank, Suit};
21//!
22//! # fn draw(frame: &mut ratatui::Frame) {
23//! let card = Card::new(Rank::Ace, Suit::Spades);
24//! frame.render_widget(&card, frame.area());
25//! # }
26//! ```
27//!
28//! # Demo
29//!
30//! ```shell
31//! cargo run --example card
32//! ```
33//!
34//! # More widgets
35//!
36//! For the full suite of widgets, see [tui-widgets].
37//!
38//! [Crate]: https://crates.io/crates/tui-cards
39//! [Docs]: https://docs.rs/tui-cards/
40//! [Dependency Status]: https://deps.rs/repo/github/ratatui/tui-widgets
41//! [Coverage]: https://app.codecov.io/gh/ratatui/tui-widgets
42//! [Ratatui Discord]: https://discord.gg/pMCEU9hNEj
43//! [Crate badge]: https://img.shields.io/crates/v/tui-cards?logo=rust&style=flat
44//! [Docs Badge]: https://img.shields.io/docsrs/tui-cards?logo=rust&style=flat
45//! [Deps Badge]: https://deps.rs/repo/github/ratatui/tui-widgets/status.svg?style=flat
46//! [License Badge]: https://img.shields.io/crates/l/tui-cards?style=flat
47//! [License]: https://github.com/ratatui/tui-widgets/blob/main/LICENSE-MIT
48//! [Coverage Badge]:
49//!     https://img.shields.io/codecov/c/github/ratatui/tui-widgets?logo=codecov&style=flat
50//! [Discord Badge]: https://img.shields.io/discord/1070692720437383208?logo=discord&style=flat
51//!
52//! [GitHub Repository]: https://github.com/ratatui/tui-widgets
53//! [API Docs]: https://docs.rs/tui-cards/
54//! [Examples]: https://github.com/ratatui/tui-widgets/tree/main/tui-cards/examples
55//! [Changelog]: https://github.com/ratatui/tui-widgets/blob/main/tui-cards/CHANGELOG.md
56//! [Contributing]: https://github.com/ratatui/tui-widgets/blob/main/CONTRIBUTING.md
57//! [Joshka]: https://github.com/joshka
58//! [Ratatui]: https://ratatui.rs
59//! [tui-widgets]: https://crates.io/crates/tui-widgets
60use std::iter::zip;
61
62use indoc::indoc;
63use ratatui_core::buffer::Buffer;
64use ratatui_core::layout::Rect;
65use ratatui_core::style::{Color, Stylize};
66use ratatui_core::widgets::Widget;
67use strum::{Display, EnumIter};
68
69/// A playing card.
70///
71/// # Example
72///
73/// ```rust
74/// use tui_cards::{Card, Rank, Suit};
75/// # fn draw(frame: &mut ratatui::Frame) {
76/// let card = Card::new(Rank::Ace, Suit::Spades);
77/// frame.render_widget(&card, frame.area());
78/// # }
79/// ```
80#[derive(Debug, Clone, Copy)]
81pub struct Card {
82    pub rank: Rank,
83    pub suit: Suit,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter)]
87pub enum Rank {
88    Ace,
89    Two,
90    Three,
91    Four,
92    Five,
93    Six,
94    Seven,
95    Eight,
96    Nine,
97    Ten,
98    Jack,
99    Queen,
100    King,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, EnumIter)]
104pub enum Suit {
105    Spades,
106    Hearts,
107    Diamonds,
108    Clubs,
109}
110
111impl Card {
112    pub const fn new(rank: Rank, suit: Suit) -> Self {
113        Self { rank, suit }
114    }
115
116    pub fn as_colored_symbol(&self) -> String {
117        format!(
118            "{}{}",
119            self.rank.as_symbol(),
120            self.suit.as_four_color_symbol()
121        )
122    }
123}
124
125impl Rank {
126    pub const fn as_symbol(self) -> char {
127        match self {
128            Self::Ace => 'A',
129            Self::Two => '2',
130            Self::Three => '3',
131            Self::Four => '4',
132            Self::Five => '5',
133            Self::Six => '6',
134            Self::Seven => '7',
135            Self::Eight => '8',
136            Self::Nine => '9',
137            Self::Ten => 'T',
138            Self::Jack => 'J',
139            Self::Queen => 'Q',
140            Self::King => 'K',
141        }
142    }
143}
144
145impl Suit {
146    pub const fn color(self) -> Color {
147        match self {
148            Self::Clubs => Color::Green,
149            Self::Diamonds => Color::Blue,
150            Self::Hearts => Color::Red,
151            Self::Spades => Color::Black,
152        }
153    }
154
155    pub const fn as_symbol(self) -> char {
156        match self {
157            Self::Clubs => '♣',
158            Self::Diamonds => '♦',
159            Self::Hearts => '♥',
160            Self::Spades => '♠',
161        }
162    }
163
164    pub const fn as_colored_symbol(self) -> &'static str {
165        match self {
166            Self::Clubs => "\u{2663}\u{FE0F}",
167            Self::Diamonds => "\u{2666}\u{FE0F}",
168            Self::Hearts => "\u{2665}\u{FE0F}",
169            Self::Spades => "\u{2660}\u{FE0F}",
170        }
171    }
172
173    pub const fn as_four_color_symbol(self) -> &'static str {
174        match self {
175            Self::Clubs => "\u{2618}\u{FE0F}",     // shamrock
176            Self::Diamonds => "\u{1F537}\u{FE0F}", // blue diamond
177            Self::Hearts => "\u{2665}\u{FE0F}",
178            Self::Spades => "\u{2660}\u{FE0F}",
179        }
180    }
181}
182
183impl Rank {
184    pub const fn template(self) -> &'static str {
185        match self {
186            Self::Ace => indoc! {"
187                ╭────────────╮
188                │ A          │
189                │            │
190                │            │
191                │     xx     │
192                │            │
193                │            │
194                │          A │
195                ╰────────────╯"},
196            Self::Two => indoc! {"
197                ╭────────────╮
198                │ 2   xx     │
199                │            │
200                │            │
201                │            │
202                │            │
203                │            │
204                │     xx   2 │
205                ╰────────────╯"},
206            Self::Three => indoc! {"
207                ╭────────────╮
208                │ 3   xx     │
209                │            │
210                │            │
211                │     xx     │
212                │            │
213                │            │
214                │     xx   3 │
215                ╰────────────╯"},
216            Self::Four => indoc! {"
217                ╭────────────╮
218                │ 4xx    xx  │
219                │            │
220                │            │
221                │            │
222                │            │
223                │            │
224                │  xx    xx4 │
225                ╰────────────╯"},
226            Self::Five => indoc! {"
227                ╭────────────╮
228                │ 5xx    xx  │
229                │            │
230                │            │
231                │     xx     │
232                │            │
233                │            │
234                │  xx    xx5 │
235                ╰────────────╯"},
236            Self::Six => indoc! {"
237                ╭────────────╮
238                │ 6xx    xx  │
239                │            │
240                │            │
241                │  xx    xx  │
242                │            │
243                │            │
244                │  xx    xx6 │
245                ╰────────────╯"},
246            Self::Seven => indoc! {"
247                ╭────────────╮
248                │ 7xx    xx  │
249                │            │
250                │     xx     │
251                │  xx    xx  │
252                │            │
253                │            │
254                │  xx    xx7 │
255                ╰────────────╯"},
256            Self::Eight => indoc! {"
257                ╭────────────╮
258                │ 8xx    xx  │
259                │            │
260                │     xx     │
261                │  xx    xx  │
262                │     xx     │
263                │            │
264                │  xx    xx8 │
265                ╰────────────╯"},
266            Self::Nine => indoc! {"
267                ╭────────────╮
268                │ 9xx    xx  │
269                │            │
270                │  xx    xx  │
271                │     xx     │
272                │  xx    xx  │
273                │            │
274                │  xx    xx9 │
275                ╰────────────╯
276                "},
277            Self::Ten => indoc! {"
278                ╭────────────╮
279                │10xx    xx  │
280                │     xx     │
281                │  xx    xx  │
282                │            │
283                │  xx    xx  │
284                │     xx     │
285                │  xx    xx10│
286                ╰────────────╯"},
287            Self::Jack => indoc! {"
288                ╭────────────╮
289                │ Jxx        │
290                │       JJ   │
291                │       JJ   │
292                │       JJ   │
293                │  JJ   JJ   │
294                │   JJJJJ    │
295                │        xxJ │
296                ╰────────────╯"},
297            Self::Queen => indoc! {"
298                ╭────────────╮
299                │ Qxx        │
300                │   QQQQQ    │
301                │  QQ   QQ   │
302                │  QQ   QQ   │
303                │  QQ   QQ   │
304                │   QQQQ  Q  │
305                │        xxQ │
306                ╰────────────╯
307            "},
308            Self::King => indoc! {"
309                ╭────────────╮
310                │ Kxx        │
311                │  KK    KK  │
312                │  KK   KK   │
313                │  KK KK     │
314                │  KK   KK   │
315                │  KK    KK  │
316                │        xxK │
317                ╰────────────╯"},
318        }
319    }
320}
321
322impl Widget for &Card {
323    fn render(self, area: Rect, buf: &mut Buffer)
324    where
325        Self: Sized,
326    {
327        let template = self.rank.template();
328        let symbol = self.suit.as_four_color_symbol();
329        let card = template.replace("xx", symbol);
330        let color = self.suit.color();
331        for (line, row) in zip(card.lines(), area.rows()) {
332            let span = line.fg(color).bg(Color::White);
333            span.render(row, buf);
334        }
335    }
336}