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 render_hash(&self) -> u64 {
175 use std::hash::{Hash, Hasher};
176
177 let mut hasher = std::collections::hash_map::DefaultHasher::new();
178 self.text.hash(&mut hasher);
179 self.span_styles.len().hash(&mut hasher);
180 for span in &self.span_styles {
181 span.range.start.hash(&mut hasher);
182 span.range.end.hash(&mut hasher);
183 span.item.render_hash().hash(&mut hasher);
184 }
185 self.paragraph_styles.len().hash(&mut hasher);
186 for paragraph in &self.paragraph_styles {
187 paragraph.range.start.hash(&mut hasher);
188 paragraph.range.end.hash(&mut hasher);
189 paragraph.item.render_hash().hash(&mut hasher);
190 }
191 hasher.finish()
192 }
193
194 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
197 if range.is_empty() {
198 return Self::new(String::new());
199 }
200
201 let start = range.start.min(self.text.len());
202 let end = range.end.max(start).min(self.text.len());
203
204 if start == end {
205 return Self::new(String::new());
206 }
207
208 let mut new_spans = Vec::new();
209 for span in &self.span_styles {
210 let intersection_start = span.range.start.max(start);
211 let intersection_end = span.range.end.min(end);
212 if intersection_start < intersection_end {
213 new_spans.push(RangeStyle {
214 item: span.item.clone(),
215 range: (intersection_start - start)..(intersection_end - start),
216 });
217 }
218 }
219
220 let mut new_paragraphs = Vec::new();
221 for span in &self.paragraph_styles {
222 let intersection_start = span.range.start.max(start);
223 let intersection_end = span.range.end.min(end);
224 if intersection_start < intersection_end {
225 new_paragraphs.push(RangeStyle {
226 item: span.item.clone(),
227 range: (intersection_start - start)..(intersection_end - start),
228 });
229 }
230 }
231
232 let mut new_string_annotations = Vec::new();
233 for ann in &self.string_annotations {
234 let intersection_start = ann.range.start.max(start);
235 let intersection_end = ann.range.end.min(end);
236 if intersection_start < intersection_end {
237 new_string_annotations.push(RangeStyle {
238 item: ann.item.clone(),
239 range: (intersection_start - start)..(intersection_end - start),
240 });
241 }
242 }
243
244 let mut new_link_annotations = Vec::new();
245 for ann in &self.link_annotations {
246 let intersection_start = ann.range.start.max(start);
247 let intersection_end = ann.range.end.min(end);
248 if intersection_start < intersection_end {
249 new_link_annotations.push(RangeStyle {
250 item: ann.item.clone(),
251 range: (intersection_start - start)..(intersection_end - start),
252 });
253 }
254 }
255
256 Self {
257 text: self.text[start..end].to_string(),
258 span_styles: new_spans,
259 paragraph_styles: new_paragraphs,
260 string_annotations: new_string_annotations,
261 link_annotations: new_link_annotations,
262 }
263 }
264
265 pub fn get_string_annotations(
269 &self,
270 tag: &str,
271 start: usize,
272 end: usize,
273 ) -> Vec<&RangeStyle<StringAnnotation>> {
274 self.string_annotations
275 .iter()
276 .filter(|ann| ann.item.tag == tag && ann.range.start < end && ann.range.end > start)
277 .collect()
278 }
279
280 pub fn get_link_annotations(
284 &self,
285 start: usize,
286 end: usize,
287 ) -> Vec<&RangeStyle<LinkAnnotation>> {
288 self.link_annotations
289 .iter()
290 .filter(|ann| ann.range.start < end && ann.range.end > start)
291 .collect()
292 }
293}
294
295impl From<String> for AnnotatedString {
296 fn from(text: String) -> Self {
297 Self::new(text)
298 }
299}
300
301impl From<&str> for AnnotatedString {
302 fn from(text: &str) -> Self {
303 Self::new(text.to_owned())
304 }
305}
306
307impl From<&String> for AnnotatedString {
308 fn from(text: &String) -> Self {
309 Self::new(text.clone())
310 }
311}
312
313impl From<&mut String> for AnnotatedString {
314 fn from(text: &mut String) -> Self {
315 Self::new(text.clone())
316 }
317}
318
319#[derive(Debug, Default, Clone)]
321pub struct Builder {
322 text: String,
323 span_styles: Vec<MutableRange<SpanStyle>>,
324 paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
325 string_annotations: Vec<MutableRange<StringAnnotation>>,
326 link_annotations: Vec<MutableRange<LinkAnnotation>>,
327 style_stack: Vec<StyleStackRecord>,
328}
329
330#[derive(Debug, Clone)]
331struct MutableRange<T> {
332 item: T,
333 start: usize,
334 end: usize,
335}
336
337#[derive(Debug, Clone)]
338struct StyleStackRecord {
339 style_type: StyleType,
340 index: usize,
341}
342
343#[derive(Debug, Clone, Copy, PartialEq, Eq)]
344enum StyleType {
345 Span,
346 Paragraph,
347 StringAnnotation,
348 LinkAnnotation,
349}
350
351fn clamp_subsequence_range(text: &str, range: Range<usize>) -> Range<usize> {
352 let start = range.start.min(text.len());
353 let end = range.end.max(start).min(text.len());
354 start..end
355}
356
357fn append_clipped_ranges<T: Clone>(
358 target: &mut Vec<MutableRange<T>>,
359 source: &[RangeStyle<T>],
360 source_range: Range<usize>,
361 target_offset: usize,
362) {
363 for style in source {
364 let intersection_start = style.range.start.max(source_range.start);
365 let intersection_end = style.range.end.min(source_range.end);
366 if intersection_start < intersection_end {
367 target.push(MutableRange {
368 item: style.item.clone(),
369 start: (intersection_start - source_range.start) + target_offset,
370 end: (intersection_end - source_range.start) + target_offset,
371 });
372 }
373 }
374}
375
376impl Builder {
377 pub fn new() -> Self {
378 Self::default()
379 }
380
381 pub fn append(mut self, text: &str) -> Self {
383 self.text.push_str(text);
384 self
385 }
386
387 pub fn append_annotated(self, annotated: &AnnotatedString) -> Self {
388 self.append_annotated_subsequence(annotated, 0..annotated.text.len())
389 }
390
391 pub fn append_annotated_subsequence(
392 mut self,
393 annotated: &AnnotatedString,
394 range: Range<usize>,
395 ) -> Self {
396 let range = clamp_subsequence_range(annotated.text.as_str(), range);
397 if range.is_empty() {
398 return self;
399 }
400
401 debug_assert!(annotated.text.is_char_boundary(range.start));
402 debug_assert!(annotated.text.is_char_boundary(range.end));
403
404 let target_offset = self.text.len();
405 self.text.push_str(&annotated.text[range.clone()]);
406 append_clipped_ranges(
407 &mut self.span_styles,
408 &annotated.span_styles,
409 range.clone(),
410 target_offset,
411 );
412 append_clipped_ranges(
413 &mut self.paragraph_styles,
414 &annotated.paragraph_styles,
415 range.clone(),
416 target_offset,
417 );
418 append_clipped_ranges(
419 &mut self.string_annotations,
420 &annotated.string_annotations,
421 range.clone(),
422 target_offset,
423 );
424 append_clipped_ranges(
425 &mut self.link_annotations,
426 &annotated.link_annotations,
427 range,
428 target_offset,
429 );
430 self
431 }
432
433 pub fn push_style(mut self, style: SpanStyle) -> Self {
437 let index = self.span_styles.len();
438 self.span_styles.push(MutableRange {
439 item: style,
440 start: self.text.len(),
441 end: usize::MAX,
442 });
443 self.style_stack.push(StyleStackRecord {
444 style_type: StyleType::Span,
445 index,
446 });
447 self
448 }
449
450 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
452 let index = self.paragraph_styles.len();
453 self.paragraph_styles.push(MutableRange {
454 item: style,
455 start: self.text.len(),
456 end: usize::MAX,
457 });
458 self.style_stack.push(StyleStackRecord {
459 style_type: StyleType::Paragraph,
460 index,
461 });
462 self
463 }
464
465 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
469 let index = self.string_annotations.len();
470 self.string_annotations.push(MutableRange {
471 item: StringAnnotation {
472 tag: tag.to_string(),
473 annotation: annotation.to_string(),
474 },
475 start: self.text.len(),
476 end: usize::MAX,
477 });
478 self.style_stack.push(StyleStackRecord {
479 style_type: StyleType::StringAnnotation,
480 index,
481 });
482 self
483 }
484
485 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
490 let index = self.link_annotations.len();
491 self.link_annotations.push(MutableRange {
492 item: link,
493 start: self.text.len(),
494 end: usize::MAX,
495 });
496 self.style_stack.push(StyleStackRecord {
497 style_type: StyleType::LinkAnnotation,
498 index,
499 });
500 self
501 }
502
503 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
518 let b = self.push_link(link);
519 let b = block(b);
520 b.pop()
521 }
522
523 pub fn pop(mut self) -> Self {
525 if let Some(record) = self.style_stack.pop() {
526 match record.style_type {
527 StyleType::Span => {
528 self.span_styles[record.index].end = self.text.len();
529 }
530 StyleType::Paragraph => {
531 self.paragraph_styles[record.index].end = self.text.len();
532 }
533 StyleType::StringAnnotation => {
534 self.string_annotations[record.index].end = self.text.len();
535 }
536 StyleType::LinkAnnotation => {
537 self.link_annotations[record.index].end = self.text.len();
538 }
539 }
540 }
541 self
542 }
543
544 pub fn to_annotated_string(mut self) -> AnnotatedString {
546 while let Some(record) = self.style_stack.pop() {
548 match record.style_type {
549 StyleType::Span => {
550 self.span_styles[record.index].end = self.text.len();
551 }
552 StyleType::Paragraph => {
553 self.paragraph_styles[record.index].end = self.text.len();
554 }
555 StyleType::StringAnnotation => {
556 self.string_annotations[record.index].end = self.text.len();
557 }
558 StyleType::LinkAnnotation => {
559 self.link_annotations[record.index].end = self.text.len();
560 }
561 }
562 }
563
564 AnnotatedString {
565 text: self.text,
566 span_styles: self
567 .span_styles
568 .into_iter()
569 .map(|s| RangeStyle {
570 item: s.item,
571 range: s.start..s.end,
572 })
573 .collect(),
574 paragraph_styles: self
575 .paragraph_styles
576 .into_iter()
577 .map(|s| RangeStyle {
578 item: s.item,
579 range: s.start..s.end,
580 })
581 .collect(),
582 string_annotations: self
583 .string_annotations
584 .into_iter()
585 .map(|s| RangeStyle {
586 item: s.item,
587 range: s.start..s.end,
588 })
589 .collect(),
590 link_annotations: self
591 .link_annotations
592 .into_iter()
593 .map(|s| RangeStyle {
594 item: s.item,
595 range: s.start..s.end,
596 })
597 .collect(),
598 }
599 }
600}
601
602#[cfg(test)]
603mod tests {
604 use super::*;
605
606 #[test]
607 fn test_builder_span() {
608 let span1 = SpanStyle {
609 alpha: Some(0.5),
610 ..Default::default()
611 };
612
613 let span2 = SpanStyle {
614 alpha: Some(1.0),
615 ..Default::default()
616 };
617
618 let annotated = AnnotatedString::builder()
619 .append("Hello ")
620 .push_style(span1.clone())
621 .append("World")
622 .push_style(span2.clone())
623 .append("!")
624 .pop()
625 .pop()
626 .to_annotated_string();
627
628 assert_eq!(annotated.text, "Hello World!");
629 assert_eq!(annotated.span_styles.len(), 2);
630 assert_eq!(annotated.span_styles[0].range, 6..12);
631 assert_eq!(annotated.span_styles[0].item, span1);
632 assert_eq!(annotated.span_styles[1].range, 11..12);
633 assert_eq!(annotated.span_styles[1].item, span2);
634 }
635
636 #[test]
637 fn with_link_url_roundtrips() {
638 let url = "https://developer.android.com";
639 let annotated = AnnotatedString::builder()
640 .append("Visit ")
641 .with_link(LinkAnnotation::Url(url.into()), |b| {
642 b.append("Android Developers")
643 })
644 .append(".")
645 .to_annotated_string();
646
647 assert_eq!(annotated.text, "Visit Android Developers.");
648 assert_eq!(annotated.link_annotations.len(), 1);
649 let ann = &annotated.link_annotations[0];
650 assert_eq!(ann.range, 6..24);
652 assert_eq!(ann.item, LinkAnnotation::Url(url.into()));
653 }
654
655 #[test]
656 fn with_link_clickable_calls_handler() {
657 use std::cell::Cell;
658 let called = Rc::new(Cell::new(false));
659 let called_clone = Rc::clone(&called);
660
661 let annotated = AnnotatedString::builder()
662 .with_link(
663 LinkAnnotation::Clickable {
664 tag: "action".into(),
665 handler: Rc::new(move || called_clone.set(true)),
666 },
667 |b| b.append("click me"),
668 )
669 .to_annotated_string();
670
671 assert_eq!(annotated.link_annotations.len(), 1);
672 let ann = &annotated.link_annotations[0];
674 if let LinkAnnotation::Clickable { handler, .. } = &ann.item {
675 handler();
676 }
677 assert!(called.get(), "Clickable handler should have been called");
678 }
679
680 #[test]
681 fn with_link_subsequence_trims_range() {
682 let annotated = AnnotatedString::builder()
683 .append("pre ")
684 .with_link(LinkAnnotation::Url("http://x.com".into()), |b| {
685 b.append("link")
686 })
687 .append(" post")
688 .to_annotated_string();
689
690 let sub = annotated.subsequence(4..8); assert_eq!(sub.link_annotations.len(), 1);
693 assert_eq!(sub.link_annotations[0].range, 0..4);
694 }
695
696 #[test]
697 fn append_annotated_preserves_ranges_with_existing_prefix() {
698 let annotated = AnnotatedString::builder()
699 .append("Hello ")
700 .push_style(SpanStyle {
701 alpha: Some(0.5),
702 ..Default::default()
703 })
704 .append("World")
705 .pop()
706 .push_string_annotation("kind", "planet")
707 .append("!")
708 .pop()
709 .to_annotated_string();
710
711 let combined = AnnotatedString::builder()
712 .append("Prefix ")
713 .append_annotated(&annotated)
714 .to_annotated_string();
715
716 assert_eq!(combined.text, "Prefix Hello World!");
717 assert_eq!(combined.span_styles.len(), 1);
718 assert_eq!(combined.span_styles[0].range, 13..18);
719 assert_eq!(combined.string_annotations.len(), 1);
720 assert_eq!(combined.string_annotations[0].range, 18..19);
721 }
722
723 #[test]
724 fn append_annotated_subsequence_clips_ranges_to_slice() {
725 let annotated = AnnotatedString::builder()
726 .append("Before ")
727 .push_style(SpanStyle {
728 alpha: Some(0.5),
729 ..Default::default()
730 })
731 .append("Styled")
732 .pop()
733 .with_link(LinkAnnotation::Url("https://example.com".into()), |b| {
734 b.append(" Link")
735 })
736 .to_annotated_string();
737
738 let slice = AnnotatedString::builder()
739 .append("-> ")
740 .append_annotated_subsequence(&annotated, 7..18)
741 .to_annotated_string();
742
743 assert_eq!(slice.text, "-> Styled Link");
744 assert_eq!(slice.span_styles.len(), 1);
745 assert_eq!(slice.span_styles[0].range, 3..9);
746 assert_eq!(slice.link_annotations.len(), 1);
747 assert_eq!(slice.link_annotations[0].range, 9..14);
748 }
749
750 #[test]
751 fn render_hash_changes_for_visual_style_ranges() {
752 let plain = AnnotatedString::builder()
753 .append("Hello")
754 .to_annotated_string();
755 let styled = AnnotatedString::builder()
756 .push_style(SpanStyle {
757 color: Some(crate::modifier::Color(1.0, 0.0, 0.0, 1.0)),
758 ..Default::default()
759 })
760 .append("Hello")
761 .pop()
762 .to_annotated_string();
763
764 assert_ne!(plain.render_hash(), styled.render_hash());
765 }
766}