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
25static 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
40static CUSTOM_FIGFONT_CACHE: OnceLock<std::sync::Mutex<CustomFigletCache>> = OnceLock::new();
42
43#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
45pub enum BigFont {
46 #[default]
48 Standard,
49 Pixel,
52 PixelBold,
54 Quadrant,
56 Slant,
58 Bloody,
60 Colossal,
62 Roman,
64 SubZero,
66 Poison,
68 Nancyj,
70 SmallPoison,
72 DosRebel,
74 AnsiShadow,
76 Small,
78 CustomFiglet,
80}
81
82#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
84pub struct Shadow {
85 pub style: Style,
87 pub offset_x: i16,
89 pub offset_y: i16,
91}
92
93#[derive(Clone)]
95pub struct BigText {
96 pub text: RichText,
98 pub font: BigFont,
100 pub style: Style,
102 pub shadow: Option<Shadow>,
104 pub custom_figlet: Option<Arc<str>>,
106 pub gradient: Option<(ColorGradient, GradientDirection)>,
108}
109
110static 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 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 pub fn text(mut self, text: impl Into<RichText>) -> Self {
204 self.text = text.into();
205 self
206 }
207
208 pub fn font(mut self, font: BigFont) -> Self {
210 self.font = font;
211 self
212 }
213
214 pub fn style(mut self, style: Style) -> Self {
216 self.style = style;
217 self
218 }
219
220 pub fn shadow(mut self, shadow: impl Into<Option<Shadow>>) -> Self {
222 self.shadow = shadow.into();
223 self
224 }
225
226 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 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 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 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 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}