1use std::{ops::Range, rc::Rc};
2
3use crate::{ParagraphStyle, SpanStyle};
4
5#[derive(Clone)]
26pub enum LinkAnnotation {
27 Url(String),
31
32 Clickable {
36 tag: String,
38 handler: Rc<dyn Fn()>,
40 },
41}
42
43impl std::fmt::Debug for LinkAnnotation {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Url(url) => f.debug_tuple("Url").field(url).finish(),
47 Self::Clickable { tag, .. } => f.debug_struct("Clickable").field("tag", tag).finish(),
48 }
49 }
50}
51
52impl PartialEq for LinkAnnotation {
53 fn eq(&self, other: &Self) -> bool {
54 match (self, other) {
55 (Self::Url(a), Self::Url(b)) => a == b,
56 (
57 Self::Clickable {
58 tag: ta,
59 handler: ha,
60 },
61 Self::Clickable {
62 tag: tb,
63 handler: hb,
64 },
65 ) => ta == tb && Rc::ptr_eq(ha, hb),
66 _ => false,
67 }
68 }
69}
70
71#[derive(Debug, Clone, PartialEq)]
76pub struct StringAnnotation {
77 pub tag: String,
80 pub annotation: String,
82}
83
84#[derive(Debug, Clone, PartialEq)]
88pub enum LinkKey {
89 Url(String),
91 Clickable(String),
93}
94
95#[derive(Debug, Clone, PartialEq)]
104pub struct RenderString {
105 text: String,
106 span_styles: Vec<RangeStyle<SpanStyle>>,
107 paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
108 string_annotations: Vec<RangeStyle<StringAnnotation>>,
109 links: Vec<RangeStyle<LinkKey>>,
110 hash: u64,
111}
112
113impl Default for RenderString {
114 fn default() -> Self {
115 Self::from_parts(
116 String::new(),
117 Vec::new(),
118 Vec::new(),
119 Vec::new(),
120 Vec::new(),
121 )
122 }
123}
124
125const _: () = {
126 fn assert_send<T: Send + Sync>() {}
127 #[expect(dead_code)]
128 fn assert_render_string_is_send_sync() {
129 assert_send::<RenderString>();
130 }
131};
132
133impl RenderString {
134 fn from_parts(
135 text: String,
136 span_styles: Vec<RangeStyle<SpanStyle>>,
137 paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
138 string_annotations: Vec<RangeStyle<StringAnnotation>>,
139 links: Vec<RangeStyle<LinkKey>>,
140 ) -> Self {
141 let hash = render_hash_impl(&text, &span_styles, ¶graph_styles);
142 Self {
143 text,
144 span_styles,
145 paragraph_styles,
146 string_annotations,
147 links,
148 hash,
149 }
150 }
151
152 pub fn text(&self) -> &str {
154 &self.text
155 }
156
157 pub fn span_styles(&self) -> &[RangeStyle<SpanStyle>] {
159 &self.span_styles
160 }
161
162 pub fn paragraph_styles(&self) -> &[RangeStyle<ParagraphStyle>] {
164 &self.paragraph_styles
165 }
166
167 pub fn string_annotations(&self) -> &[RangeStyle<StringAnnotation>] {
169 &self.string_annotations
170 }
171
172 pub fn links(&self) -> &[RangeStyle<LinkKey>] {
175 &self.links
176 }
177
178 pub fn len(&self) -> usize {
180 self.text.len()
181 }
182
183 pub fn is_empty(&self) -> bool {
185 self.text.is_empty()
186 }
187
188 pub fn span_boundaries(&self) -> SpanBoundaries {
192 span_boundaries_impl(&self.text, &self.span_styles)
193 }
194
195 pub fn render_hash(&self) -> u64 {
199 self.hash
200 }
201
202 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
206 let start = range.start.min(self.text.len());
207 let end = range.end.max(start).min(self.text.len());
208 if start == end {
209 return Self::default();
210 }
211
212 Self::from_parts(
213 self.text[start..end].to_string(),
214 clip_range_styles(&self.span_styles, start, end),
215 clip_range_styles(&self.paragraph_styles, start, end),
216 clip_range_styles(&self.string_annotations, start, end),
217 clip_range_styles(&self.links, start, end),
218 )
219 }
220}
221
222fn clip_range_styles<T: Clone>(
223 styles: &[RangeStyle<T>],
224 start: usize,
225 end: usize,
226) -> Vec<RangeStyle<T>> {
227 let mut clipped = Vec::new();
228 for style in styles {
229 let intersection_start = style.range.start.max(start);
230 let intersection_end = style.range.end.min(end);
231 if intersection_start < intersection_end {
232 clipped.push(RangeStyle {
233 item: style.item.clone(),
234 range: (intersection_start - start)..(intersection_end - start),
235 });
236 }
237 }
238 clipped
239}
240
241pub type SpanBoundaries = smallvec::SmallVec<[usize; 8]>;
244
245fn span_boundaries_impl(text: &str, span_styles: &[RangeStyle<SpanStyle>]) -> SpanBoundaries {
246 let mut boundaries: SpanBoundaries = smallvec::smallvec![0, text.len()];
247 for span in span_styles {
248 boundaries.push(span.range.start);
249 boundaries.push(span.range.end);
250 }
251 boundaries.sort_unstable();
252 boundaries.dedup();
253 boundaries.retain(|boundary| *boundary <= text.len() && text.is_char_boundary(*boundary));
254 boundaries
255}
256
257fn render_hash_impl(
258 text: &str,
259 span_styles: &[RangeStyle<SpanStyle>],
260 paragraph_styles: &[RangeStyle<ParagraphStyle>],
261) -> u64 {
262 use std::hash::{Hash, Hasher};
263
264 let mut hasher = cranpose_ui_graphics::FxHasher::default();
265 text.hash(&mut hasher);
266 span_styles.len().hash(&mut hasher);
267 for span in span_styles {
268 span.range.start.hash(&mut hasher);
269 span.range.end.hash(&mut hasher);
270 span.item.render_hash().hash(&mut hasher);
271 }
272 paragraph_styles.len().hash(&mut hasher);
273 for paragraph in paragraph_styles {
274 paragraph.range.start.hash(&mut hasher);
275 paragraph.range.end.hash(&mut hasher);
276 paragraph.item.render_hash().hash(&mut hasher);
277 }
278 hasher.finish()
279}
280
281#[derive(Debug, Clone, PartialEq, Default)]
285pub struct AnnotatedString {
286 pub text: String,
288 pub span_styles: Vec<RangeStyle<SpanStyle>>,
290 pub paragraph_styles: Vec<RangeStyle<ParagraphStyle>>,
292 pub string_annotations: Vec<RangeStyle<StringAnnotation>>,
295 pub link_annotations: Vec<RangeStyle<LinkAnnotation>>,
298}
299
300#[derive(Debug, Clone, PartialEq)]
302pub struct RangeStyle<T> {
303 pub item: T,
305 pub range: Range<usize>,
307}
308
309pub fn shared_plain_annotated_string(text: &str) -> Rc<AnnotatedString> {
320 with_shared_plain_text(text, |shared| Rc::clone(&shared.annotated))
321}
322
323pub fn shared_plain_render_string(text: &str) -> std::sync::Arc<RenderString> {
327 with_shared_plain_text(text, |shared| {
328 std::sync::Arc::clone(
329 shared
330 .render
331 .get_or_init(|| std::sync::Arc::new(shared.annotated.render_string())),
332 )
333 })
334}
335
336pub(crate) struct SharedPlainText {
337 annotated: Rc<AnnotatedString>,
338 render: std::cell::OnceCell<std::sync::Arc<RenderString>>,
339}
340
341fn with_shared_plain_text<R>(text: &str, read: impl FnOnce(&SharedPlainText) -> R) -> R {
342 use std::hash::{Hash, Hasher};
343
344 const POOL_CAPACITY: usize = 256;
345
346 let mut hasher = cranpose_ui_graphics::FxHasher::default();
347 text.hash(&mut hasher);
348 let key = hasher.finish();
349
350 crate::thread_state::with(|state| {
351 let mut pool = state.shared_plain_texts.borrow_mut();
352 if let Some(shared) = pool.get(&key)
353 && shared.annotated.text == text
354 {
355 return read(shared);
356 }
357 if pool.len() >= POOL_CAPACITY {
358 pool.clear();
359 }
360 let shared = pool.entry(key).insert_entry(SharedPlainText {
361 annotated: Rc::new(AnnotatedString::new(text.to_owned())),
362 render: std::cell::OnceCell::new(),
363 });
364 read(shared.get())
365 })
366}
367
368impl AnnotatedString {
369 pub fn new(text: String) -> Self {
371 Self {
372 text,
373 span_styles: vec![],
374 paragraph_styles: vec![],
375 string_annotations: vec![],
376 link_annotations: vec![],
377 }
378 }
379
380 pub fn builder() -> Builder {
382 Builder::new()
383 }
384
385 pub fn len(&self) -> usize {
387 self.text.len()
388 }
389
390 pub fn is_empty(&self) -> bool {
392 self.text.is_empty()
393 }
394
395 pub fn span_boundaries(&self) -> SpanBoundaries {
397 span_boundaries_impl(&self.text, &self.span_styles)
398 }
399
400 pub fn render_string(&self) -> RenderString {
405 RenderString::from_parts(
406 self.text.clone(),
407 self.span_styles.clone(),
408 self.paragraph_styles.clone(),
409 self.string_annotations.clone(),
410 self.link_annotations
411 .iter()
412 .map(|link| RangeStyle {
413 item: match &link.item {
414 LinkAnnotation::Url(url) => LinkKey::Url(url.clone()),
415 LinkAnnotation::Clickable { tag, .. } => LinkKey::Clickable(tag.clone()),
416 },
417 range: link.range.clone(),
418 })
419 .collect(),
420 )
421 }
422
423 pub fn span_measurement_hash(&self) -> u64 {
427 use std::hash::Hasher;
428 let mut hasher = cranpose_ui_graphics::FxHasher::default();
429 hasher.write_usize(self.span_styles.len());
430 for span in &self.span_styles {
431 hasher.write_usize(span.range.start);
432 hasher.write_usize(span.range.end);
433 crate::text::style::hash_span_measurement(&span.item, &mut hasher);
434 }
435 hasher.finish()
436 }
437
438 pub fn render_hash(&self) -> u64 {
440 render_hash_impl(&self.text, &self.span_styles, &self.paragraph_styles)
441 }
442
443 pub fn subsequence(&self, range: std::ops::Range<usize>) -> Self {
446 if range.is_empty() {
447 return Self::new(String::new());
448 }
449
450 let start = range.start.min(self.text.len());
451 let end = range.end.max(start).min(self.text.len());
452
453 if start == end {
454 return Self::new(String::new());
455 }
456
457 let mut new_spans = Vec::new();
458 for span in &self.span_styles {
459 let intersection_start = span.range.start.max(start);
460 let intersection_end = span.range.end.min(end);
461 if intersection_start < intersection_end {
462 new_spans.push(RangeStyle {
463 item: span.item.clone(),
464 range: (intersection_start - start)..(intersection_end - start),
465 });
466 }
467 }
468
469 let mut new_paragraphs = Vec::new();
470 for span in &self.paragraph_styles {
471 let intersection_start = span.range.start.max(start);
472 let intersection_end = span.range.end.min(end);
473 if intersection_start < intersection_end {
474 new_paragraphs.push(RangeStyle {
475 item: span.item.clone(),
476 range: (intersection_start - start)..(intersection_end - start),
477 });
478 }
479 }
480
481 let mut new_string_annotations = Vec::new();
482 for ann in &self.string_annotations {
483 let intersection_start = ann.range.start.max(start);
484 let intersection_end = ann.range.end.min(end);
485 if intersection_start < intersection_end {
486 new_string_annotations.push(RangeStyle {
487 item: ann.item.clone(),
488 range: (intersection_start - start)..(intersection_end - start),
489 });
490 }
491 }
492
493 let mut new_link_annotations = Vec::new();
494 for ann in &self.link_annotations {
495 let intersection_start = ann.range.start.max(start);
496 let intersection_end = ann.range.end.min(end);
497 if intersection_start < intersection_end {
498 new_link_annotations.push(RangeStyle {
499 item: ann.item.clone(),
500 range: (intersection_start - start)..(intersection_end - start),
501 });
502 }
503 }
504
505 Self {
506 text: self.text[start..end].to_string(),
507 span_styles: new_spans,
508 paragraph_styles: new_paragraphs,
509 string_annotations: new_string_annotations,
510 link_annotations: new_link_annotations,
511 }
512 }
513
514 pub fn get_string_annotations(
518 &self,
519 tag: &str,
520 start: usize,
521 end: usize,
522 ) -> Vec<&RangeStyle<StringAnnotation>> {
523 self.string_annotations
524 .iter()
525 .filter(|ann| ann.item.tag == tag && ann.range.start < end && ann.range.end > start)
526 .collect()
527 }
528
529 pub fn get_link_annotations(
533 &self,
534 start: usize,
535 end: usize,
536 ) -> Vec<&RangeStyle<LinkAnnotation>> {
537 self.link_annotations
538 .iter()
539 .filter(|ann| ann.range.start < end && ann.range.end > start)
540 .collect()
541 }
542}
543
544impl From<String> for AnnotatedString {
545 fn from(text: String) -> Self {
546 Self::new(text)
547 }
548}
549
550impl From<&str> for AnnotatedString {
551 fn from(text: &str) -> Self {
552 Self::new(text.to_owned())
553 }
554}
555
556impl From<&String> for AnnotatedString {
557 fn from(text: &String) -> Self {
558 Self::new(text.clone())
559 }
560}
561
562impl From<&mut String> for AnnotatedString {
563 fn from(text: &mut String) -> Self {
564 Self::new(text.clone())
565 }
566}
567
568#[derive(Debug, Default, Clone)]
570pub struct Builder {
571 text: String,
572 span_styles: Vec<MutableRange<SpanStyle>>,
573 paragraph_styles: Vec<MutableRange<ParagraphStyle>>,
574 string_annotations: Vec<MutableRange<StringAnnotation>>,
575 link_annotations: Vec<MutableRange<LinkAnnotation>>,
576 style_stack: Vec<StyleStackRecord>,
577}
578
579#[derive(Debug, Clone)]
580struct MutableRange<T> {
581 item: T,
582 start: usize,
583 end: usize,
584}
585
586#[derive(Debug, Clone)]
587struct StyleStackRecord {
588 style_type: StyleType,
589 index: usize,
590}
591
592#[derive(Debug, Clone, Copy, PartialEq, Eq)]
593enum StyleType {
594 Span,
595 Paragraph,
596 StringAnnotation,
597 LinkAnnotation,
598}
599
600fn clamp_subsequence_range(text: &str, range: Range<usize>) -> Range<usize> {
601 let start = range.start.min(text.len());
602 let end = range.end.max(start).min(text.len());
603 start..end
604}
605
606fn finished_ranges<T>(ranges: Vec<MutableRange<T>>) -> Vec<RangeStyle<T>> {
611 let mut finished: Vec<RangeStyle<T>> = ranges
612 .into_iter()
613 .map(|range| RangeStyle {
614 item: range.item,
615 range: range.start..range.end,
616 })
617 .collect();
618 finished.shrink_to_fit();
619 finished
620}
621
622fn append_clipped_ranges<T: Clone>(
623 target: &mut Vec<MutableRange<T>>,
624 source: &[RangeStyle<T>],
625 source_range: Range<usize>,
626 target_offset: usize,
627) {
628 for style in source {
629 let intersection_start = style.range.start.max(source_range.start);
630 let intersection_end = style.range.end.min(source_range.end);
631 if intersection_start < intersection_end {
632 target.push(MutableRange {
633 item: style.item.clone(),
634 start: (intersection_start - source_range.start) + target_offset,
635 end: (intersection_end - source_range.start) + target_offset,
636 });
637 }
638 }
639}
640
641impl Builder {
642 pub fn new() -> Self {
644 Self::default()
645 }
646
647 pub fn append(mut self, text: &str) -> Self {
649 self.text.push_str(text);
650 self
651 }
652
653 pub fn append_annotated(self, annotated: &AnnotatedString) -> Self {
655 self.append_annotated_subsequence(annotated, 0..annotated.text.len())
656 }
657
658 pub fn append_annotated_subsequence(
661 mut self,
662 annotated: &AnnotatedString,
663 range: Range<usize>,
664 ) -> Self {
665 let range = clamp_subsequence_range(annotated.text.as_str(), range);
666 if range.is_empty() {
667 return self;
668 }
669
670 debug_assert!(annotated.text.is_char_boundary(range.start));
671 debug_assert!(annotated.text.is_char_boundary(range.end));
672
673 let target_offset = self.text.len();
674 self.text.push_str(&annotated.text[range.clone()]);
675 append_clipped_ranges(
676 &mut self.span_styles,
677 &annotated.span_styles,
678 range.clone(),
679 target_offset,
680 );
681 append_clipped_ranges(
682 &mut self.paragraph_styles,
683 &annotated.paragraph_styles,
684 range.clone(),
685 target_offset,
686 );
687 append_clipped_ranges(
688 &mut self.string_annotations,
689 &annotated.string_annotations,
690 range.clone(),
691 target_offset,
692 );
693 append_clipped_ranges(
694 &mut self.link_annotations,
695 &annotated.link_annotations,
696 range,
697 target_offset,
698 );
699 self
700 }
701
702 pub fn push_style(mut self, style: SpanStyle) -> Self {
706 let index = self.span_styles.len();
707 self.span_styles.push(MutableRange {
708 item: style,
709 start: self.text.len(),
710 end: usize::MAX,
711 });
712 self.style_stack.push(StyleStackRecord {
713 style_type: StyleType::Span,
714 index,
715 });
716 self
717 }
718
719 pub fn push_paragraph_style(mut self, style: ParagraphStyle) -> Self {
721 let index = self.paragraph_styles.len();
722 self.paragraph_styles.push(MutableRange {
723 item: style,
724 start: self.text.len(),
725 end: usize::MAX,
726 });
727 self.style_stack.push(StyleStackRecord {
728 style_type: StyleType::Paragraph,
729 index,
730 });
731 self
732 }
733
734 pub fn push_string_annotation(mut self, tag: &str, annotation: &str) -> Self {
738 let index = self.string_annotations.len();
739 self.string_annotations.push(MutableRange {
740 item: StringAnnotation {
741 tag: tag.to_string(),
742 annotation: annotation.to_string(),
743 },
744 start: self.text.len(),
745 end: usize::MAX,
746 });
747 self.style_stack.push(StyleStackRecord {
748 style_type: StyleType::StringAnnotation,
749 index,
750 });
751 self
752 }
753
754 pub fn push_link(mut self, link: LinkAnnotation) -> Self {
759 let index = self.link_annotations.len();
760 self.link_annotations.push(MutableRange {
761 item: link,
762 start: self.text.len(),
763 end: usize::MAX,
764 });
765 self.style_stack.push(StyleStackRecord {
766 style_type: StyleType::LinkAnnotation,
767 index,
768 });
769 self
770 }
771
772 pub fn with_link(self, link: LinkAnnotation, block: impl FnOnce(Self) -> Self) -> Self {
787 let b = self.push_link(link);
788 let b = block(b);
789 b.pop()
790 }
791
792 pub fn pop(mut self) -> Self {
794 if let Some(record) = self.style_stack.pop() {
795 match record.style_type {
796 StyleType::Span => {
797 self.span_styles[record.index].end = self.text.len();
798 }
799 StyleType::Paragraph => {
800 self.paragraph_styles[record.index].end = self.text.len();
801 }
802 StyleType::StringAnnotation => {
803 self.string_annotations[record.index].end = self.text.len();
804 }
805 StyleType::LinkAnnotation => {
806 self.link_annotations[record.index].end = self.text.len();
807 }
808 }
809 }
810 self
811 }
812
813 pub fn to_annotated_string(mut self) -> AnnotatedString {
815 while let Some(record) = self.style_stack.pop() {
816 match record.style_type {
817 StyleType::Span => {
818 self.span_styles[record.index].end = self.text.len();
819 }
820 StyleType::Paragraph => {
821 self.paragraph_styles[record.index].end = self.text.len();
822 }
823 StyleType::StringAnnotation => {
824 self.string_annotations[record.index].end = self.text.len();
825 }
826 StyleType::LinkAnnotation => {
827 self.link_annotations[record.index].end = self.text.len();
828 }
829 }
830 }
831
832 AnnotatedString {
833 text: self.text,
834 span_styles: finished_ranges(self.span_styles),
835 paragraph_styles: finished_ranges(self.paragraph_styles),
836 string_annotations: finished_ranges(self.string_annotations),
837 link_annotations: finished_ranges(self.link_annotations),
838 }
839 }
840}
841
842#[cfg(test)]
843#[path = "tests/annotated_string_tests.rs"]
844mod tests;