Skip to main content

basalt_tui/
stylized_text.rs

1//! Text stylizing.
2//!
3//! The [`stylize`] function allows converting regular A-z letters and 0โ€“9 numbers into stylized
4//! variants. The text is converted according to the [`FontStyle`] enum.
5//!
6//! # Examples
7//!
8//! ```
9//! use basalt_tui::stylized_text::{FontStyle, stylize};
10//!
11//! assert_eq!(stylize("My Heading", FontStyle::FrakturBold), "๐•ธ๐–ž ๐•ณ๐–Š๐–†๐–‰๐–Ž๐–“๐–Œ");
12//! ```
13
14/// Enum representing different font styles.
15///
16/// - BlackBoardBold (๐”น๐•๐•’๐•”๐•œ๐”น๐• ๐•’๐•ฃ๐••๐”น๐• ๐•๐••)
17/// - FrakturBold (๐•ฑ๐–—๐–†๐–๐–™๐–š๐–—๐•ญ๐–”๐–‘๐–‰)
18/// - Script (๐“ข๐“ฌ๐“ป๐“ฒ๐“น๐“ฝ)
19#[derive(Debug, Clone, Copy, PartialEq, serde::Deserialize)]
20#[serde(rename_all = "kebab-case")]
21pub enum FontStyle {
22    /// Blackboard Bold (Double-struck) style (e.g., ๐•‹๐•š๐•ฅ๐•๐•–).
23    BlackBoardBold,
24    /// Bold Fraktur style. (e.g., ๐•ฟ๐–Ž๐–™๐–‘๐–Š)
25    FrakturBold,
26    /// Script style. (e.g., ๐“ฃ๐“ฒ๐“ฝ๐“ต๐“ฎ)
27    Script,
28}
29
30/// Stylizes the given input string using the specified [`FontStyle`].
31///
32/// Each character in the input is mapped to its corresponding stylized Unicode character based on
33/// the provided style. Characters that do not have a stylized equivalent are returned unchanged.
34///
35/// # Examples
36///
37/// ```
38/// use basalt_tui::stylized_text::{FontStyle, stylize};
39///
40/// assert_eq!(stylize("Black Board Bold", FontStyle::BlackBoardBold), "๐”น๐•๐•’๐•”๐•œ ๐”น๐• ๐•’๐•ฃ๐•• ๐”น๐• ๐•๐••");
41/// assert_eq!(stylize("Fraktur Bold", FontStyle::FrakturBold), "๐•ฑ๐–—๐–†๐–๐–™๐–š๐–— ๐•ญ๐–”๐–‘๐–‰");
42/// assert_eq!(stylize("Script", FontStyle::Script), "๐“ข๐“ฌ๐“ป๐“ฒ๐“น๐“ฝ");
43/// ```
44pub fn stylize(input: &str, style: FontStyle) -> String {
45    input.chars().map(|c| stylize_char(c, style)).collect()
46}
47
48/// Returns the stylized Unicode character for a given `char` and [`FontStyle`].
49///
50/// Letters between A-z and number 0-9 are stylized. Characters outside the stylized range (e.g.,
51/// punctuation) are returned as-is.
52///
53/// To find the corresponding stylized character, we add the remainder to the unicode character
54/// range, which is achieved by subtracting the start position from the input `char`.
55fn stylize_char(c: char, style: FontStyle) -> char {
56    match style {
57        FontStyle::BlackBoardBold => match c {
58            'C' => char::from_u32(0x2102),
59            'H' => char::from_u32(0x210D),
60            'N' => char::from_u32(0x2115),
61            'P' => char::from_u32(0x2119),
62            'Q' => char::from_u32(0x211A),
63            'R' => char::from_u32(0x211D),
64            'Z' => char::from_u32(0x2124),
65            'A'..='Z' => char::from_u32(0x1D538 + (c as u32 - 'A' as u32)),
66            'a'..='z' => char::from_u32(0x1D552 + (c as u32 - 'a' as u32)),
67            '0'..='9' => char::from_u32(0x1D7D8 + (c as u32 - '0' as u32)),
68            _ => None,
69        },
70        FontStyle::FrakturBold => match c {
71            'A'..='Z' => char::from_u32(0x1D56C + (c as u32 - 'A' as u32)),
72            'a'..='z' => char::from_u32(0x1D586 + (c as u32 - 'a' as u32)),
73            '0'..='9' => char::from_u32(0x1D7CE + (c as u32 - '0' as u32)),
74            _ => None,
75        },
76        FontStyle::Script => match c {
77            'A'..='Z' => char::from_u32(0x1D4D0 + (c as u32 - 'A' as u32)),
78            'a'..='z' => char::from_u32(0x1D4EA + (c as u32 - 'a' as u32)),
79            '0'..='9' => char::from_u32(0x1D7CE + (c as u32 - '0' as u32)),
80            _ => None,
81        },
82    }
83    .unwrap_or(c)
84}
85
86#[cfg(test)]
87mod tests {
88    use similar_asserts::assert_eq;
89
90    use super::*;
91
92    #[test]
93    fn test_stylize() {
94        let text = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
95        let tests = [
96            (
97                FontStyle::Script,
98                "๐“๐“‘๐“’๐““๐“”๐“•๐“–๐“—๐“˜๐“™๐“š๐“›๐“œ๐“๐“ž๐“Ÿ๐“ ๐“ก๐“ข๐“ฃ๐“ค๐“ฅ๐“ฆ๐“ง๐“จ๐“ฉ๐“ช๐“ซ๐“ฌ๐“ญ๐“ฎ๐“ฏ๐“ฐ๐“ฑ๐“ฒ๐“ณ๐“ด๐“ต๐“ถ๐“ท๐“ธ๐“น๐“บ๐“ป๐“ผ๐“ฝ๐“พ๐“ฟ๐”€๐”๐”‚๐”ƒ๐ŸŽ๐Ÿ๐Ÿ๐Ÿ‘๐Ÿ’๐Ÿ“๐Ÿ”๐Ÿ•๐Ÿ–๐Ÿ—",
99            ),
100            (
101                FontStyle::FrakturBold,
102                "๐•ฌ๐•ญ๐•ฎ๐•ฏ๐•ฐ๐•ฑ๐•ฒ๐•ณ๐•ด๐•ต๐•ถ๐•ท๐•ธ๐•น๐•บ๐•ป๐•ผ๐•ฝ๐•พ๐•ฟ๐–€๐–๐–‚๐–ƒ๐–„๐–…๐–†๐–‡๐–ˆ๐–‰๐–Š๐–‹๐–Œ๐–๐–Ž๐–๐–๐–‘๐–’๐–“๐–”๐–•๐––๐–—๐–˜๐–™๐–š๐–›๐–œ๐–๐–ž๐–Ÿ๐ŸŽ๐Ÿ๐Ÿ๐Ÿ‘๐Ÿ’๐Ÿ“๐Ÿ”๐Ÿ•๐Ÿ–๐Ÿ—",
103            ),
104            (
105                FontStyle::BlackBoardBold,
106                "๐”ธ๐”นโ„‚๐”ป๐”ผ๐”ฝ๐”พโ„๐•€๐•๐•‚๐•ƒ๐•„โ„•๐•†โ„™โ„šโ„๐•Š๐•‹๐•Œ๐•๐•Ž๐•๐•โ„ค๐•’๐•“๐•”๐••๐•–๐•—๐•˜๐•™๐•š๐•›๐•œ๐•๐•ž๐•Ÿ๐• ๐•ก๐•ข๐•ฃ๐•ค๐•ฅ๐•ฆ๐•ง๐•จ๐•ฉ๐•ช๐•ซ๐Ÿ˜๐Ÿ™๐Ÿš๐Ÿ›๐Ÿœ๐Ÿ๐Ÿž๐ŸŸ๐Ÿ ๐Ÿก",
107            ),
108        ];
109
110        tests
111            .iter()
112            .for_each(|test| assert_eq!(stylize(text, test.0), test.1));
113    }
114}