Skip to main content

tui_lipan/widgets/big_text/
mod.rs

1use figlet_rs::FIGlet;
2use font8x8::{BASIC_FONTS, UnicodeFonts};
3use std::sync::{Arc, OnceLock};
4
5use crate::core::element::{Element, ElementKind};
6use crate::style::{RichText, Span, Style};
7use crate::utils::gradient::{ColorGradient, GradientDirection};
8
9pub(crate) use node::BigTextCacheKey;
10
11mod font;
12mod layout;
13mod node;
14mod reconcile;
15
16pub use layout::{GlyphLayout, measure_big_text};
17pub use node::BigTextNode;
18pub use reconcile::reconcile_big_text;
19
20use self::font::{
21    ANSI_SHADOW_FONT, BLOODY_FONT, COLOSSAL_FONT, DOS_REBEL_FONT, NANCYJ_FONT, POISON_FONT,
22    ROMAN_FONT, SLANT_FONT, SMALL_FONT, SMALL_POISON_FONT, STANDARD_FONT, SUB_ZERO_FONT,
23};
24
25// Cached parsed FIGlet objects to avoid expensive parsing on every render.
26// Wrapped in Option so that load failures are cached too (no repeated retries).
27static FIGFONT_STANDARD: OnceLock<Option<FIGlet>> = OnceLock::new();
28static FIGFONT_SLANT: OnceLock<Option<FIGlet>> = OnceLock::new();
29static FIGFONT_BLOODY: OnceLock<Option<FIGlet>> = OnceLock::new();
30static FIGFONT_COLOSSAL: OnceLock<Option<FIGlet>> = OnceLock::new();
31static FIGFONT_ROMAN: OnceLock<Option<FIGlet>> = OnceLock::new();
32static FIGFONT_SUB_ZERO: OnceLock<Option<FIGlet>> = OnceLock::new();
33static FIGFONT_POISON: OnceLock<Option<FIGlet>> = OnceLock::new();
34static FIGFONT_NANCYJ: OnceLock<Option<FIGlet>> = OnceLock::new();
35static FIGFONT_SMALL_POISON: OnceLock<Option<FIGlet>> = OnceLock::new();
36static FIGFONT_DOS_REBEL: OnceLock<Option<FIGlet>> = OnceLock::new();
37static FIGFONT_ANSI_SHADOW: OnceLock<Option<FIGlet>> = OnceLock::new();
38static FIGFONT_SMALL: OnceLock<Option<FIGlet>> = OnceLock::new();
39
40// Cached custom FIGlet fonts to avoid repeated parsing.
41static CUSTOM_FIGFONT_CACHE: OnceLock<std::sync::Mutex<CustomFigletCache>> = OnceLock::new();
42
43/// Font style for BigText.
44#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub enum BigFont {
46    /// Standard FIGlet font.
47    #[default]
48    Standard,
49    /// 8-bit Pixel Art font (using half-blocks).
50    /// Renders text using `▀`, `▄`, `█` to simulate high-resolution pixels.
51    Pixel,
52    /// Blocky 8-bit font (algorithmic bold).
53    PixelBold,
54    /// High-resolution Quadrant font (2x2 blocks).
55    Quadrant,
56    /// Slant FIGlet font - italic style.
57    Slant,
58    /// Bloody FIGlet font - horror style with dripping effect.
59    Bloody,
60    /// Colossal FIGlet font - very large block letters.
61    Colossal,
62    /// Roman FIGlet font - classic roman style.
63    Roman,
64    /// Sub-Zero FIGlet font - clean geometric style.
65    SubZero,
66    /// Poison FIGlet font - stylized dripping text.
67    Poison,
68    /// Nancyj FIGlet font - decorative style.
69    Nancyj,
70    /// Small Poison FIGlet font - compact poison style.
71    SmallPoison,
72    /// DOS Rebel FIGlet font - retro DOS style.
73    DosRebel,
74    /// ANSI Shadow FIGlet font - shadow effect using box-drawing characters.
75    AnsiShadow,
76    /// Small FIGlet font - compact version of Standard.
77    Small,
78    /// Custom FIGlet font loaded from an `.flf` file.
79    CustomFiglet,
80}
81
82/// Shadow configuration.
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub struct Shadow {
85    /// Style of the shadow characters.
86    pub style: Style,
87    /// Horizontal offset.
88    pub offset_x: i16,
89    /// Vertical offset.
90    pub offset_y: i16,
91}
92
93/// A widget that renders text using ASCII art.
94#[derive(Clone)]
95pub struct BigText {
96    /// The text to render.
97    pub text: RichText,
98    /// The font to use.
99    pub font: BigFont,
100    /// Style of the main text.
101    pub style: Style,
102    /// Optional shadow configuration.
103    pub shadow: Option<Shadow>,
104    /// Optional custom FIGlet font content.
105    pub custom_figlet: Option<Arc<str>>,
106    /// Optional color gradient applied at render time (not cached).
107    pub gradient: Option<(ColorGradient, GradientDirection)>,
108}
109
110// Cached FIGlet rendering results to avoid redundant expensive computations.
111static BIG_TEXT_CACHE: OnceLock<std::sync::Mutex<BigTextVisualCache>> = OnceLock::new();
112
113#[derive(Clone, Debug)]
114pub(crate) struct BigTextVisualCache {
115    entries: Vec<(BigTextCacheKey, Arc<BigTextRenderOutput>)>,
116}
117
118struct CustomFigletCache {
119    entries: Vec<(Arc<str>, Arc<FIGlet>)>,
120}
121
122impl CustomFigletCache {
123    fn new() -> Self {
124        Self {
125            entries: Vec::new(),
126        }
127    }
128
129    fn get(&self, content: &Arc<str>) -> Option<Arc<FIGlet>> {
130        self.entries
131            .iter()
132            .find(|(k, _)| k.as_ref() == content.as_ref())
133            .map(|(_, v)| Arc::clone(v))
134    }
135
136    fn insert(&mut self, content: Arc<str>, font: Arc<FIGlet>) {
137        if let Some(idx) = self
138            .entries
139            .iter()
140            .position(|(k, _)| k.as_ref() == content.as_ref())
141        {
142            self.entries.remove(idx);
143        }
144        self.entries.push((content, font));
145        if self.entries.len() > 32 {
146            self.entries.remove(0);
147        }
148    }
149}
150
151impl BigTextVisualCache {
152    fn new() -> Self {
153        Self {
154            entries: Vec::new(),
155        }
156    }
157
158    fn get(&self, key: &BigTextCacheKey) -> Option<Arc<BigTextRenderOutput>> {
159        self.entries
160            .iter()
161            .find(|(k, _)| k == key)
162            .map(|(_, v)| Arc::clone(v))
163    }
164
165    fn insert(&mut self, key: BigTextCacheKey, value: Arc<BigTextRenderOutput>) {
166        if let Some(idx) = self.entries.iter().position(|(k, _)| k == &key) {
167            self.entries.remove(idx);
168        }
169        self.entries.push((key, value));
170        if self.entries.len() > 100 {
171            self.entries.remove(0);
172        }
173    }
174}
175
176#[derive(Debug)]
177pub(crate) struct BigTextRenderOutput {
178    pub lines: Vec<Vec<Span>>,
179    pub width: u16,
180    pub height: u16,
181}
182
183impl Default for BigText {
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189impl BigText {
190    /// Create a new BigText widget with default settings.
191    pub fn new() -> Self {
192        Self {
193            text: RichText::new(),
194            font: BigFont::Standard,
195            style: Style::default(),
196            shadow: None,
197            custom_figlet: None,
198            gradient: None,
199        }
200    }
201
202    /// Set the text content.
203    pub fn text(mut self, text: impl Into<RichText>) -> Self {
204        self.text = text.into();
205        self
206    }
207
208    /// Set the font.
209    pub fn font(mut self, font: BigFont) -> Self {
210        self.font = font;
211        self
212    }
213
214    /// Set the text style.
215    pub fn style(mut self, style: Style) -> Self {
216        self.style = style;
217        self
218    }
219
220    /// Set the shadow.
221    pub fn shadow(mut self, shadow: impl Into<Option<Shadow>>) -> Self {
222        self.shadow = shadow.into();
223        self
224    }
225
226    /// Set a custom FIGlet font from `.flf` content.
227    pub fn custom_figlet(mut self, content: impl Into<Arc<str>>) -> Self {
228        self.font = BigFont::CustomFiglet;
229        self.custom_figlet = Some(content.into());
230        self
231    }
232
233    /// Load a custom FIGlet font from a file path.
234    pub fn custom_figlet_from_file(self, path: impl AsRef<std::path::Path>) -> crate::Result<Self> {
235        let content = std::fs::read_to_string(path)?;
236        Ok(self.custom_figlet(content))
237    }
238
239    /// Helper to set simple shadow with offset (1, 1).
240    pub fn with_shadow(mut self, shadow_style: Style) -> Self {
241        self.shadow = Some(Shadow {
242            style: shadow_style,
243            offset_x: 1,
244            offset_y: 1,
245        });
246        self
247    }
248
249    /// Apply a color gradient over the rendered output.
250    ///
251    /// The gradient is a render-time effect and does not affect the glyph cache.
252    /// Use [`GradientDirection::Vertical`] for a top-to-bottom color wash across
253    /// the font rows, or [`GradientDirection::Horizontal`] for a left-to-right
254    /// wash across character columns.
255    pub fn gradient(mut self, gradient: ColorGradient, direction: GradientDirection) -> Self {
256        self.gradient = Some((gradient, direction));
257        self
258    }
259
260    pub(crate) fn build_lines(&self) -> Arc<BigTextRenderOutput> {
261        let cache_key = BigTextCacheKey::new(
262            &self.text,
263            self.font,
264            self.style,
265            self.shadow,
266            self.custom_figlet.as_ref(),
267        );
268
269        let cache_mutex =
270            BIG_TEXT_CACHE.get_or_init(|| std::sync::Mutex::new(BigTextVisualCache::new()));
271        if let Ok(cache) = cache_mutex.lock()
272            && let Some(cached) = cache.get(&cache_key)
273        {
274            return cached;
275        }
276
277        struct Segment {
278            lines: Vec<String>,
279            style: Style,
280            width: usize,
281        }
282
283        let mut segments = Vec::new();
284        for span in &self.text.spans {
285            if span.content.is_empty() {
286                continue;
287            }
288
289            let lines = self.render_text(span.content.as_ref());
290            if lines.is_empty() {
291                continue;
292            }
293
294            let width = lines.iter().map(|l| l.chars().count()).max().unwrap_or(0);
295            if width == 0 {
296                continue;
297            }
298
299            let style = self.style.patch(span.style);
300            segments.push(Segment {
301                lines,
302                style,
303                width,
304            });
305        }
306
307        if segments.is_empty() {
308            let output = Arc::new(BigTextRenderOutput {
309                lines: Vec::new(),
310                width: 0,
311                height: 0,
312            });
313            if let Ok(mut cache) = cache_mutex.lock() {
314                cache.insert(cache_key, output.clone());
315            }
316            return output;
317        }
318
319        let raw_height = segments
320            .iter()
321            .map(|segment| segment.lines.len())
322            .max()
323            .unwrap_or(0);
324
325        if raw_height == 0 {
326            let output = Arc::new(BigTextRenderOutput {
327                lines: Vec::new(),
328                width: 0,
329                height: 0,
330            });
331            if let Ok(mut cache) = cache_mutex.lock() {
332                cache.insert(cache_key, output.clone());
333            }
334            return output;
335        }
336
337        let mut raw_grid: Vec<Vec<(char, Style)>> = vec![Vec::new(); raw_height];
338
339        for segment in segments {
340            for (row_idx, row) in raw_grid.iter_mut().enumerate().take(raw_height) {
341                let line = segment.lines.get(row_idx).map(|s| s.as_str()).unwrap_or("");
342                let mut line_len = 0usize;
343
344                for c in line.chars() {
345                    let style = if c == ' ' {
346                        Style::default()
347                    } else {
348                        segment.style
349                    };
350                    row.push((c, style));
351                    line_len += 1;
352                }
353
354                if line_len < segment.width {
355                    row.extend(std::iter::repeat_n(
356                        (' ', Style::default()),
357                        segment.width - line_len,
358                    ));
359                }
360            }
361        }
362
363        let raw_width = raw_grid.first().map(|row| row.len()).unwrap_or(0);
364        if raw_width == 0 {
365            let output = Arc::new(BigTextRenderOutput {
366                lines: Vec::new(),
367                width: 0,
368                height: 0,
369            });
370            if let Ok(mut cache) = cache_mutex.lock() {
371                cache.insert(cache_key, output.clone());
372            }
373            return output;
374        }
375
376        let shadow_cfg = self.shadow;
377
378        let (final_w, final_h, offset_x, offset_y) = if let Some(s) = shadow_cfg {
379            let min_x = 0.min(s.offset_x);
380            let min_y = 0.min(s.offset_y);
381            let max_x = (raw_width as i16).max(raw_width as i16 + s.offset_x);
382            let max_y = (raw_height as i16).max(raw_height as i16 + s.offset_y);
383
384            (
385                (max_x - min_x) as usize,
386                (max_y - min_y) as usize,
387                -min_x,
388                -min_y,
389            )
390        } else {
391            (raw_width, raw_height, 0, 0)
392        };
393
394        let mut grid: Vec<Vec<(char, Style)>> =
395            vec![vec![(' ', Style::default()); final_w]; final_h];
396
397        let put_char =
398            |x: i16, y: i16, c: char, style: Style, grid: &mut Vec<Vec<(char, Style)>>| {
399                let gx = x + offset_x;
400                let gy = y + offset_y;
401                if gx >= 0 && gy >= 0 && (gx as usize) < final_w && (gy as usize) < final_h {
402                    grid[gy as usize][gx as usize] = (c, style);
403                }
404            };
405
406        if let Some(s) = shadow_cfg {
407            for (y, row) in raw_grid.iter().enumerate().take(raw_height) {
408                for (x, (c, _)) in row.iter().enumerate().take(raw_width) {
409                    if *c != ' ' {
410                        put_char(
411                            x as i16 + s.offset_x,
412                            y as i16 + s.offset_y,
413                            *c,
414                            s.style,
415                            &mut grid,
416                        );
417                    }
418                }
419            }
420        }
421
422        for (y, row) in raw_grid.iter().enumerate().take(raw_height) {
423            for (x, (c, style)) in row.iter().enumerate().take(raw_width) {
424                if *c != ' ' {
425                    put_char(x as i16, y as i16, *c, *style, &mut grid);
426                }
427            }
428        }
429
430        let mut min_y = 0;
431        let mut max_y = final_h.saturating_sub(1);
432
433        for (y, row) in grid.iter().enumerate().take(final_h) {
434            let row_is_empty = row.iter().all(|(c, _)| *c == ' ');
435            if !row_is_empty {
436                min_y = y;
437                break;
438            }
439        }
440
441        for (y, row) in grid.iter().enumerate().take(final_h).rev() {
442            let row_is_empty = row.iter().all(|(c, _)| *c == ' ');
443            if !row_is_empty {
444                max_y = y;
445                break;
446            }
447        }
448
449        if min_y > max_y {
450            let output = Arc::new(BigTextRenderOutput {
451                lines: Vec::new(),
452                width: 0,
453                height: 0,
454            });
455            if let Ok(mut cache) = cache_mutex.lock() {
456                cache.insert(cache_key, output.clone());
457            }
458            return output;
459        }
460
461        let mut lines = Vec::new();
462        for row in grid.iter().take(max_y + 1).skip(min_y) {
463            let mut spans = Vec::new();
464            let mut current_span_str = String::new();
465            let mut current_style = if row.is_empty() {
466                Style::default()
467            } else {
468                row[0].1
469            };
470
471            for (c, style) in row {
472                if *style != current_style {
473                    if !current_span_str.is_empty() {
474                        spans.push(Span::new(current_span_str.clone()).style(current_style));
475                        current_span_str.clear();
476                    }
477                    current_style = *style;
478                }
479                current_span_str.push(*c);
480            }
481            if !current_span_str.is_empty() {
482                spans.push(Span::new(current_span_str).style(current_style));
483            }
484
485            lines.push(spans);
486        }
487
488        let height = lines.len().min(u16::MAX as usize) as u16;
489        let width = final_w.min(u16::MAX as usize) as u16;
490
491        let output = Arc::new(BigTextRenderOutput {
492            lines,
493            width,
494            height,
495        });
496
497        if let Ok(mut cache) = cache_mutex.lock() {
498            cache.insert(cache_key, output.clone());
499        }
500
501        output
502    }
503
504    fn render_text(&self, text: &str) -> Vec<String> {
505        match self.font {
506            BigFont::Pixel => self.render_pixel(text, false),
507            BigFont::PixelBold => self.render_pixel(text, true),
508            BigFont::Quadrant => self.render_quadrant(text),
509            _ => self.render_figlet(text),
510        }
511    }
512
513    fn render_figlet(&self, text: &str) -> Vec<String> {
514        if matches!(self.font, BigFont::CustomFiglet)
515            && let Some(font) = self.custom_figlet_font()
516        {
517            return if let Some(figure) = font.convert(text) {
518                figure.to_string().lines().map(|s| s.to_string()).collect()
519            } else {
520                vec![text.to_string()]
521            };
522        }
523
524        fn load_font<'a>(
525            slot: &'a OnceLock<Option<FIGlet>>,
526            font_data: &str,
527        ) -> Option<&'a FIGlet> {
528            slot.get_or_init(|| {
529                FIGlet::from_content(font_data)
530                    .or_else(|_| FIGlet::standard())
531                    .ok()
532            })
533            .as_ref()
534        }
535
536        // Get or initialize the cached font for this font type.
537        // If even the standard fallback fails, return plain text.
538        let font = match self.font {
539            BigFont::Standard => load_font(&FIGFONT_STANDARD, STANDARD_FONT),
540            BigFont::Slant => load_font(&FIGFONT_SLANT, SLANT_FONT),
541            BigFont::Bloody => load_font(&FIGFONT_BLOODY, BLOODY_FONT),
542            BigFont::Colossal => load_font(&FIGFONT_COLOSSAL, COLOSSAL_FONT),
543            BigFont::Roman => load_font(&FIGFONT_ROMAN, ROMAN_FONT),
544            BigFont::SubZero => load_font(&FIGFONT_SUB_ZERO, SUB_ZERO_FONT),
545            BigFont::Poison => load_font(&FIGFONT_POISON, POISON_FONT),
546            BigFont::Nancyj => load_font(&FIGFONT_NANCYJ, NANCYJ_FONT),
547            BigFont::SmallPoison => load_font(&FIGFONT_SMALL_POISON, SMALL_POISON_FONT),
548            BigFont::DosRebel => load_font(&FIGFONT_DOS_REBEL, DOS_REBEL_FONT),
549            BigFont::AnsiShadow => load_font(&FIGFONT_ANSI_SHADOW, ANSI_SHADOW_FONT),
550            BigFont::Small => load_font(&FIGFONT_SMALL, SMALL_FONT),
551            BigFont::CustomFiglet | BigFont::Pixel | BigFont::PixelBold | BigFont::Quadrant => {
552                load_font(&FIGFONT_STANDARD, STANDARD_FONT)
553            }
554        };
555
556        let Some(font) = font else {
557            return vec![text.to_string()];
558        };
559
560        if let Some(f) = font.convert(text) {
561            f.to_string()
562                .lines()
563                .map(|s: &str| s.to_string())
564                .collect::<Vec<_>>()
565        } else {
566            vec![text.to_string()]
567        }
568    }
569
570    fn custom_figlet_font(&self) -> Option<Arc<FIGlet>> {
571        let content = self.custom_figlet.as_ref()?;
572        let cache_mutex =
573            CUSTOM_FIGFONT_CACHE.get_or_init(|| std::sync::Mutex::new(CustomFigletCache::new()));
574        if let Ok(cache) = cache_mutex.lock()
575            && let Some(cached) = cache.get(content)
576        {
577            return Some(cached);
578        }
579
580        let parsed = FIGlet::from_content(content.as_ref()).ok()?;
581        let font = Arc::new(parsed);
582
583        if let Ok(mut cache) = cache_mutex.lock() {
584            cache.insert(content.clone(), font.clone());
585        }
586
587        Some(font)
588    }
589
590    fn render_pixel(&self, text: &str, bold: bool) -> Vec<String> {
591        let mut bitmap: Vec<Vec<bool>> = Vec::new();
592        let char_height = 8;
593
594        for _ in 0..char_height {
595            bitmap.push(Vec::new());
596        }
597
598        for c in text.chars() {
599            if let Some(glyph) = BASIC_FONTS.get(c) {
600                for (row_idx, byte) in glyph.iter().enumerate() {
601                    if row_idx >= char_height {
602                        break;
603                    }
604                    for bit in 0..8 {
605                        let is_set = (byte & (1 << bit)) != 0;
606                        bitmap[row_idx].push(is_set);
607                    }
608                }
609            } else {
610                for row in bitmap.iter_mut() {
611                    for _ in 0..8 {
612                        row.push(false);
613                    }
614                }
615            }
616        }
617
618        if bold {
619            for row in bitmap.iter_mut() {
620                let original = row.clone();
621                let mut new_row = Vec::with_capacity(original.len());
622                for (i, &pixel) in original.iter().enumerate() {
623                    let prev = if i > 0 { original[i - 1] } else { false };
624                    new_row.push(pixel | prev);
625                }
626                *row = new_row;
627            }
628        }
629
630        let mut lines = Vec::new();
631        for y in (0..char_height).step_by(2) {
632            let mut line = String::new();
633            if y + 1 >= char_height {
634                break;
635            }
636
637            let row_top = &bitmap[y];
638            let row_bottom = &bitmap[y + 1];
639
640            for x in 0..row_top.len() {
641                let top = row_top[x];
642                let bottom = row_bottom[x];
643
644                let char = match (top, bottom) {
645                    (true, true) => '█',
646                    (true, false) => '▀',
647                    (false, true) => '▄',
648                    (false, false) => ' ',
649                };
650                line.push(char);
651            }
652            lines.push(line);
653        }
654
655        lines
656    }
657
658    fn render_quadrant(&self, text: &str) -> Vec<String> {
659        let mut bitmap: Vec<Vec<bool>> = Vec::new();
660        let char_height = 8;
661
662        for _ in 0..char_height {
663            bitmap.push(Vec::new());
664        }
665
666        for c in text.chars() {
667            if let Some(glyph) = BASIC_FONTS.get(c) {
668                for (row_idx, byte) in glyph.iter().enumerate() {
669                    if row_idx >= char_height {
670                        break;
671                    }
672                    for bit in 0..8 {
673                        let is_set = (byte & (1 << bit)) != 0;
674                        bitmap[row_idx].push(is_set);
675                    }
676                }
677            } else {
678                for row in bitmap.iter_mut() {
679                    for _ in 0..8 {
680                        row.push(false);
681                    }
682                }
683            }
684        }
685
686        let mut lines = Vec::new();
687        for y in (0..char_height).step_by(2) {
688            let mut line = String::new();
689            if y + 1 >= char_height {
690                break;
691            }
692
693            let row_top = &bitmap[y];
694            let row_bottom = &bitmap[y + 1];
695
696            for x in (0..row_top.len()).step_by(2) {
697                if x + 1 >= row_top.len() {
698                    break;
699                }
700
701                let tl = row_top[x];
702                let tr = row_top[x + 1];
703                let bl = row_bottom[x];
704                let br = row_bottom[x + 1];
705
706                let char = match (tl, tr, bl, br) {
707                    (false, false, false, false) => ' ',
708                    (false, false, false, true) => '▗',
709                    (false, false, true, false) => '▖',
710                    (false, false, true, true) => '▄',
711                    (false, true, false, false) => '▝',
712                    (false, true, false, true) => '▐',
713                    (false, true, true, false) => '▞',
714                    (false, true, true, true) => '▟',
715                    (true, false, false, false) => '▘',
716                    (true, false, false, true) => '▚',
717                    (true, false, true, false) => '▌',
718                    (true, false, true, true) => '▙',
719                    (true, true, false, false) => '▀',
720                    (true, true, false, true) => '▜',
721                    (true, true, true, false) => '▛',
722                    (true, true, true, true) => '█',
723                };
724                line.push(char);
725            }
726            lines.push(line);
727        }
728
729        lines
730    }
731}
732
733impl From<BigText> for Element {
734    fn from(val: BigText) -> Self {
735        Element::new(ElementKind::BigText(val))
736    }
737}
738
739impl crate::layout::hash::LayoutHash for BigText {
740    fn layout_hash(
741        &self,
742        hasher: &mut impl std::hash::Hasher,
743        _recurse: &dyn Fn(&crate::core::element::Element) -> Option<u64>,
744    ) -> Option<()> {
745        use std::hash::Hash;
746        self.font.hash(hasher);
747        crate::layout::hash::hash_spans_content(&self.text.spans, hasher);
748        self.shadow.hash(hasher);
749        self.custom_figlet.hash(hasher);
750        Some(())
751    }
752}