1use std::ops::Range;
2use std::rc::Rc;
3
4use crate::{ParagraphStyle, SpanStyle};
5
6#[derive(Clone)]
27pub enum LinkAnnotation {
28 Url(String),
32
33 Clickable { tag: String, handler: Rc<dyn Fn()> },
37}
38
39impl std::fmt::Debug for LinkAnnotation {
40 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
41 match self {
42 Self::Url(url) => f.debug_tuple("Url").field(url).finish(),
43 Self::Clickable { tag, .. } => f.debug_struct("Clickable").field("tag", tag).finish(),
44 }
45 }
46}
47
48impl PartialEq for LinkAnnotation {
49 fn eq(&self, other: &Self) -> bool {
50 match (self, other) {
51 (Self::Url(a), Self::Url(b)) => a == b,
52 (
53 Self::Clickable {
54 tag: ta,
55 handler: ha,
56 },
57 Self::Clickable {
58 tag: tb,
59 handler: hb,
60 },
61 ) => ta == tb && Rc::ptr_eq(ha, hb),
62 _ => false,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq)]
72pub struct StringAnnotation {
73 pub tag: String,
74 pub annotation: String,
75}
76
77#[derive(Debug, Clone, PartialEq, Default)]
81pub struct AnnotatedString {
82 pub text: String,
83 pub span_styles: Vec<RangeStyle<SpanStyle>>,
84 pub paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
85 pub string_annotations: Vec<RangeStyle<StringAnnotation>>,
88 pub link_annotations: Vec<RangeStyle<LinkAnnotation>>,
91}
92
93#[derive(Debug, Clone, PartialEq)]
95pub struct RangeStyle<T> {
96 pub item: T,
97 pub range: Range<usize>,
98}
99
100impl AnnotatedString {
101 pub fn new(text: String) -> Self {
102 Self {
103 text,
104 span_styles: vec![],
105 paragraph_styles: vec![],
106 string_annotations: vec![],
107 link_annotations: vec![],
108 }
109 }
110
111 pub fn builder() -> Builder {
112 Builder::new()
113 }
114
115 pub fn len(&self) -> usize {
116 self.text.len()
117 }
118
119 pub fn is_empty(&self) -> bool {
120 self.text.is_empty()
121 }
122
123 pub fn span_boundaries(&self) -> Vec<usize> {
125 let mut boundaries = vec![0, self.text.len()];
126 for span in &self.span_styles {
127 boundaries.push(span.range.start);
128 boundaries.push(span.range.end);
129 }
130 boundaries.sort_unstable();
131 boundaries.dedup();
132 boundaries
133 .into_iter()
134 .filter(|&b| b <= self.text.len() && self.text.is_char_boundary(b))
135 .collect()
136 }
137
138 pub fn span_styles_hash(&self) -> u64 {
140 use std::hash::{Hash, Hasher};
141 let mut hasher = std::collections::hash_map::DefaultHasher::new();
142 hasher.write_usize(self.span_styles.len());
143 for span in &self.span_styles {
144 hasher.write_usize(span.range.start);
145 hasher.write_usize(span.range.end);
146
147 let dummy = crate::text::TextStyle {
149 span_style: span.item.clone(),
150 ..Default::default()
151 };
152 hasher.write_u64(dummy.measurement_hash());
153
154 if let Some(c) = &span.item.color {
156 hasher.write_u32(c.0.to_bits());
157 hasher.write_u32(c.1.to_bits());
158 hasher.write_u32(c.2.to_bits());
159 hasher.write_u32(c.3.to_bits());
160 }
161 if let Some(bg) = &span.item.background {
162 hasher.write_u32(bg.0.to_bits());
163 hasher.write_u32(bg.1.to_bits());
164 hasher.write_u32(bg.2.to_bits());
165 hasher.write_u32(bg.3.to_bits());
166 }
167 if let Some(d) = &span.item.text_decoration {
168 d.hash(&mut hasher);
169 }
170 }
171 hasher.finish()
172 }
173
174 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
177 if range.is_empty() {
178 return Self::new(String::new());
179 }
180
181 let start = range.start.min(self.text.len());
182 let end = range.end.max(start).min(self.text.len());
183
184 if start == end {
185 return Self::new(String::new());
186 }
187
188 let mut new_spans = Vec::new();
189 for span in &self.span_styles {
190 let intersection_start = span.range.start.max(start);
191 let intersection_end = span.range.end.min(end);
192 if intersection_start < intersection_end {
193 new_spans.push(RangeStyle {
194 item: span.item.clone(),
195 range: (intersection_start - start)..(intersection_end - start),
196 });
197 }
198 }
199
200 let mut new_paragraphs = Vec::new();
201 for span in &self.paragraph_styles {
202 let intersection_start = span.range.start.max(start);
203 let intersection_end = span.range.end.min(end);
204 if intersection_start < intersection_end {
205 new_paragraphs.push(RangeStyle {
206 item: span.item.clone(),
207 range: (intersection_start - start)..(intersection_end - start),
208 });
209 }
210 }
211
212 let mut new_string_annotations = Vec::new();
213 for ann in &self.string_annotations {
214 let intersection_start = ann.range.start.max(start);
215 let intersection_end = ann.range.end.min(end);
216 if intersection_start < intersection_end {
217 new_string_annotations.push(RangeStyle {
218 item: ann.item.clone(),
219 range: (intersection_start - start)..(intersection_end - start),
220 });
221 }
222 }
223
224 let mut new_link_annotations = Vec::new();
225 for ann in &self.link_annotations {
226 let intersection_start = ann.range.start.max(start);
227 let intersection_end = ann.range.end.min(end);
228 if intersection_start < intersection_end {
229 new_link_annotations.push(RangeStyle {
230 item: ann.item.clone(),
231 range: (intersection_start - start)..(intersection_end - start),
232 });
233 }
234 }
235
236 Self {
237 text: self.text[start..end].to_string(),
238 span_styles: new_spans,
239 paragraph_styles: new_paragraphs,
240 string_annotations: new_string_annotations,
241 link_annotations: new_link_annotations,
242 }
243 }
244
245 pub fn get_string_annotations(
249 &self,
250 tag: &str,
251 start: usize,
252 end: usize,
253 ) -> Vec<&RangeStyle<StringAnnotation>> {
254 self.string_annotations
255 .iter()
256 .filter(|ann| ann.item.tag == tag && ann.range.start < end && ann.range.end > start)
257 .collect()
258 }
259
260 pub fn get_link_annotations(
264 &self,
265 start: usize,
266 end: usize,
267 ) -> Vec<&RangeStyle<LinkAnnotation>> {
268 self.link_annotations
269 .iter()
270 .filter(|ann| ann.range.start < end && ann.range.end > start)
271 .collect()
272 }
273}
274
275impl From<String> for AnnotatedString {
276 fn from(text: String) -> Self {
277 Self::new(text)
278 }
279}
280
281impl From<&str> for AnnotatedString {
282 fn from(text: &str) -> Self {
283 Self::new(text.to_owned())
284 }
285}
286
287impl From<&String> for AnnotatedString {
288 fn from(text: &String) -> Self {
289 Self::new(text.clone())
290 }
291}
292
293impl From<&mut String> for AnnotatedString {
294 fn from(text: &mut String) -> Self {
295 Self::new(text.clone())
296 }
297}
298
299#[derive(Debug, Default, Clone)]
301pub struct Builder {
302 text: String,
303 span_styles: Vec<MutableRange<SpanStyle>>,
304 paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
305 string_annotations: Vec<MutableRange<StringAnnotation>>,
306 link_annotations: Vec<MutableRange<LinkAnnotation>>,
307 style_stack: Vec<StyleStackRecord>,
308}
309
310#[derive(Debug, Clone)]
311struct MutableRange<T> {
312 item: T,
313 start: usize,
314 end: usize,
315}
316
317#[derive(Debug, Clone)]
318struct StyleStackRecord {
319 style_type: StyleType,
320 index: usize,
321}
322
323#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324enum StyleType {
325 Span,
326 Paragraph,
327 StringAnnotation,
328 LinkAnnotation,
329}
330
331impl Builder {
332 pub fn new() -> Self {
333 Self::default()
334 }
335
336 pub fn append(mut self, text: &str) -> Self {
338 self.text.push_str(text);
339 self
340 }
341
342 pub fn push_style(mut self, style: SpanStyle) -> Self {
346 let index = self.span_styles.len();
347 self.span_styles.push(MutableRange {
348 item: style,
349 start: self.text.len(),
350 end: usize::MAX,
351 });
352 self.style_stack.push(StyleStackRecord {
353 style_type: StyleType::Span,
354 index,
355 });
356 self
357 }
358
359 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
361 let index = self.paragraph_styles.len();
362 self.paragraph_styles.push(MutableRange {
363 item: style,
364 start: self.text.len(),
365 end: usize::MAX,
366 });
367 self.style_stack.push(StyleStackRecord {
368 style_type: StyleType::Paragraph,
369 index,
370 });
371 self
372 }
373
374 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
378 let index = self.string_annotations.len();
379 self.string_annotations.push(MutableRange {
380 item: StringAnnotation {
381 tag: tag.to_string(),
382 annotation: annotation.to_string(),
383 },
384 start: self.text.len(),
385 end: usize::MAX,
386 });
387 self.style_stack.push(StyleStackRecord {
388 style_type: StyleType::StringAnnotation,
389 index,
390 });
391 self
392 }
393
394 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
399 let index = self.link_annotations.len();
400 self.link_annotations.push(MutableRange {
401 item: link,
402 start: self.text.len(),
403 end: usize::MAX,
404 });
405 self.style_stack.push(StyleStackRecord {
406 style_type: StyleType::LinkAnnotation,
407 index,
408 });
409 self
410 }
411
412 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
427 let b = self.push_link(link);
428 let b = block(b);
429 b.pop()
430 }
431
432 pub fn pop(mut self) -> Self {
434 if let Some(record) = self.style_stack.pop() {
435 match record.style_type {
436 StyleType::Span => {
437 self.span_styles[record.index].end = self.text.len();
438 }
439 StyleType::Paragraph => {
440 self.paragraph_styles[record.index].end = self.text.len();
441 }
442 StyleType::StringAnnotation => {
443 self.string_annotations[record.index].end = self.text.len();
444 }
445 StyleType::LinkAnnotation => {
446 self.link_annotations[record.index].end = self.text.len();
447 }
448 }
449 }
450 self
451 }
452
453 pub fn to_annotated_string(mut self) -> AnnotatedString {
455 while let Some(record) = self.style_stack.pop() {
457 match record.style_type {
458 StyleType::Span => {
459 self.span_styles[record.index].end = self.text.len();
460 }
461 StyleType::Paragraph => {
462 self.paragraph_styles[record.index].end = self.text.len();
463 }
464 StyleType::StringAnnotation => {
465 self.string_annotations[record.index].end = self.text.len();
466 }
467 StyleType::LinkAnnotation => {
468 self.link_annotations[record.index].end = self.text.len();
469 }
470 }
471 }
472
473 AnnotatedString {
474 text: self.text,
475 span_styles: self
476 .span_styles
477 .into_iter()
478 .map(|s| RangeStyle {
479 item: s.item,
480 range: s.start..s.end,
481 })
482 .collect(),
483 paragraph_styles: self
484 .paragraph_styles
485 .into_iter()
486 .map(|s| RangeStyle {
487 item: s.item,
488 range: s.start..s.end,
489 })
490 .collect(),
491 string_annotations: self
492 .string_annotations
493 .into_iter()
494 .map(|s| RangeStyle {
495 item: s.item,
496 range: s.start..s.end,
497 })
498 .collect(),
499 link_annotations: self
500 .link_annotations
501 .into_iter()
502 .map(|s| RangeStyle {
503 item: s.item,
504 range: s.start..s.end,
505 })
506 .collect(),
507 }
508 }
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514
515 #[test]
516 fn test_builder_span() {
517 let span1 = SpanStyle {
518 alpha: Some(0.5),
519 ..Default::default()
520 };
521
522 let span2 = SpanStyle {
523 alpha: Some(1.0),
524 ..Default::default()
525 };
526
527 let annotated = AnnotatedString::builder()
528 .append("Hello ")
529 .push_style(span1.clone())
530 .append("World")
531 .push_style(span2.clone())
532 .append("!")
533 .pop()
534 .pop()
535 .to_annotated_string();
536
537 assert_eq!(annotated.text, "Hello World!");
538 assert_eq!(annotated.span_styles.len(), 2);
539 assert_eq!(annotated.span_styles[0].range, 6..12);
540 assert_eq!(annotated.span_styles[0].item, span1);
541 assert_eq!(annotated.span_styles[1].range, 11..12);
542 assert_eq!(annotated.span_styles[1].item, span2);
543 }
544
545 #[test]
546 fn with_link_url_roundtrips() {
547 let url = "https://developer.android.com";
548 let annotated = AnnotatedString::builder()
549 .append("Visit ")
550 .with_link(LinkAnnotation::Url(url.into()), |b| {
551 b.append("Android Developers")
552 })
553 .append(".")
554 .to_annotated_string();
555
556 assert_eq!(annotated.text, "Visit Android Developers.");
557 assert_eq!(annotated.link_annotations.len(), 1);
558 let ann = &annotated.link_annotations[0];
559 assert_eq!(ann.range, 6..24);
561 assert_eq!(ann.item, LinkAnnotation::Url(url.into()));
562 }
563
564 #[test]
565 fn with_link_clickable_calls_handler() {
566 use std::cell::Cell;
567 let called = Rc::new(Cell::new(false));
568 let called_clone = Rc::clone(&called);
569
570 let annotated = AnnotatedString::builder()
571 .with_link(
572 LinkAnnotation::Clickable {
573 tag: "action".into(),
574 handler: Rc::new(move || called_clone.set(true)),
575 },
576 |b| b.append("click me"),
577 )
578 .to_annotated_string();
579
580 assert_eq!(annotated.link_annotations.len(), 1);
581 let ann = &annotated.link_annotations[0];
583 if let LinkAnnotation::Clickable { handler, .. } = &ann.item {
584 handler();
585 }
586 assert!(called.get(), "Clickable handler should have been called");
587 }
588
589 #[test]
590 fn with_link_subsequence_trims_range() {
591 let annotated = AnnotatedString::builder()
592 .append("pre ")
593 .with_link(LinkAnnotation::Url("http://x.com".into()), |b| {
594 b.append("link")
595 })
596 .append(" post")
597 .to_annotated_string();
598
599 let sub = annotated.subsequence(4..8); assert_eq!(sub.link_annotations.len(), 1);
602 assert_eq!(sub.link_annotations[0].range, 0..4);
603 }
604}