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) -> Vec<usize> {
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
228fn span_boundaries_impl(text: &str, span_styles: &[RangeStyle<SpanStyle>]) -> Vec<usize> {
229 let mut boundaries = vec![0, text.len()];
230 for span in span_styles {
231 boundaries.push(span.range.start);
232 boundaries.push(span.range.end);
233 }
234 boundaries.sort_unstable();
235 boundaries.dedup();
236 boundaries
237 .into_iter()
238 .filter(|&b| b <= text.len() && text.is_char_boundary(b))
239 .collect()
240}
241
242fn render_hash_impl(
243 text: &str,
244 span_styles: &[RangeStyle<SpanStyle>],
245 paragraph_styles: &[RangeStyle<ParagraphStyle>],
246) -> u64 {
247 use std::hash::{Hash, Hasher};
248
249 let mut hasher = cranpose_ui_graphics::FxHasher::default();
250 text.hash(&mut hasher);
251 span_styles.len().hash(&mut hasher);
252 for span in span_styles {
253 span.range.start.hash(&mut hasher);
254 span.range.end.hash(&mut hasher);
255 span.item.render_hash().hash(&mut hasher);
256 }
257 paragraph_styles.len().hash(&mut hasher);
258 for paragraph in paragraph_styles {
259 paragraph.range.start.hash(&mut hasher);
260 paragraph.range.end.hash(&mut hasher);
261 paragraph.item.render_hash().hash(&mut hasher);
262 }
263 hasher.finish()
264}
265
266#[derive(Debug, Clone, PartialEq, Default)]
270pub struct AnnotatedString {
271 pub text: String,
272 pub span_styles: Vec<RangeStyle<SpanStyle>>,
273 pub paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
274 pub string_annotations: Vec<RangeStyle<StringAnnotation>>,
277 pub link_annotations: Vec<RangeStyle<LinkAnnotation>>,
280}
281
282#[derive(Debug, Clone, PartialEq)]
284pub struct RangeStyle<T> {
285 pub item: T,
286 pub range: Range<usize>,
287}
288
289pub fn shared_plain_annotated_string(text: &str) -> Rc<AnnotatedString> {
300 with_shared_plain_text(text, |shared| Rc::clone(&shared.annotated))
301}
302
303pub fn shared_plain_render_string(text: &str) -> std::sync::Arc<RenderString> {
307 with_shared_plain_text(text, |shared| {
308 std::sync::Arc::clone(
309 shared
310 .render
311 .get_or_init(|| std::sync::Arc::new(shared.annotated.render_string())),
312 )
313 })
314}
315
316struct SharedPlainText {
317 annotated: Rc<AnnotatedString>,
318 render: std::cell::OnceCell<std::sync::Arc<RenderString>>,
319}
320
321fn with_shared_plain_text<R>(text: &str, read: impl FnOnce(&SharedPlainText) -> R) -> R {
322 use std::{
323 cell::RefCell,
324 collections::HashMap,
325 hash::{Hash, Hasher},
326 };
327
328 const POOL_CAPACITY: usize = 256;
329 thread_local! {
330 static POOL: RefCell<HashMap<u64, SharedPlainText>> = RefCell::new(HashMap::new());
331 }
332
333 let mut hasher = cranpose_ui_graphics::FxHasher::default();
334 text.hash(&mut hasher);
335 let key = hasher.finish();
336
337 POOL.with(|pool| {
338 let mut pool = pool.borrow_mut();
339 if let Some(shared) = pool.get(&key)
340 && shared.annotated.text == text
341 {
342 return read(shared);
343 }
344 if pool.len() >= POOL_CAPACITY {
345 pool.clear();
346 }
347 let shared = pool.entry(key).insert_entry(SharedPlainText {
348 annotated: Rc::new(AnnotatedString::new(text.to_owned())),
349 render: std::cell::OnceCell::new(),
350 });
351 read(shared.get())
352 })
353}
354
355impl AnnotatedString {
356 pub fn new(text: String) -> Self {
357 Self {
358 text,
359 span_styles: vec![],
360 paragraph_styles: vec![],
361 string_annotations: vec![],
362 link_annotations: vec![],
363 }
364 }
365
366 pub fn builder() -> Builder {
367 Builder::new()
368 }
369
370 pub fn len(&self) -> usize {
371 self.text.len()
372 }
373
374 pub fn is_empty(&self) -> bool {
375 self.text.is_empty()
376 }
377
378 pub fn span_boundaries(&self) -> Vec<usize> {
380 span_boundaries_impl(&self.text, &self.span_styles)
381 }
382
383 pub fn render_string(&self) -> RenderString {
388 RenderString::from_parts(
389 self.text.clone(),
390 self.span_styles.clone(),
391 self.paragraph_styles.clone(),
392 self.string_annotations.clone(),
393 self.link_annotations
394 .iter()
395 .map(|link| RangeStyle {
396 item: match &link.item {
397 LinkAnnotation::Url(url) => LinkKey::Url(url.clone()),
398 LinkAnnotation::Clickable { tag, .. } => LinkKey::Clickable(tag.clone()),
399 },
400 range: link.range.clone(),
401 })
402 .collect(),
403 )
404 }
405
406 pub fn span_styles_hash(&self) -> u64 {
408 use std::hash::{Hash, Hasher};
409 let mut hasher = cranpose_ui_graphics::FxHasher::default();
410 hasher.write_usize(self.span_styles.len());
411 for span in &self.span_styles {
412 hasher.write_usize(span.range.start);
413 hasher.write_usize(span.range.end);
414
415 let dummy = crate::text::TextStyle {
416 span_style: span.item.clone(),
417 ..Default::default()
418 };
419 hasher.write_u64(dummy.measurement_hash());
420
421 if let Some(c) = &span.item.color {
422 hasher.write_u32(c.0.to_bits());
423 hasher.write_u32(c.1.to_bits());
424 hasher.write_u32(c.2.to_bits());
425 hasher.write_u32(c.3.to_bits());
426 }
427 if let Some(bg) = &span.item.background {
428 hasher.write_u32(bg.0.to_bits());
429 hasher.write_u32(bg.1.to_bits());
430 hasher.write_u32(bg.2.to_bits());
431 hasher.write_u32(bg.3.to_bits());
432 }
433 if let Some(d) = &span.item.text_decoration {
434 d.hash(&mut hasher);
435 }
436 }
437 hasher.finish()
438 }
439
440 pub fn render_hash(&self) -> u64 {
441 render_hash_impl(&self.text, &self.span_styles, &self.paragraph_styles)
442 }
443
444 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
447 if range.is_empty() {
448 return Self::new(String::new());
449 }
450
451 let start = range.start.min(self.text.len());
452 let end = range.end.max(start).min(self.text.len());
453
454 if start == end {
455 return Self::new(String::new());
456 }
457
458 let mut new_spans = Vec::new();
459 for span in &self.span_styles {
460 let intersection_start = span.range.start.max(start);
461 let intersection_end = span.range.end.min(end);
462 if intersection_start < intersection_end {
463 new_spans.push(RangeStyle {
464 item: span.item.clone(),
465 range: (intersection_start - start)..(intersection_end - start),
466 });
467 }
468 }
469
470 let mut new_paragraphs = Vec::new();
471 for span in &self.paragraph_styles {
472 let intersection_start = span.range.start.max(start);
473 let intersection_end = span.range.end.min(end);
474 if intersection_start < intersection_end {
475 new_paragraphs.push(RangeStyle {
476 item: span.item.clone(),
477 range: (intersection_start - start)..(intersection_end - start),
478 });
479 }
480 }
481
482 let mut new_string_annotations = Vec::new();
483 for ann in &self.string_annotations {
484 let intersection_start = ann.range.start.max(start);
485 let intersection_end = ann.range.end.min(end);
486 if intersection_start < intersection_end {
487 new_string_annotations.push(RangeStyle {
488 item: ann.item.clone(),
489 range: (intersection_start - start)..(intersection_end - start),
490 });
491 }
492 }
493
494 let mut new_link_annotations = Vec::new();
495 for ann in &self.link_annotations {
496 let intersection_start = ann.range.start.max(start);
497 let intersection_end = ann.range.end.min(end);
498 if intersection_start < intersection_end {
499 new_link_annotations.push(RangeStyle {
500 item: ann.item.clone(),
501 range: (intersection_start - start)..(intersection_end - start),
502 });
503 }
504 }
505
506 Self {
507 text: self.text[start..end].to_string(),
508 span_styles: new_spans,
509 paragraph_styles: new_paragraphs,
510 string_annotations: new_string_annotations,
511 link_annotations: new_link_annotations,
512 }
513 }
514
515 pub fn get_string_annotations(
519 &self,
520 tag: &str,
521 start: usize,
522 end: usize,
523 ) -> Vec<&RangeStyle<StringAnnotation>> {
524 self.string_annotations
525 .iter()
526 .filter(|ann| ann.item.tag == tag && ann.range.start < end && ann.range.end > start)
527 .collect()
528 }
529
530 pub fn get_link_annotations(
534 &self,
535 start: usize,
536 end: usize,
537 ) -> Vec<&RangeStyle<LinkAnnotation>> {
538 self.link_annotations
539 .iter()
540 .filter(|ann| ann.range.start < end && ann.range.end > start)
541 .collect()
542 }
543}
544
545impl From<String> for AnnotatedString {
546 fn from(text: String) -> Self {
547 Self::new(text)
548 }
549}
550
551impl From<&str> for AnnotatedString {
552 fn from(text: &str) -> Self {
553 Self::new(text.to_owned())
554 }
555}
556
557impl From<&String> for AnnotatedString {
558 fn from(text: &String) -> Self {
559 Self::new(text.clone())
560 }
561}
562
563impl From<&mut String> for AnnotatedString {
564 fn from(text: &mut String) -> Self {
565 Self::new(text.clone())
566 }
567}
568
569#[derive(Debug, Default, Clone)]
571pub struct Builder {
572 text: String,
573 span_styles: Vec<MutableRange<SpanStyle>>,
574 paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
575 string_annotations: Vec<MutableRange<StringAnnotation>>,
576 link_annotations: Vec<MutableRange<LinkAnnotation>>,
577 style_stack: Vec<StyleStackRecord>,
578}
579
580#[derive(Debug, Clone)]
581struct MutableRange<T> {
582 item: T,
583 start: usize,
584 end: usize,
585}
586
587#[derive(Debug, Clone)]
588struct StyleStackRecord {
589 style_type: StyleType,
590 index: usize,
591}
592
593#[derive(Debug, Clone, Copy, PartialEq, Eq)]
594enum StyleType {
595 Span,
596 Paragraph,
597 StringAnnotation,
598 LinkAnnotation,
599}
600
601fn clamp_subsequence_range(text: &str, range: Range<usize>) -> Range<usize> {
602 let start = range.start.min(text.len());
603 let end = range.end.max(start).min(text.len());
604 start..end
605}
606
607fn append_clipped_ranges<T: Clone>(
608 target: &mut Vec<MutableRange<T>>,
609 source: &[RangeStyle<T>],
610 source_range: Range<usize>,
611 target_offset: usize,
612) {
613 for style in source {
614 let intersection_start = style.range.start.max(source_range.start);
615 let intersection_end = style.range.end.min(source_range.end);
616 if intersection_start < intersection_end {
617 target.push(MutableRange {
618 item: style.item.clone(),
619 start: (intersection_start - source_range.start) + target_offset,
620 end: (intersection_end - source_range.start) + target_offset,
621 });
622 }
623 }
624}
625
626impl Builder {
627 pub fn new() -> Self {
628 Self::default()
629 }
630
631 pub fn append(mut self, text: &str) -> Self {
633 self.text.push_str(text);
634 self
635 }
636
637 pub fn append_annotated(self, annotated: &AnnotatedString) -> Self {
638 self.append_annotated_subsequence(annotated, 0..annotated.text.len())
639 }
640
641 pub fn append_annotated_subsequence(
642 mut self,
643 annotated: &AnnotatedString,
644 range: Range<usize>,
645 ) -> Self {
646 let range = clamp_subsequence_range(annotated.text.as_str(), range);
647 if range.is_empty() {
648 return self;
649 }
650
651 debug_assert!(annotated.text.is_char_boundary(range.start));
652 debug_assert!(annotated.text.is_char_boundary(range.end));
653
654 let target_offset = self.text.len();
655 self.text.push_str(&annotated.text[range.clone()]);
656 append_clipped_ranges(
657 &mut self.span_styles,
658 &annotated.span_styles,
659 range.clone(),
660 target_offset,
661 );
662 append_clipped_ranges(
663 &mut self.paragraph_styles,
664 &annotated.paragraph_styles,
665 range.clone(),
666 target_offset,
667 );
668 append_clipped_ranges(
669 &mut self.string_annotations,
670 &annotated.string_annotations,
671 range.clone(),
672 target_offset,
673 );
674 append_clipped_ranges(
675 &mut self.link_annotations,
676 &annotated.link_annotations,
677 range,
678 target_offset,
679 );
680 self
681 }
682
683 pub fn push_style(mut self, style: SpanStyle) -> Self {
687 let index = self.span_styles.len();
688 self.span_styles.push(MutableRange {
689 item: style,
690 start: self.text.len(),
691 end: usize::MAX,
692 });
693 self.style_stack.push(StyleStackRecord {
694 style_type: StyleType::Span,
695 index,
696 });
697 self
698 }
699
700 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
702 let index = self.paragraph_styles.len();
703 self.paragraph_styles.push(MutableRange {
704 item: style,
705 start: self.text.len(),
706 end: usize::MAX,
707 });
708 self.style_stack.push(StyleStackRecord {
709 style_type: StyleType::Paragraph,
710 index,
711 });
712 self
713 }
714
715 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
719 let index = self.string_annotations.len();
720 self.string_annotations.push(MutableRange {
721 item: StringAnnotation {
722 tag: tag.to_string(),
723 annotation: annotation.to_string(),
724 },
725 start: self.text.len(),
726 end: usize::MAX,
727 });
728 self.style_stack.push(StyleStackRecord {
729 style_type: StyleType::StringAnnotation,
730 index,
731 });
732 self
733 }
734
735 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
740 let index = self.link_annotations.len();
741 self.link_annotations.push(MutableRange {
742 item: link,
743 start: self.text.len(),
744 end: usize::MAX,
745 });
746 self.style_stack.push(StyleStackRecord {
747 style_type: StyleType::LinkAnnotation,
748 index,
749 });
750 self
751 }
752
753 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
768 let b = self.push_link(link);
769 let b = block(b);
770 b.pop()
771 }
772
773 pub fn pop(mut self) -> Self {
775 if let Some(record) = self.style_stack.pop() {
776 match record.style_type {
777 StyleType::Span => {
778 self.span_styles[record.index].end = self.text.len();
779 }
780 StyleType::Paragraph => {
781 self.paragraph_styles[record.index].end = self.text.len();
782 }
783 StyleType::StringAnnotation => {
784 self.string_annotations[record.index].end = self.text.len();
785 }
786 StyleType::LinkAnnotation => {
787 self.link_annotations[record.index].end = self.text.len();
788 }
789 }
790 }
791 self
792 }
793
794 pub fn to_annotated_string(mut self) -> AnnotatedString {
796 while let Some(record) = self.style_stack.pop() {
797 match record.style_type {
798 StyleType::Span => {
799 self.span_styles[record.index].end = self.text.len();
800 }
801 StyleType::Paragraph => {
802 self.paragraph_styles[record.index].end = self.text.len();
803 }
804 StyleType::StringAnnotation => {
805 self.string_annotations[record.index].end = self.text.len();
806 }
807 StyleType::LinkAnnotation => {
808 self.link_annotations[record.index].end = self.text.len();
809 }
810 }
811 }
812
813 AnnotatedString {
814 text: self.text,
815 span_styles: self
816 .span_styles
817 .into_iter()
818 .map(|s| RangeStyle {
819 item: s.item,
820 range: s.start..s.end,
821 })
822 .collect(),
823 paragraph_styles: self
824 .paragraph_styles
825 .into_iter()
826 .map(|s| RangeStyle {
827 item: s.item,
828 range: s.start..s.end,
829 })
830 .collect(),
831 string_annotations: self
832 .string_annotations
833 .into_iter()
834 .map(|s| RangeStyle {
835 item: s.item,
836 range: s.start..s.end,
837 })
838 .collect(),
839 link_annotations: self
840 .link_annotations
841 .into_iter()
842 .map(|s| RangeStyle {
843 item: s.item,
844 range: s.start..s.end,
845 })
846 .collect(),
847 }
848 }
849}
850
851#[cfg(test)]
852#[path = "tests/annotated_string_tests.rs"]
853mod tests;