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