Skip to main content

tui_lipan/widgets/text_area/
color.rs

1use std::any::Any;
2use std::hash::{Hash, Hasher};
3use std::rc::Rc;
4
5use crate::style::Span;
6
7/// Input data for text coloring strategies.
8#[derive(Clone, Copy, Debug)]
9pub struct TextAreaColorInput<'a> {
10    /// The full text content.
11    pub value: &'a str,
12    /// Optional language identifier (e.g., "rust", "rs").
13    pub language: Option<&'a str>,
14    /// Optional theme identifier.
15    pub theme: Option<&'a str>,
16}
17
18/// Per-line styled spans. Each entry corresponds to a logical line.
19pub type TextAreaColorLines = Vec<Vec<Span>>;
20
21/// Strategy for applying per-line styling to text content.
22pub trait TextAreaColorStrategy: Any {
23    /// Return styled spans per logical line.
24    fn highlight(&self, input: TextAreaColorInput<'_>) -> TextAreaColorLines;
25
26    /// Return a stable hash for this strategy's configuration.
27    fn cache_key(&self) -> u64 {
28        0
29    }
30
31    /// Downcast support for framework theme integration.
32    fn as_any(&self) -> &dyn Any;
33
34    /// Mutable downcast support for framework theme integration.
35    fn as_any_mut(&mut self) -> &mut dyn Any;
36}
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
39pub(crate) struct TextAreaColorKey {
40    pub value_hash: u64,
41    pub strategy_hash: u64,
42    pub language_hash: u64,
43    pub theme_hash: u64,
44}
45
46#[derive(Clone, Debug, Default)]
47pub(crate) struct TextAreaColorCache {
48    pub key: Option<TextAreaColorKey>,
49    pub lines: TextAreaColorLines,
50    pub line_starts: Vec<usize>,
51    pub line_lengths: Vec<usize>,
52}
53
54impl TextAreaColorCache {
55    pub(crate) fn update(
56        &mut self,
57        strategy: Option<&Rc<dyn TextAreaColorStrategy>>,
58        value: &str,
59        value_hash: u64,
60        language: Option<&str>,
61        theme: Option<&str>,
62    ) {
63        let Some(strategy) = strategy else {
64            self.key = None;
65            self.lines.clear();
66            return;
67        };
68
69        let key = TextAreaColorKey {
70            value_hash,
71            strategy_hash: strategy.cache_key(),
72            language_hash: hash_optional_str(language),
73            theme_hash: hash_optional_str(theme),
74        };
75
76        if self.key == Some(key) {
77            return;
78        }
79
80        let input = TextAreaColorInput {
81            value,
82            language,
83            theme,
84        };
85
86        let lines = normalize_color_lines(strategy.highlight(input), value);
87        self.lines = lines;
88        let (line_starts, line_lengths) = line_meta(value);
89        self.line_starts = line_starts;
90        self.line_lengths = line_lengths;
91        self.key = Some(key);
92    }
93}
94
95fn logical_lines(value: &str) -> Vec<&str> {
96    if value.is_empty() {
97        vec![""]
98    } else {
99        value.split('\n').collect()
100    }
101}
102
103fn normalize_color_lines(mut lines: TextAreaColorLines, value: &str) -> TextAreaColorLines {
104    let expected = logical_lines(value).len();
105    if lines.len() > expected {
106        lines.truncate(expected);
107    }
108    while lines.len() < expected {
109        lines.push(vec![Span::new("")]);
110    }
111    lines
112}
113
114fn line_meta(value: &str) -> (Vec<usize>, Vec<usize>) {
115    if value.is_empty() {
116        return (vec![0], vec![0]);
117    }
118
119    let mut starts = Vec::new();
120    let mut lengths = Vec::new();
121    let mut current = 0usize;
122    for line in value.split('\n') {
123        starts.push(current);
124        lengths.push(line.len());
125        current = current.saturating_add(line.len()).saturating_add(1);
126    }
127    (starts, lengths)
128}
129
130fn hash_optional_str(value: Option<&str>) -> u64 {
131    let mut hasher = rustc_hash::FxHasher::default();
132    match value {
133        Some(v) => {
134            1u8.hash(&mut hasher);
135            v.hash(&mut hasher);
136        }
137        None => {
138            0u8.hash(&mut hasher);
139        }
140    }
141    hasher.finish()
142}