azalea_chat/
style.rs

1use std::{collections::HashMap, fmt, sync::LazyLock};
2
3#[cfg(feature = "azalea-buf")]
4use azalea_buf::AzBuf;
5use serde::{ser::SerializeStruct, Serialize, Serializer};
6use serde_json::Value;
7#[cfg(feature = "simdnbt")]
8use simdnbt::owned::{NbtCompound, NbtTag};
9
10#[derive(Clone, PartialEq, Eq, Debug, Hash)]
11pub struct TextColor {
12    pub value: u32,
13    pub name: Option<String>,
14}
15
16impl Serialize for TextColor {
17    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
18    where
19        S: Serializer,
20    {
21        serializer.serialize_str(&self.serialize())
22    }
23}
24
25#[cfg(feature = "simdnbt")]
26impl simdnbt::ToNbtTag for TextColor {
27    fn to_nbt_tag(self) -> simdnbt::owned::NbtTag {
28        NbtTag::String(self.serialize().into())
29    }
30}
31
32impl TextColor {
33    pub fn parse(value: String) -> Option<TextColor> {
34        if value.starts_with('#') {
35            let n = value.chars().skip(1).collect::<String>();
36            let n = u32::from_str_radix(&n, 16).ok()?;
37            return Some(TextColor::from_rgb(n));
38        }
39        let color_option = NAMED_COLORS.get(&value.to_ascii_uppercase());
40        if let Some(color) = color_option {
41            return Some(color.clone());
42        }
43        None
44    }
45
46    fn from_rgb(value: u32) -> TextColor {
47        TextColor { value, name: None }
48    }
49}
50
51static LEGACY_FORMAT_TO_COLOR: LazyLock<HashMap<&'static ChatFormatting, TextColor>> =
52    LazyLock::new(|| {
53        let mut legacy_format_to_color = HashMap::new();
54        for formatter in &ChatFormatting::FORMATTERS {
55            if !formatter.is_format() && *formatter != ChatFormatting::Reset {
56                legacy_format_to_color.insert(
57                    formatter,
58                    TextColor {
59                        value: formatter.color().unwrap(),
60                        name: Some(formatter.name().to_string()),
61                    },
62                );
63            }
64        }
65        legacy_format_to_color
66    });
67static NAMED_COLORS: LazyLock<HashMap<String, TextColor>> = LazyLock::new(|| {
68    let mut named_colors = HashMap::new();
69    for color in LEGACY_FORMAT_TO_COLOR.values() {
70        named_colors.insert(color.name.clone().unwrap(), color.clone());
71    }
72    named_colors
73});
74
75pub struct Ansi {}
76impl Ansi {
77    pub const BOLD: &'static str = "\u{1b}[1m";
78    pub const ITALIC: &'static str = "\u{1b}[3m";
79    pub const UNDERLINED: &'static str = "\u{1b}[4m";
80    pub const STRIKETHROUGH: &'static str = "\u{1b}[9m";
81    pub const OBFUSCATED: &'static str = "\u{1b}[8m";
82    pub const RESET: &'static str = "\u{1b}[m";
83
84    pub fn rgb(value: u32) -> String {
85        format!(
86            "\u{1b}[38;2;{};{};{}m",
87            (value >> 16) & 0xFF,
88            (value >> 8) & 0xFF,
89            value & 0xFF
90        )
91    }
92}
93
94#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
95#[cfg_attr(feature = "azalea-buf", derive(AzBuf))]
96pub enum ChatFormatting {
97    Black,
98    DarkBlue,
99    DarkGreen,
100    DarkAqua,
101    DarkRed,
102    DarkPurple,
103    Gold,
104    Gray,
105    DarkGray,
106    Blue,
107    Green,
108    Aqua,
109    Red,
110    LightPurple,
111    Yellow,
112    White,
113    Obfuscated,
114    Strikethrough,
115    Bold,
116    Underline,
117    Italic,
118    Reset,
119}
120
121impl ChatFormatting {
122    pub const FORMATTERS: [ChatFormatting; 22] = [
123        ChatFormatting::Black,
124        ChatFormatting::DarkBlue,
125        ChatFormatting::DarkGreen,
126        ChatFormatting::DarkAqua,
127        ChatFormatting::DarkRed,
128        ChatFormatting::DarkPurple,
129        ChatFormatting::Gold,
130        ChatFormatting::Gray,
131        ChatFormatting::DarkGray,
132        ChatFormatting::Blue,
133        ChatFormatting::Green,
134        ChatFormatting::Aqua,
135        ChatFormatting::Red,
136        ChatFormatting::LightPurple,
137        ChatFormatting::Yellow,
138        ChatFormatting::White,
139        ChatFormatting::Obfuscated,
140        ChatFormatting::Strikethrough,
141        ChatFormatting::Bold,
142        ChatFormatting::Underline,
143        ChatFormatting::Italic,
144        ChatFormatting::Reset,
145    ];
146
147    pub fn name(&self) -> &'static str {
148        match self {
149            ChatFormatting::Black => "BLACK",
150            ChatFormatting::DarkBlue => "DARK_BLUE",
151            ChatFormatting::DarkGreen => "DARK_GREEN",
152            ChatFormatting::DarkAqua => "DARK_AQUA",
153            ChatFormatting::DarkRed => "DARK_RED",
154            ChatFormatting::DarkPurple => "DARK_PURPLE",
155            ChatFormatting::Gold => "GOLD",
156            ChatFormatting::Gray => "GRAY",
157            ChatFormatting::DarkGray => "DARK_GRAY",
158            ChatFormatting::Blue => "BLUE",
159            ChatFormatting::Green => "GREEN",
160            ChatFormatting::Aqua => "AQUA",
161            ChatFormatting::Red => "RED",
162            ChatFormatting::LightPurple => "LIGHT_PURPLE",
163            ChatFormatting::Yellow => "YELLOW",
164            ChatFormatting::White => "WHITE",
165            ChatFormatting::Obfuscated => "OBFUSCATED",
166            ChatFormatting::Strikethrough => "STRIKETHROUGH",
167            ChatFormatting::Bold => "BOLD",
168            ChatFormatting::Underline => "UNDERLINE",
169            ChatFormatting::Italic => "ITALIC",
170            ChatFormatting::Reset => "RESET",
171        }
172    }
173
174    pub fn code(&self) -> char {
175        match self {
176            ChatFormatting::Black => '0',
177            ChatFormatting::DarkBlue => '1',
178            ChatFormatting::DarkGreen => '2',
179            ChatFormatting::DarkAqua => '3',
180            ChatFormatting::DarkRed => '4',
181            ChatFormatting::DarkPurple => '5',
182            ChatFormatting::Gold => '6',
183            ChatFormatting::Gray => '7',
184            ChatFormatting::DarkGray => '8',
185            ChatFormatting::Blue => '9',
186            ChatFormatting::Green => 'a',
187            ChatFormatting::Aqua => 'b',
188            ChatFormatting::Red => 'c',
189            ChatFormatting::LightPurple => 'd',
190            ChatFormatting::Yellow => 'e',
191            ChatFormatting::White => 'f',
192            ChatFormatting::Obfuscated => 'k',
193            ChatFormatting::Strikethrough => 'm',
194            ChatFormatting::Bold => 'l',
195            ChatFormatting::Underline => 'n',
196            ChatFormatting::Italic => 'o',
197            ChatFormatting::Reset => 'r',
198        }
199    }
200
201    pub fn from_code(code: char) -> Option<ChatFormatting> {
202        match code {
203            '0' => Some(ChatFormatting::Black),
204            '1' => Some(ChatFormatting::DarkBlue),
205            '2' => Some(ChatFormatting::DarkGreen),
206            '3' => Some(ChatFormatting::DarkAqua),
207            '4' => Some(ChatFormatting::DarkRed),
208            '5' => Some(ChatFormatting::DarkPurple),
209            '6' => Some(ChatFormatting::Gold),
210            '7' => Some(ChatFormatting::Gray),
211            '8' => Some(ChatFormatting::DarkGray),
212            '9' => Some(ChatFormatting::Blue),
213            'a' => Some(ChatFormatting::Green),
214            'b' => Some(ChatFormatting::Aqua),
215            'c' => Some(ChatFormatting::Red),
216            'd' => Some(ChatFormatting::LightPurple),
217            'e' => Some(ChatFormatting::Yellow),
218            'f' => Some(ChatFormatting::White),
219            'k' => Some(ChatFormatting::Obfuscated),
220            'm' => Some(ChatFormatting::Strikethrough),
221            'l' => Some(ChatFormatting::Bold),
222            'n' => Some(ChatFormatting::Underline),
223            'o' => Some(ChatFormatting::Italic),
224            'r' => Some(ChatFormatting::Reset),
225            _ => None,
226        }
227    }
228
229    pub fn is_format(&self) -> bool {
230        matches!(
231            self,
232            ChatFormatting::Obfuscated
233                | ChatFormatting::Strikethrough
234                | ChatFormatting::Bold
235                | ChatFormatting::Underline
236                | ChatFormatting::Italic
237                | ChatFormatting::Reset
238        )
239    }
240
241    pub fn color(&self) -> Option<u32> {
242        match self {
243            ChatFormatting::Black => Some(0),
244            ChatFormatting::DarkBlue => Some(170),
245            ChatFormatting::DarkGreen => Some(43520),
246            ChatFormatting::DarkAqua => Some(43690),
247            ChatFormatting::DarkRed => Some(1114112),
248            ChatFormatting::DarkPurple => Some(11141290),
249            ChatFormatting::Gold => Some(16755200),
250            ChatFormatting::Gray => Some(11184810),
251            ChatFormatting::DarkGray => Some(5592405),
252            ChatFormatting::Blue => Some(5592575),
253            ChatFormatting::Green => Some(5635925),
254            ChatFormatting::Aqua => Some(5636095),
255            ChatFormatting::Red => Some(16733525),
256            ChatFormatting::LightPurple => Some(16733695),
257            ChatFormatting::Yellow => Some(16777045),
258            ChatFormatting::White => Some(16777215),
259            _ => None,
260        }
261    }
262}
263
264impl TextColor {
265    fn new(value: u32, name: Option<String>) -> Self {
266        Self { value, name }
267    }
268
269    fn serialize(&self) -> String {
270        if let Some(name) = &self.name {
271            name.clone()
272        } else {
273            self.format_value()
274        }
275    }
276
277    pub fn format_value(&self) -> String {
278        format!("#{:06X}", self.value)
279    }
280}
281
282impl fmt::Display for TextColor {
283    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
284        write!(f, "{}", self.serialize())
285    }
286}
287
288// from ChatFormatting to TextColor
289impl TryFrom<ChatFormatting> for TextColor {
290    type Error = String;
291
292    fn try_from(formatter: ChatFormatting) -> Result<Self, Self::Error> {
293        if formatter.is_format() {
294            return Err(format!("{} is not a color", formatter.name()));
295        }
296        let color = formatter.color().unwrap_or(0);
297        Ok(Self::new(color, Some(formatter.name().to_string())))
298    }
299}
300
301#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
302pub struct Style {
303    // These are options instead of just bools because None is different than false in this case
304    pub color: Option<TextColor>,
305    pub bold: Option<bool>,
306    pub italic: Option<bool>,
307    pub underlined: Option<bool>,
308    pub strikethrough: Option<bool>,
309    pub obfuscated: Option<bool>,
310    /// Whether formatting should be reset before applying these styles
311    pub reset: bool,
312}
313
314fn serde_serialize_field<S: serde::ser::SerializeStruct>(
315    state: &mut S,
316    name: &'static str,
317    value: &Option<impl serde::Serialize>,
318    default: &(impl serde::Serialize + ?Sized),
319    reset: bool,
320) -> Result<(), S::Error> {
321    if let Some(value) = value {
322        state.serialize_field(name, value)?;
323    } else if reset {
324        state.serialize_field(name, default)?;
325    }
326    Ok(())
327}
328
329#[cfg(feature = "simdnbt")]
330fn simdnbt_serialize_field(
331    compound: &mut simdnbt::owned::NbtCompound,
332    name: &'static str,
333    value: Option<impl simdnbt::ToNbtTag>,
334    default: impl simdnbt::ToNbtTag,
335    reset: bool,
336) {
337    if let Some(value) = value {
338        compound.insert(name, value);
339    } else if reset {
340        compound.insert(name, default);
341    }
342}
343
344impl Serialize for Style {
345    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
346    where
347        S: Serializer,
348    {
349        let len = if self.reset {
350            6
351        } else {
352            usize::from(self.color.is_some())
353                + usize::from(self.bold.is_some())
354                + usize::from(self.italic.is_some())
355                + usize::from(self.underlined.is_some())
356                + usize::from(self.strikethrough.is_some())
357                + usize::from(self.obfuscated.is_some())
358        };
359        let mut state = serializer.serialize_struct("Style", len)?;
360
361        serde_serialize_field(&mut state, "color", &self.color, "white", self.reset)?;
362        serde_serialize_field(&mut state, "bold", &self.bold, &false, self.reset)?;
363        serde_serialize_field(&mut state, "italic", &self.italic, &false, self.reset)?;
364        serde_serialize_field(
365            &mut state,
366            "underlined",
367            &self.underlined,
368            &false,
369            self.reset,
370        )?;
371        serde_serialize_field(
372            &mut state,
373            "strikethrough",
374            &self.strikethrough,
375            &false,
376            self.reset,
377        )?;
378        serde_serialize_field(
379            &mut state,
380            "obfuscated",
381            &self.obfuscated,
382            &false,
383            self.reset,
384        )?;
385
386        state.end()
387    }
388}
389
390#[cfg(feature = "simdnbt")]
391impl simdnbt::Serialize for Style {
392    fn to_compound(self) -> NbtCompound {
393        let mut compound = NbtCompound::new();
394
395        simdnbt_serialize_field(&mut compound, "color", self.color, "white", self.reset);
396        simdnbt_serialize_field(&mut compound, "bold", self.bold, false, self.reset);
397        simdnbt_serialize_field(&mut compound, "italic", self.italic, false, self.reset);
398        simdnbt_serialize_field(
399            &mut compound,
400            "underlined",
401            self.underlined,
402            false,
403            self.reset,
404        );
405        simdnbt_serialize_field(
406            &mut compound,
407            "strikethrough",
408            self.strikethrough,
409            false,
410            self.reset,
411        );
412        simdnbt_serialize_field(
413            &mut compound,
414            "obfuscated",
415            self.obfuscated,
416            false,
417            self.reset,
418        );
419
420        compound
421    }
422}
423
424impl Style {
425    pub fn empty() -> Self {
426        Self::default()
427    }
428
429    pub fn deserialize(json: &Value) -> Style {
430        let Some(json_object) = json.as_object() else {
431            return Style::default();
432        };
433        let bold = json_object.get("bold").and_then(|v| v.as_bool());
434        let italic = json_object.get("italic").and_then(|v| v.as_bool());
435        let underlined = json_object.get("underlined").and_then(|v| v.as_bool());
436        let strikethrough = json_object.get("strikethrough").and_then(|v| v.as_bool());
437        let obfuscated = json_object.get("obfuscated").and_then(|v| v.as_bool());
438        let color: Option<TextColor> = json_object
439            .get("color")
440            .and_then(|v| v.as_str())
441            .and_then(|v| TextColor::parse(v.to_string()));
442        Style {
443            color,
444            bold,
445            italic,
446            underlined,
447            strikethrough,
448            obfuscated,
449            ..Style::default()
450        }
451    }
452
453    /// Check if a style has no attributes set
454    pub fn is_empty(&self) -> bool {
455        self.color.is_none()
456            && self.bold.is_none()
457            && self.italic.is_none()
458            && self.underlined.is_none()
459            && self.strikethrough.is_none()
460            && self.obfuscated.is_none()
461    }
462
463    /// find the necessary ansi code to get from this style to another
464    pub fn compare_ansi(&self, after: &Style, default_style: &Style) -> String {
465        let should_reset = after.reset ||
466            // if it used to be bold and now it's not, reset
467            (self.bold.unwrap_or(false) && !after.bold.unwrap_or(true)) ||
468            // if it used to be italic and now it's not, reset
469            (self.italic.unwrap_or(false) && !after.italic.unwrap_or(true)) ||
470            // if it used to be underlined and now it's not, reset
471            (self.underlined.unwrap_or(false) && !after.underlined.unwrap_or(true)) ||
472            // if it used to be strikethrough and now it's not, reset
473            (self.strikethrough.unwrap_or(false) && !after.strikethrough.unwrap_or(true)) ||
474            // if it used to be obfuscated and now it's not, reset
475            (self.obfuscated.unwrap_or(false) && !after.obfuscated.unwrap_or(true));
476
477        let mut ansi_codes = String::new();
478
479        let empty_style = Style::empty();
480
481        let (before, after) = if should_reset {
482            ansi_codes.push_str(Ansi::RESET);
483            let mut updated_after = if after.reset {
484                default_style.clone()
485            } else {
486                self.clone()
487            };
488            updated_after.apply(after);
489            (&empty_style, updated_after)
490        } else {
491            (self, after.clone())
492        };
493
494        // if bold used to be false/default and now it's true, set bold
495        if !before.bold.unwrap_or(false) && after.bold.unwrap_or(false) {
496            ansi_codes.push_str(Ansi::BOLD);
497        }
498        // if italic used to be false/default and now it's true, set italic
499        if !before.italic.unwrap_or(false) && after.italic.unwrap_or(false) {
500            ansi_codes.push_str(Ansi::ITALIC);
501        }
502        // if underlined used to be false/default and now it's true, set underlined
503        if !before.underlined.unwrap_or(false) && after.underlined.unwrap_or(false) {
504            ansi_codes.push_str(Ansi::UNDERLINED);
505        }
506        // if strikethrough used to be false/default and now it's true, set
507        // strikethrough
508        if !before.strikethrough.unwrap_or(false) && after.strikethrough.unwrap_or(false) {
509            ansi_codes.push_str(Ansi::STRIKETHROUGH);
510        }
511        // if obfuscated used to be false/default and now it's true, set obfuscated
512        if !before.obfuscated.unwrap_or(false) && after.obfuscated.unwrap_or(false) {
513            ansi_codes.push_str(Ansi::OBFUSCATED);
514        }
515
516        // if the new color is different and not none, set color
517        let color_changed = {
518            if before.color.is_none() && after.color.is_some() {
519                true
520            } else if before.color.is_some() && after.color.is_some() {
521                before.color.clone().unwrap().value != after.color.as_ref().unwrap().value
522            } else {
523                false
524            }
525        };
526
527        if color_changed {
528            let after_color = after.color.as_ref().unwrap();
529            ansi_codes.push_str(&Ansi::rgb(after_color.value));
530        }
531
532        ansi_codes
533    }
534
535    /// Apply another style to this one
536    pub fn apply(&mut self, style: &Style) {
537        if let Some(color) = &style.color {
538            self.color = Some(color.clone());
539        }
540        if let Some(bold) = &style.bold {
541            self.bold = Some(*bold);
542        }
543        if let Some(italic) = &style.italic {
544            self.italic = Some(*italic);
545        }
546        if let Some(underlined) = &style.underlined {
547            self.underlined = Some(*underlined);
548        }
549        if let Some(strikethrough) = &style.strikethrough {
550            self.strikethrough = Some(*strikethrough);
551        }
552        if let Some(obfuscated) = &style.obfuscated {
553            self.obfuscated = Some(*obfuscated);
554        }
555    }
556
557    /// Apply a ChatFormatting to this style
558    pub fn apply_formatting(&mut self, formatting: &ChatFormatting) {
559        match *formatting {
560            ChatFormatting::Bold => self.bold = Some(true),
561            ChatFormatting::Italic => self.italic = Some(true),
562            ChatFormatting::Underline => self.underlined = Some(true),
563            ChatFormatting::Strikethrough => self.strikethrough = Some(true),
564            ChatFormatting::Obfuscated => self.obfuscated = Some(true),
565            ChatFormatting::Reset => self.reset = true,
566            formatter => {
567                // if it's a color, set it
568                if let Some(color) = formatter.color() {
569                    self.color = Some(TextColor::from_rgb(color));
570                }
571            }
572        }
573    }
574}
575
576#[cfg(feature = "simdnbt")]
577impl simdnbt::Deserialize for Style {
578    fn from_compound(
579        compound: simdnbt::borrow::NbtCompound,
580    ) -> Result<Self, simdnbt::DeserializeError> {
581        let bold = compound.byte("bold").map(|v| v != 0);
582        let italic = compound.byte("italic").map(|v| v != 0);
583        let underlined = compound.byte("underlined").map(|v| v != 0);
584        let strikethrough = compound.byte("strikethrough").map(|v| v != 0);
585        let obfuscated = compound.byte("obfuscated").map(|v| v != 0);
586        let color: Option<TextColor> = compound
587            .string("color")
588            .and_then(|v| TextColor::parse(v.to_string()));
589        Ok(Style {
590            color,
591            bold,
592            italic,
593            underlined,
594            strikethrough,
595            obfuscated,
596            ..Style::default()
597        })
598    }
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604    use crate::component::DEFAULT_STYLE;
605
606    #[test]
607    fn text_color_named_colors() {
608        assert_eq!(TextColor::parse("red".to_string()).unwrap().value, 16733525);
609    }
610    #[test]
611    fn text_color_hex_colors() {
612        assert_eq!(
613            TextColor::parse("#a1b2c3".to_string()).unwrap().value,
614            10597059
615        );
616    }
617
618    #[test]
619    fn ansi_difference_should_reset() {
620        let style_a = Style {
621            bold: Some(true),
622            italic: Some(true),
623            ..Style::default()
624        };
625        let style_b = Style {
626            bold: Some(false),
627            ..Style::default()
628        };
629        let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
630        assert_eq!(
631            ansi_difference,
632            format!(
633                "{reset}{italic}",
634                reset = Ansi::RESET,
635                italic = Ansi::ITALIC
636            )
637        )
638    }
639    #[test]
640    fn ansi_difference_shouldnt_reset() {
641        let style_a = Style {
642            bold: Some(true),
643            ..Style::default()
644        };
645        let style_b = Style {
646            italic: Some(true),
647            ..Style::default()
648        };
649        let ansi_difference = style_a.compare_ansi(&style_b, &Style::default());
650        assert_eq!(ansi_difference, Ansi::ITALIC)
651    }
652
653    #[test]
654    fn ansi_difference_explicit_reset() {
655        let style_a = Style {
656            bold: Some(true),
657            ..Style::empty()
658        };
659        let style_b = Style {
660            italic: Some(true),
661            reset: true,
662            ..Style::empty()
663        };
664        let ansi_difference = style_a.compare_ansi(&style_b, &DEFAULT_STYLE);
665        assert_eq!(
666            ansi_difference,
667            format!(
668                "{reset}{italic}{white}",
669                reset = Ansi::RESET,
670                white = Ansi::rgb(ChatFormatting::White.color().unwrap()),
671                italic = Ansi::ITALIC
672            )
673        )
674    }
675
676    #[test]
677    fn test_from_code() {
678        assert_eq!(
679            ChatFormatting::from_code('a').unwrap(),
680            ChatFormatting::Green
681        );
682    }
683
684    #[test]
685    fn test_apply_formatting() {
686        let mut style = Style::default();
687        style.apply_formatting(&ChatFormatting::Bold);
688        style.apply_formatting(&ChatFormatting::Red);
689        assert_eq!(style.color, Some(TextColor::from_rgb(16733525)));
690    }
691}