1use std::{ops::Range, rc::Rc};
2
3use crate::{ParagraphStyle, SpanStyle};
4
5#[derive(Clone)]
26pub enum LinkAnnotation {
27 Url(String),
31
32 Clickable { tag: String, handler: Rc<dyn Fn()> },
36}
37
38impl std::fmt::Debug for LinkAnnotation {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 Self::Url(url) => f.debug_tuple("Url").field(url).finish(),
42 Self::Clickable { tag, .. } => f.debug_struct("Clickable").field("tag", tag).finish(),
43 }
44 }
45}
46
47impl PartialEq for LinkAnnotation {
48 fn eq(&self, other: &Self) -> bool {
49 match (self, other) {
50 (Self::Url(a), Self::Url(b)) => a == b,
51 (
52 Self::Clickable {
53 tag: ta,
54 handler: ha,
55 },
56 Self::Clickable {
57 tag: tb,
58 handler: hb,
59 },
60 ) => ta == tb && Rc::ptr_eq(ha, hb),
61 _ => false,
62 }
63 }
64}
65
66#[derive(Debug, Clone, PartialEq)]
71pub struct StringAnnotation {
72 pub tag: String,
73 pub annotation: String,
74}
75
76#[derive(Debug, Clone, PartialEq)]
80pub enum LinkKey {
81 Url(String),
83 Clickable(String),
85}
86
87#[derive(Debug, Clone, PartialEq)]
96pub struct RenderString {
97 text: String,
98 span_styles: Vec<RangeStyle<SpanStyle>>,
99 paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
100 string_annotations: Vec<RangeStyle<StringAnnotation>>,
101 links: Vec<RangeStyle<LinkKey>>,
102 hash: u64,
103}
104
105impl Default for RenderString {
106 fn default() -> Self {
107 Self::from_parts(
108 String::new(),
109 Vec::new(),
110 Vec::new(),
111 Vec::new(),
112 Vec::new(),
113 )
114 }
115}
116
117const _: () = {
118 fn assert_send<T: Send + Sync>() {}
119 #[expect(dead_code)]
120 fn assert_render_string_is_send_sync() {
121 assert_send::<RenderString>();
122 }
123};
124
125impl RenderString {
126 fn from_parts(
127 text: String,
128 span_styles: Vec<RangeStyle<SpanStyle>>,
129 paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
130 string_annotations: Vec<RangeStyle<StringAnnotation>>,
131 links: Vec<RangeStyle<LinkKey>>,
132 ) -> Self {
133 let hash = render_hash_impl(&text, &span_styles, ¶graph_styles);
134 Self {
135 text,
136 span_styles,
137 paragraph_styles,
138 string_annotations,
139 links,
140 hash,
141 }
142 }
143
144 pub fn text(&self) -> &str {
146 &self.text
147 }
148
149 pub fn span_styles(&self) -> &[RangeStyle<SpanStyle>] {
150 &self.span_styles
151 }
152
153 pub fn paragraph_styles(&self) -> &[RangeStyle<ParagraphStyle>] {
154 &self.paragraph_styles
155 }
156
157 pub fn string_annotations(&self) -> &[RangeStyle<StringAnnotation>] {
158 &self.string_annotations
159 }
160
161 pub fn links(&self) -> &[RangeStyle<LinkKey>] {
164 &self.links
165 }
166
167 pub fn len(&self) -> usize {
168 self.text.len()
169 }
170
171 pub fn is_empty(&self) -> bool {
172 self.text.is_empty()
173 }
174
175 pub fn span_boundaries(&self) -> SpanBoundaries {
179 span_boundaries_impl(&self.text, &self.span_styles)
180 }
181
182 pub fn render_hash(&self) -> u64 {
186 self.hash
187 }
188
189 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
193 let start = range.start.min(self.text.len());
194 let end = range.end.max(start).min(self.text.len());
195 if start == end {
196 return Self::default();
197 }
198
199 Self::from_parts(
200 self.text[start..end].to_string(),
201 clip_range_styles(&self.span_styles, start, end),
202 clip_range_styles(&self.paragraph_styles, start, end),
203 clip_range_styles(&self.string_annotations, start, end),
204 clip_range_styles(&self.links, start, end),
205 )
206 }
207}
208
209fn clip_range_styles<T: Clone>(
210 styles: &[RangeStyle<T>],
211 start: usize,
212 end: usize,
213) -> Vec<RangeStyle<T>> {
214 let mut clipped = Vec::new();
215 for style in styles {
216 let intersection_start = style.range.start.max(start);
217 let intersection_end = style.range.end.min(end);
218 if intersection_start < intersection_end {
219 clipped.push(RangeStyle {
220 item: style.item.clone(),
221 range: (intersection_start - start)..(intersection_end - start),
222 });
223 }
224 }
225 clipped
226}
227
228pub type SpanBoundaries = smallvec::SmallVec<[usize; 8]>;
231
232fn span_boundaries_impl(text: &str, span_styles: &[RangeStyle<SpanStyle>]) -> SpanBoundaries {
233 let mut boundaries: SpanBoundaries = smallvec::smallvec![0, text.len()];
234 for span in span_styles {
235 boundaries.push(span.range.start);
236 boundaries.push(span.range.end);
237 }
238 boundaries.sort_unstable();
239 boundaries.dedup();
240 boundaries.retain(|boundary| *boundary <= text.len() && text.is_char_boundary(*boundary));
241 boundaries
242}
243
244fn render_hash_impl(
245 text: &str,
246 span_styles: &[RangeStyle<SpanStyle>],
247 paragraph_styles: &[RangeStyle<ParagraphStyle>],
248) -> u64 {
249 use std::hash::{Hash, Hasher};
250
251 let mut hasher = cranpose_ui_graphics::FxHasher::default();
252 text.hash(&mut hasher);
253 span_styles.len().hash(&mut hasher);
254 for span in span_styles {
255 span.range.start.hash(&mut hasher);
256 span.range.end.hash(&mut hasher);
257 span.item.render_hash().hash(&mut hasher);
258 }
259 paragraph_styles.len().hash(&mut hasher);
260 for paragraph in paragraph_styles {
261 paragraph.range.start.hash(&mut hasher);
262 paragraph.range.end.hash(&mut hasher);
263 paragraph.item.render_hash().hash(&mut hasher);
264 }
265 hasher.finish()
266}
267
268#[derive(Debug, Clone, PartialEq, Default)]
272pub struct AnnotatedString {
273 pub text: String,
274 pub span_styles: Vec<RangeStyle<SpanStyle>>,
275 pub paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
276 pub string_annotations: Vec<RangeStyle<StringAnnotation>>,
279 pub link_annotations: Vec<RangeStyle<LinkAnnotation>>,
282}
283
284#[derive(Debug, Clone, PartialEq)]
286pub struct RangeStyle<T> {
287 pub item: T,
288 pub range: Range<usize>,
289}
290
291pub fn shared_plain_annotated_string(text: &str) -> Rc<AnnotatedString> {
302 with_shared_plain_text(text, |shared| Rc::clone(&shared.annotated))
303}
304
305pub fn shared_plain_render_string(text: &str) -> std::sync::Arc<RenderString> {
309 with_shared_plain_text(text, |shared| {
310 std::sync::Arc::clone(
311 shared
312 .render
313 .get_or_init(|| std::sync::Arc::new(shared.annotated.render_string())),
314 )
315 })
316}
317
318struct SharedPlainText {
319 annotated: Rc<AnnotatedString>,
320 render: std::cell::OnceCell<std::sync::Arc<RenderString>>,
321}
322
323fn with_shared_plain_text<R>(text: &str, read: impl FnOnce(&SharedPlainText) -> R) -> R {
324 use std::{
325 cell::RefCell,
326 collections::HashMap,
327 hash::{Hash, Hasher},
328 };
329
330 const POOL_CAPACITY: usize = 256;
331 thread_local! {
332 static POOL: RefCell<HashMap<u64, SharedPlainText>> = RefCell::new(HashMap::new());
333 }
334
335 let mut hasher = cranpose_ui_graphics::FxHasher::default();
336 text.hash(&mut hasher);
337 let key = hasher.finish();
338
339 POOL.with(|pool| {
340 let mut pool = pool.borrow_mut();
341 if let Some(shared) = pool.get(&key)
342 && shared.annotated.text == text
343 {
344 return read(shared);
345 }
346 if pool.len() >= POOL_CAPACITY {
347 pool.clear();
348 }
349 let shared = pool.entry(key).insert_entry(SharedPlainText {
350 annotated: Rc::new(AnnotatedString::new(text.to_owned())),
351 render: std::cell::OnceCell::new(),
352 });
353 read(shared.get())
354 })
355}
356
357impl AnnotatedString {
358 pub fn new(text: String) -> Self {
359 Self {
360 text,
361 span_styles: vec![],
362 paragraph_styles: vec![],
363 string_annotations: vec![],
364 link_annotations: vec![],
365 }
366 }
367
368 pub fn builder() -> Builder {
369 Builder::new()
370 }
371
372 pub fn len(&self) -> usize {
373 self.text.len()
374 }
375
376 pub fn is_empty(&self) -> bool {
377 self.text.is_empty()
378 }
379
380 pub fn span_boundaries(&self) -> SpanBoundaries {
382 span_boundaries_impl(&self.text, &self.span_styles)
383 }
384
385 pub fn render_string(&self) -> RenderString {
390 RenderString::from_parts(
391 self.text.clone(),
392 self.span_styles.clone(),
393 self.paragraph_styles.clone(),
394 self.string_annotations.clone(),
395 self.link_annotations
396 .iter()
397 .map(|link| RangeStyle {
398 item: match &link.item {
399 LinkAnnotation::Url(url) => LinkKey::Url(url.clone()),
400 LinkAnnotation::Clickable { tag, .. } => LinkKey::Clickable(tag.clone()),
401 },
402 range: link.range.clone(),
403 })
404 .collect(),
405 )
406 }
407
408 pub fn span_styles_hash(&self) -> u64 {
410 use std::hash::{Hash, Hasher};
411 let mut hasher = cranpose_ui_graphics::FxHasher::default();
412 hasher.write_usize(self.span_styles.len());
413 for span in &self.span_styles {
414 hasher.write_usize(span.range.start);
415 hasher.write_usize(span.range.end);
416
417 let dummy = crate::text::TextStyle {
418 span_style: span.item.clone(),
419 ..Default::default()
420 };
421 hasher.write_u64(dummy.measurement_hash());
422
423 if let Some(c) = &span.item.color {
424 hasher.write_u32(c.0.to_bits());
425 hasher.write_u32(c.1.to_bits());
426 hasher.write_u32(c.2.to_bits());
427 hasher.write_u32(c.3.to_bits());
428 }
429 if let Some(bg) = &span.item.background {
430 hasher.write_u32(bg.0.to_bits());
431 hasher.write_u32(bg.1.to_bits());
432 hasher.write_u32(bg.2.to_bits());
433 hasher.write_u32(bg.3.to_bits());
434 }
435 if let Some(d) = &span.item.text_decoration {
436 d.hash(&mut hasher);
437 }
438 }
439 hasher.finish()
440 }
441
442 pub fn render_hash(&self) -> u64 {
443 render_hash_impl(&self.text, &self.span_styles, &self.paragraph_styles)
444 }
445
446 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
449 if range.is_empty() {
450 return Self::new(String::new());
451 }
452
453 let start = range.start.min(self.text.len());
454 let end = range.end.max(start).min(self.text.len());
455
456 if start == end {
457 return Self::new(String::new());
458 }
459
460 let mut new_spans = Vec::new();
461 for span in &self.span_styles {
462 let intersection_start = span.range.start.max(start);
463 let intersection_end = span.range.end.min(end);
464 if intersection_start < intersection_end {
465 new_spans.push(RangeStyle {
466 item: span.item.clone(),
467 range: (intersection_start - start)..(intersection_end - start),
468 });
469 }
470 }
471
472 let mut new_paragraphs = Vec::new();
473 for span in &self.paragraph_styles {
474 let intersection_start = span.range.start.max(start);
475 let intersection_end = span.range.end.min(end);
476 if intersection_start < intersection_end {
477 new_paragraphs.push(RangeStyle {
478 item: span.item.clone(),
479 range: (intersection_start - start)..(intersection_end - start),
480 });
481 }
482 }
483
484 let mut new_string_annotations = Vec::new();
485 for ann in &self.string_annotations {
486 let intersection_start = ann.range.start.max(start);
487 let intersection_end = ann.range.end.min(end);
488 if intersection_start < intersection_end {
489 new_string_annotations.push(RangeStyle {
490 item: ann.item.clone(),
491 range: (intersection_start - start)..(intersection_end - start),
492 });
493 }
494 }
495
496 let mut new_link_annotations = Vec::new();
497 for ann in &self.link_annotations {
498 let intersection_start = ann.range.start.max(start);
499 let intersection_end = ann.range.end.min(end);
500 if intersection_start < intersection_end {
501 new_link_annotations.push(RangeStyle {
502 item: ann.item.clone(),
503 range: (intersection_start - start)..(intersection_end - start),
504 });
505 }
506 }
507
508 Self {
509 text: self.text[start..end].to_string(),
510 span_styles: new_spans,
511 paragraph_styles: new_paragraphs,
512 string_annotations: new_string_annotations,
513 link_annotations: new_link_annotations,
514 }
515 }
516
517 pub fn get_string_annotations(
521 &self,
522 tag: &str,
523 start: usize,
524 end: usize,
525 ) -> Vec<&RangeStyle<StringAnnotation>> {
526 self.string_annotations
527 .iter()
528 .filter(|ann| ann.item.tag == tag && ann.range.start < end && ann.range.end > start)
529 .collect()
530 }
531
532 pub fn get_link_annotations(
536 &self,
537 start: usize,
538 end: usize,
539 ) -> Vec<&RangeStyle<LinkAnnotation>> {
540 self.link_annotations
541 .iter()
542 .filter(|ann| ann.range.start < end && ann.range.end > start)
543 .collect()
544 }
545}
546
547impl From<String> for AnnotatedString {
548 fn from(text: String) -> Self {
549 Self::new(text)
550 }
551}
552
553impl From<&str> for AnnotatedString {
554 fn from(text: &str) -> Self {
555 Self::new(text.to_owned())
556 }
557}
558
559impl From<&String> for AnnotatedString {
560 fn from(text: &String) -> Self {
561 Self::new(text.clone())
562 }
563}
564
565impl From<&mut String> for AnnotatedString {
566 fn from(text: &mut String) -> Self {
567 Self::new(text.clone())
568 }
569}
570
571#[derive(Debug, Default, Clone)]
573pub struct Builder {
574 text: String,
575 span_styles: Vec<MutableRange<SpanStyle>>,
576 paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
577 string_annotations: Vec<MutableRange<StringAnnotation>>,
578 link_annotations: Vec<MutableRange<LinkAnnotation>>,
579 style_stack: Vec<StyleStackRecord>,
580}
581
582#[derive(Debug, Clone)]
583struct MutableRange<T> {
584 item: T,
585 start: usize,
586 end: usize,
587}
588
589#[derive(Debug, Clone)]
590struct StyleStackRecord {
591 style_type: StyleType,
592 index: usize,
593}
594
595#[derive(Debug, Clone, Copy, PartialEq, Eq)]
596enum StyleType {
597 Span,
598 Paragraph,
599 StringAnnotation,
600 LinkAnnotation,
601}
602
603fn clamp_subsequence_range(text: &str, range: Range<usize>) -> Range<usize> {
604 let start = range.start.min(text.len());
605 let end = range.end.max(start).min(text.len());
606 start..end
607}
608
609fn append_clipped_ranges<T: Clone>(
610 target: &mut Vec<MutableRange<T>>,
611 source: &[RangeStyle<T>],
612 source_range: Range<usize>,
613 target_offset: usize,
614) {
615 for style in source {
616 let intersection_start = style.range.start.max(source_range.start);
617 let intersection_end = style.range.end.min(source_range.end);
618 if intersection_start < intersection_end {
619 target.push(MutableRange {
620 item: style.item.clone(),
621 start: (intersection_start - source_range.start) + target_offset,
622 end: (intersection_end - source_range.start) + target_offset,
623 });
624 }
625 }
626}
627
628impl Builder {
629 pub fn new() -> Self {
630 Self::default()
631 }
632
633 pub fn append(mut self, text: &str) -> Self {
635 self.text.push_str(text);
636 self
637 }
638
639 pub fn append_annotated(self, annotated: &AnnotatedString) -> Self {
640 self.append_annotated_subsequence(annotated, 0..annotated.text.len())
641 }
642
643 pub fn append_annotated_subsequence(
644 mut self,
645 annotated: &AnnotatedString,
646 range: Range<usize>,
647 ) -> Self {
648 let range = clamp_subsequence_range(annotated.text.as_str(), range);
649 if range.is_empty() {
650 return self;
651 }
652
653 debug_assert!(annotated.text.is_char_boundary(range.start));
654 debug_assert!(annotated.text.is_char_boundary(range.end));
655
656 let target_offset = self.text.len();
657 self.text.push_str(&annotated.text[range.clone()]);
658 append_clipped_ranges(
659 &mut self.span_styles,
660 &annotated.span_styles,
661 range.clone(),
662 target_offset,
663 );
664 append_clipped_ranges(
665 &mut self.paragraph_styles,
666 &annotated.paragraph_styles,
667 range.clone(),
668 target_offset,
669 );
670 append_clipped_ranges(
671 &mut self.string_annotations,
672 &annotated.string_annotations,
673 range.clone(),
674 target_offset,
675 );
676 append_clipped_ranges(
677 &mut self.link_annotations,
678 &annotated.link_annotations,
679 range,
680 target_offset,
681 );
682 self
683 }
684
685 pub fn push_style(mut self, style: SpanStyle) -> Self {
689 let index = self.span_styles.len();
690 self.span_styles.push(MutableRange {
691 item: style,
692 start: self.text.len(),
693 end: usize::MAX,
694 });
695 self.style_stack.push(StyleStackRecord {
696 style_type: StyleType::Span,
697 index,
698 });
699 self
700 }
701
702 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
704 let index = self.paragraph_styles.len();
705 self.paragraph_styles.push(MutableRange {
706 item: style,
707 start: self.text.len(),
708 end: usize::MAX,
709 });
710 self.style_stack.push(StyleStackRecord {
711 style_type: StyleType::Paragraph,
712 index,
713 });
714 self
715 }
716
717 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
721 let index = self.string_annotations.len();
722 self.string_annotations.push(MutableRange {
723 item: StringAnnotation {
724 tag: tag.to_string(),
725 annotation: annotation.to_string(),
726 },
727 start: self.text.len(),
728 end: usize::MAX,
729 });
730 self.style_stack.push(StyleStackRecord {
731 style_type: StyleType::StringAnnotation,
732 index,
733 });
734 self
735 }
736
737 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
742 let index = self.link_annotations.len();
743 self.link_annotations.push(MutableRange {
744 item: link,
745 start: self.text.len(),
746 end: usize::MAX,
747 });
748 self.style_stack.push(StyleStackRecord {
749 style_type: StyleType::LinkAnnotation,
750 index,
751 });
752 self
753 }
754
755 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
770 let b = self.push_link(link);
771 let b = block(b);
772 b.pop()
773 }
774
775 pub fn pop(mut self) -> Self {
777 if let Some(record) = self.style_stack.pop() {
778 match record.style_type {
779 StyleType::Span => {
780 self.span_styles[record.index].end = self.text.len();
781 }
782 StyleType::Paragraph => {
783 self.paragraph_styles[record.index].end = self.text.len();
784 }
785 StyleType::StringAnnotation => {
786 self.string_annotations[record.index].end = self.text.len();
787 }
788 StyleType::LinkAnnotation => {
789 self.link_annotations[record.index].end = self.text.len();
790 }
791 }
792 }
793 self
794 }
795
796 pub fn to_annotated_string(mut self) -> AnnotatedString {
798 while let Some(record) = self.style_stack.pop() {
799 match record.style_type {
800 StyleType::Span => {
801 self.span_styles[record.index].end = self.text.len();
802 }
803 StyleType::Paragraph => {
804 self.paragraph_styles[record.index].end = self.text.len();
805 }
806 StyleType::StringAnnotation => {
807 self.string_annotations[record.index].end = self.text.len();
808 }
809 StyleType::LinkAnnotation => {
810 self.link_annotations[record.index].end = self.text.len();
811 }
812 }
813 }
814
815 AnnotatedString {
816 text: self.text,
817 span_styles: self
818 .span_styles
819 .into_iter()
820 .map(|s| RangeStyle {
821 item: s.item,
822 range: s.start..s.end,
823 })
824 .collect(),
825 paragraph_styles: self
826 .paragraph_styles
827 .into_iter()
828 .map(|s| RangeStyle {
829 item: s.item,
830 range: s.start..s.end,
831 })
832 .collect(),
833 string_annotations: self
834 .string_annotations
835 .into_iter()
836 .map(|s| RangeStyle {
837 item: s.item,
838 range: s.start..s.end,
839 })
840 .collect(),
841 link_annotations: self
842 .link_annotations
843 .into_iter()
844 .map(|s| RangeStyle {
845 item: s.item,
846 range: s.start..s.end,
847 })
848 .collect(),
849 }
850 }
851}
852
853#[cfg(test)]
854#[path = "tests/annotated_string_tests.rs"]
855mod tests;