Skip to main content

cranpose_ui/text/
annotated_string.rs

1use std::ops::Range;
2
3use crate::{ParagraphStyle, SpanStyle};
4
5/// The basic data structure of text with multiple styles.
6///
7/// To construct an `AnnotatedString` you can use `AnnotatedString::builder()`.
8#[derive(Debug, Clone, PartialEq, Default)]
9pub struct AnnotatedString {
10    pub text: String,
11    pub span_styles: Vec<RangeStyle<SpanStyle>>,
12    pub paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
13}
14
15/// A style applied to a range of an `AnnotatedString`.
16#[derive(Debug, Clone, PartialEq)]
17pub struct RangeStyle<T> {
18    pub item: T,
19    pub range: Range<usize>,
20}
21
22impl AnnotatedString {
23    pub fn new(text: String) -> Self {
24        Self {
25            text,
26            span_styles: vec![],
27            paragraph_styles: vec![],
28        }
29    }
30
31    pub fn builder() -> Builder {
32        Builder::new()
33    }
34
35    pub fn len(&self) -> usize {
36        self.text.len()
37    }
38
39    pub fn is_empty(&self) -> bool {
40        self.text.is_empty()
41    }
42
43    /// Returns a sorted list of unique byte indices where styles change.
44    pub fn span_boundaries(&self) -> Vec<usize> {
45        let mut boundaries = vec![0, self.text.len()];
46        for span in &self.span_styles {
47            boundaries.push(span.range.start);
48            boundaries.push(span.range.end);
49        }
50        boundaries.sort_unstable();
51        boundaries.dedup();
52        boundaries
53            .into_iter()
54            .filter(|&b| b <= self.text.len() && self.text.is_char_boundary(b))
55            .collect()
56    }
57
58    /// Computes a hash representing the contents of the span styles, suitable for cache invalidation.
59    pub fn span_styles_hash(&self) -> u64 {
60        use std::hash::{Hash, Hasher};
61        let mut hasher = std::collections::hash_map::DefaultHasher::new();
62        hasher.write_usize(self.span_styles.len());
63        for span in &self.span_styles {
64            hasher.write_usize(span.range.start);
65            hasher.write_usize(span.range.end);
66
67            // Hash measurement-affecting fields
68            let dummy = crate::text::TextStyle {
69                span_style: span.item.clone(),
70                ..Default::default()
71            };
72            hasher.write_u64(dummy.measurement_hash());
73
74            // Hash visually-affecting fields ignored by measurement
75            if let Some(c) = &span.item.color {
76                hasher.write_u32(c.0.to_bits());
77                hasher.write_u32(c.1.to_bits());
78                hasher.write_u32(c.2.to_bits());
79                hasher.write_u32(c.3.to_bits());
80            }
81            if let Some(bg) = &span.item.background {
82                hasher.write_u32(bg.0.to_bits());
83                hasher.write_u32(bg.1.to_bits());
84                hasher.write_u32(bg.2.to_bits());
85                hasher.write_u32(bg.3.to_bits());
86            }
87            if let Some(d) = &span.item.text_decoration {
88                d.hash(&mut hasher);
89            }
90        }
91        hasher.finish()
92    }
93
94    /// Returns a new `AnnotatedString` containing a substring of the original text
95    /// and any styles that overlap with the new range, with indices adjusted.
96    pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
97        if range.is_empty() {
98            return Self::new(String::new());
99        }
100
101        let start = range.start.min(self.text.len());
102        let end = range.end.max(start).min(self.text.len());
103
104        if start == end {
105            return Self::new(String::new());
106        }
107
108        let mut new_spans = Vec::new();
109        for span in &self.span_styles {
110            let intersection_start = span.range.start.max(start);
111            let intersection_end = span.range.end.min(end);
112            if intersection_start < intersection_end {
113                new_spans.push(RangeStyle {
114                    item: span.item.clone(),
115                    range: (intersection_start - start)..(intersection_end - start),
116                });
117            }
118        }
119
120        let mut new_paragraphs = Vec::new();
121        for span in &self.paragraph_styles {
122            let intersection_start = span.range.start.max(start);
123            let intersection_end = span.range.end.min(end);
124            if intersection_start < intersection_end {
125                new_paragraphs.push(RangeStyle {
126                    item: span.item.clone(),
127                    range: (intersection_start - start)..(intersection_end - start),
128                });
129            }
130        }
131
132        Self {
133            text: self.text[start..end].to_string(),
134            span_styles: new_spans,
135            paragraph_styles: new_paragraphs,
136        }
137    }
138}
139
140impl From<String> for AnnotatedString {
141    fn from(text: String) -> Self {
142        Self::new(text)
143    }
144}
145
146impl From<&str> for AnnotatedString {
147    fn from(text: &str) -> Self {
148        Self::new(text.to_owned())
149    }
150}
151
152impl From<&String> for AnnotatedString {
153    fn from(text: &String) -> Self {
154        Self::new(text.clone())
155    }
156}
157
158impl From<&mut String> for AnnotatedString {
159    fn from(text: &mut String) -> Self {
160        Self::new(text.clone())
161    }
162}
163
164/// A builder to construct `AnnotatedString`.
165#[derive(Debug, Default, Clone)]
166pub struct Builder {
167    text: String,
168    span_styles: Vec<MutableRange<SpanStyle>>,
169    paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
170    style_stack: Vec<StyleStackRecord>,
171}
172
173#[derive(Debug, Clone)]
174struct MutableRange<T> {
175    item: T,
176    start: usize,
177    end: usize,
178}
179
180#[derive(Debug, Clone)]
181struct StyleStackRecord {
182    style_type: StyleType,
183    index: usize,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187enum StyleType {
188    Span,
189    Paragraph,
190}
191
192impl Builder {
193    pub fn new() -> Self {
194        Self::default()
195    }
196
197    /// Appends the given String to this Builder.
198    pub fn append(mut self, text: &str) -> Self {
199        self.text.push_str(text);
200        self
201    }
202
203    /// Applies the given `SpanStyle` to any appended text until a corresponding `pop` is called.
204    ///
205    /// Returns the index of the pushed style, which can be passed to `pop_to` or used as an ID.
206    pub fn push_style(mut self, style: SpanStyle) -> Self {
207        let index = self.span_styles.len();
208        self.span_styles.push(MutableRange {
209            item: style,
210            start: self.text.len(),
211            end: usize::MAX,
212        });
213        self.style_stack.push(StyleStackRecord {
214            style_type: StyleType::Span,
215            index,
216        });
217        self
218    }
219
220    /// Applies the given `ParagraphStyle` to any appended text until a corresponding `pop` is called.
221    pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
222        let index = self.paragraph_styles.len();
223        self.paragraph_styles.push(MutableRange {
224            item: style,
225            start: self.text.len(),
226            end: usize::MAX,
227        });
228        self.style_stack.push(StyleStackRecord {
229            style_type: StyleType::Paragraph,
230            index,
231        });
232        self
233    }
234
235    /// Ends the style that was most recently pushed.
236    pub fn pop(mut self) -> Self {
237        if let Some(record) = self.style_stack.pop() {
238            match record.style_type {
239                StyleType::Span => {
240                    self.span_styles[record.index].end = self.text.len();
241                }
242                StyleType::Paragraph => {
243                    self.paragraph_styles[record.index].end = self.text.len();
244                }
245            }
246        }
247        self
248    }
249
250    /// Completes the builder, resolving open styles to the end of the text.
251    pub fn to_annotated_string(mut self) -> AnnotatedString {
252        // Resolve unclosed styles
253        while let Some(record) = self.style_stack.pop() {
254            match record.style_type {
255                StyleType::Span => {
256                    self.span_styles[record.index].end = self.text.len();
257                }
258                StyleType::Paragraph => {
259                    self.paragraph_styles[record.index].end = self.text.len();
260                }
261            }
262        }
263
264        AnnotatedString {
265            text: self.text,
266            span_styles: self
267                .span_styles
268                .into_iter()
269                .map(|s| RangeStyle {
270                    item: s.item,
271                    range: s.start..s.end,
272                })
273                .collect(),
274            paragraph_styles: self
275                .paragraph_styles
276                .into_iter()
277                .map(|s| RangeStyle {
278                    item: s.item,
279                    range: s.start..s.end,
280                })
281                .collect(),
282        }
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use super::*;
289
290    #[test]
291    fn test_builder_span() {
292        let span1 = SpanStyle {
293            alpha: Some(0.5),
294            ..Default::default()
295        };
296
297        let span2 = SpanStyle {
298            alpha: Some(1.0),
299            ..Default::default()
300        };
301
302        let annotated = AnnotatedString::builder()
303            .append("Hello ")
304            .push_style(span1.clone())
305            .append("World")
306            .push_style(span2.clone())
307            .append("!")
308            .pop()
309            .pop()
310            .to_annotated_string();
311
312        assert_eq!(annotated.text, "Hello World!");
313        assert_eq!(annotated.span_styles.len(), 2);
314        assert_eq!(annotated.span_styles[0].range, 6..12);
315        assert_eq!(annotated.span_styles[0].item, span1);
316        assert_eq!(annotated.span_styles[1].range, 11..12);
317        assert_eq!(annotated.span_styles[1].item, span2);
318    }
319}