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
331fn clamp_subsequence_range(text: &str, range: Range<usize>) -> Range<usize> {
332 let start = range.start.min(text.len());
333 let end = range.end.max(start).min(text.len());
334 start..end
335}
336
337fn append_clipped_ranges<T: Clone>(
338 target: &mut Vec<MutableRange<T>>,
339 source: &[RangeStyle<T>],
340 source_range: Range<usize>,
341 target_offset: usize,
342) {
343 for style in source {
344 let intersection_start = style.range.start.max(source_range.start);
345 let intersection_end = style.range.end.min(source_range.end);
346 if intersection_start < intersection_end {
347 target.push(MutableRange {
348 item: style.item.clone(),
349 start: (intersection_start - source_range.start) + target_offset,
350 end: (intersection_end - source_range.start) + target_offset,
351 });
352 }
353 }
354}
355
356impl Builder {
357 pub fn new() -> Self {
358 Self::default()
359 }
360
361 pub fn append(mut self, text: &str) -> Self {
363 self.text.push_str(text);
364 self
365 }
366
367 pub fn append_annotated(self, annotated: &AnnotatedString) -> Self {
368 self.append_annotated_subsequence(annotated, 0..annotated.text.len())
369 }
370
371 pub fn append_annotated_subsequence(
372 mut self,
373 annotated: &AnnotatedString,
374 range: Range<usize>,
375 ) -> Self {
376 let range = clamp_subsequence_range(annotated.text.as_str(), range);
377 if range.is_empty() {
378 return self;
379 }
380
381 debug_assert!(annotated.text.is_char_boundary(range.start));
382 debug_assert!(annotated.text.is_char_boundary(range.end));
383
384 let target_offset = self.text.len();
385 self.text.push_str(&annotated.text[range.clone()]);
386 append_clipped_ranges(
387 &mut self.span_styles,
388 &annotated.span_styles,
389 range.clone(),
390 target_offset,
391 );
392 append_clipped_ranges(
393 &mut self.paragraph_styles,
394 &annotated.paragraph_styles,
395 range.clone(),
396 target_offset,
397 );
398 append_clipped_ranges(
399 &mut self.string_annotations,
400 &annotated.string_annotations,
401 range.clone(),
402 target_offset,
403 );
404 append_clipped_ranges(
405 &mut self.link_annotations,
406 &annotated.link_annotations,
407 range,
408 target_offset,
409 );
410 self
411 }
412
413 pub fn push_style(mut self, style: SpanStyle) -> Self {
417 let index = self.span_styles.len();
418 self.span_styles.push(MutableRange {
419 item: style,
420 start: self.text.len(),
421 end: usize::MAX,
422 });
423 self.style_stack.push(StyleStackRecord {
424 style_type: StyleType::Span,
425 index,
426 });
427 self
428 }
429
430 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
432 let index = self.paragraph_styles.len();
433 self.paragraph_styles.push(MutableRange {
434 item: style,
435 start: self.text.len(),
436 end: usize::MAX,
437 });
438 self.style_stack.push(StyleStackRecord {
439 style_type: StyleType::Paragraph,
440 index,
441 });
442 self
443 }
444
445 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
449 let index = self.string_annotations.len();
450 self.string_annotations.push(MutableRange {
451 item: StringAnnotation {
452 tag: tag.to_string(),
453 annotation: annotation.to_string(),
454 },
455 start: self.text.len(),
456 end: usize::MAX,
457 });
458 self.style_stack.push(StyleStackRecord {
459 style_type: StyleType::StringAnnotation,
460 index,
461 });
462 self
463 }
464
465 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
470 let index = self.link_annotations.len();
471 self.link_annotations.push(MutableRange {
472 item: link,
473 start: self.text.len(),
474 end: usize::MAX,
475 });
476 self.style_stack.push(StyleStackRecord {
477 style_type: StyleType::LinkAnnotation,
478 index,
479 });
480 self
481 }
482
483 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
498 let b = self.push_link(link);
499 let b = block(b);
500 b.pop()
501 }
502
503 pub fn pop(mut self) -> Self {
505 if let Some(record) = self.style_stack.pop() {
506 match record.style_type {
507 StyleType::Span => {
508 self.span_styles[record.index].end = self.text.len();
509 }
510 StyleType::Paragraph => {
511 self.paragraph_styles[record.index].end = self.text.len();
512 }
513 StyleType::StringAnnotation => {
514 self.string_annotations[record.index].end = self.text.len();
515 }
516 StyleType::LinkAnnotation => {
517 self.link_annotations[record.index].end = self.text.len();
518 }
519 }
520 }
521 self
522 }
523
524 pub fn to_annotated_string(mut self) -> AnnotatedString {
526 while let Some(record) = self.style_stack.pop() {
528 match record.style_type {
529 StyleType::Span => {
530 self.span_styles[record.index].end = self.text.len();
531 }
532 StyleType::Paragraph => {
533 self.paragraph_styles[record.index].end = self.text.len();
534 }
535 StyleType::StringAnnotation => {
536 self.string_annotations[record.index].end = self.text.len();
537 }
538 StyleType::LinkAnnotation => {
539 self.link_annotations[record.index].end = self.text.len();
540 }
541 }
542 }
543
544 AnnotatedString {
545 text: self.text,
546 span_styles: self
547 .span_styles
548 .into_iter()
549 .map(|s| RangeStyle {
550 item: s.item,
551 range: s.start..s.end,
552 })
553 .collect(),
554 paragraph_styles: self
555 .paragraph_styles
556 .into_iter()
557 .map(|s| RangeStyle {
558 item: s.item,
559 range: s.start..s.end,
560 })
561 .collect(),
562 string_annotations: self
563 .string_annotations
564 .into_iter()
565 .map(|s| RangeStyle {
566 item: s.item,
567 range: s.start..s.end,
568 })
569 .collect(),
570 link_annotations: self
571 .link_annotations
572 .into_iter()
573 .map(|s| RangeStyle {
574 item: s.item,
575 range: s.start..s.end,
576 })
577 .collect(),
578 }
579 }
580}
581
582#[cfg(test)]
583mod tests {
584 use super::*;
585
586 #[test]
587 fn test_builder_span() {
588 let span1 = SpanStyle {
589 alpha: Some(0.5),
590 ..Default::default()
591 };
592
593 let span2 = SpanStyle {
594 alpha: Some(1.0),
595 ..Default::default()
596 };
597
598 let annotated = AnnotatedString::builder()
599 .append("Hello ")
600 .push_style(span1.clone())
601 .append("World")
602 .push_style(span2.clone())
603 .append("!")
604 .pop()
605 .pop()
606 .to_annotated_string();
607
608 assert_eq!(annotated.text, "Hello World!");
609 assert_eq!(annotated.span_styles.len(), 2);
610 assert_eq!(annotated.span_styles[0].range, 6..12);
611 assert_eq!(annotated.span_styles[0].item, span1);
612 assert_eq!(annotated.span_styles[1].range, 11..12);
613 assert_eq!(annotated.span_styles[1].item, span2);
614 }
615
616 #[test]
617 fn with_link_url_roundtrips() {
618 let url = "https://developer.android.com";
619 let annotated = AnnotatedString::builder()
620 .append("Visit ")
621 .with_link(LinkAnnotation::Url(url.into()), |b| {
622 b.append("Android Developers")
623 })
624 .append(".")
625 .to_annotated_string();
626
627 assert_eq!(annotated.text, "Visit Android Developers.");
628 assert_eq!(annotated.link_annotations.len(), 1);
629 let ann = &annotated.link_annotations[0];
630 assert_eq!(ann.range, 6..24);
632 assert_eq!(ann.item, LinkAnnotation::Url(url.into()));
633 }
634
635 #[test]
636 fn with_link_clickable_calls_handler() {
637 use std::cell::Cell;
638 let called = Rc::new(Cell::new(false));
639 let called_clone = Rc::clone(&called);
640
641 let annotated = AnnotatedString::builder()
642 .with_link(
643 LinkAnnotation::Clickable {
644 tag: "action".into(),
645 handler: Rc::new(move || called_clone.set(true)),
646 },
647 |b| b.append("click me"),
648 )
649 .to_annotated_string();
650
651 assert_eq!(annotated.link_annotations.len(), 1);
652 let ann = &annotated.link_annotations[0];
654 if let LinkAnnotation::Clickable { handler, .. } = &ann.item {
655 handler();
656 }
657 assert!(called.get(), "Clickable handler should have been called");
658 }
659
660 #[test]
661 fn with_link_subsequence_trims_range() {
662 let annotated = AnnotatedString::builder()
663 .append("pre ")
664 .with_link(LinkAnnotation::Url("http://x.com".into()), |b| {
665 b.append("link")
666 })
667 .append(" post")
668 .to_annotated_string();
669
670 let sub = annotated.subsequence(4..8); assert_eq!(sub.link_annotations.len(), 1);
673 assert_eq!(sub.link_annotations[0].range, 0..4);
674 }
675
676 #[test]
677 fn append_annotated_preserves_ranges_with_existing_prefix() {
678 let annotated = AnnotatedString::builder()
679 .append("Hello ")
680 .push_style(SpanStyle {
681 alpha: Some(0.5),
682 ..Default::default()
683 })
684 .append("World")
685 .pop()
686 .push_string_annotation("kind", "planet")
687 .append("!")
688 .pop()
689 .to_annotated_string();
690
691 let combined = AnnotatedString::builder()
692 .append("Prefix ")
693 .append_annotated(&annotated)
694 .to_annotated_string();
695
696 assert_eq!(combined.text, "Prefix Hello World!");
697 assert_eq!(combined.span_styles.len(), 1);
698 assert_eq!(combined.span_styles[0].range, 13..18);
699 assert_eq!(combined.string_annotations.len(), 1);
700 assert_eq!(combined.string_annotations[0].range, 18..19);
701 }
702
703 #[test]
704 fn append_annotated_subsequence_clips_ranges_to_slice() {
705 let annotated = AnnotatedString::builder()
706 .append("Before ")
707 .push_style(SpanStyle {
708 alpha: Some(0.5),
709 ..Default::default()
710 })
711 .append("Styled")
712 .pop()
713 .with_link(LinkAnnotation::Url("https://example.com".into()), |b| {
714 b.append(" Link")
715 })
716 .to_annotated_string();
717
718 let slice = AnnotatedString::builder()
719 .append("-> ")
720 .append_annotated_subsequence(&annotated, 7..18)
721 .to_annotated_string();
722
723 assert_eq!(slice.text, "-> Styled Link");
724 assert_eq!(slice.span_styles.len(), 1);
725 assert_eq!(slice.span_styles[0].range, 3..9);
726 assert_eq!(slice.link_annotations.len(), 1);
727 assert_eq!(slice.link_annotations[0].range, 9..14);
728 }
729}