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//! 
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}