1use crate::{
2 ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
3 HighlightStyle, Hitbox, HitboxBehavior, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent,
4 MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow, TextRun, TextStyle, TooltipId,
5 TruncateFrom, WhiteSpace, Window, WrappedLine, WrappedLineLayout,
6 register_tooltip_mouse_handlers, set_tooltip_on_window,
7};
8use anyhow::Context as _;
9use gpui_util::ResultExt;
10use itertools::Itertools;
11use smallvec::SmallVec;
12use std::{
13 borrow::Cow,
14 cell::{Cell, RefCell},
15 mem,
16 ops::{Deref, DerefMut, Range},
17 rc::Rc,
18 sync::Arc,
19};
20
21#[derive(Debug, Clone)]
67pub struct Text {
68 id: Option<ElementId>,
69 text: SharedString,
70}
71
72impl Text {
73 #[inline]
79 pub const fn new(id: ElementId, text: SharedString) -> Self {
80 Self { id: Some(id), text }
81 }
82
83 #[inline]
93 pub const fn new_inaccessible(text: SharedString) -> Self {
94 Self { id: None, text }
95 }
96
97 #[inline]
99 pub const fn id(&self) -> Option<&ElementId> {
100 self.id.as_ref()
101 }
102
103 pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
105 self.id = Some(id.into());
106 self
107 }
108
109 #[inline]
111 pub const fn text(&self) -> &SharedString {
112 &self.text
113 }
114}
115
116impl Deref for Text {
117 type Target = SharedString;
118 fn deref(&self) -> &Self::Target {
119 &self.text
120 }
121}
122
123impl DerefMut for Text {
124 fn deref_mut(&mut self) -> &mut Self::Target {
125 &mut self.text
126 }
127}
128
129#[doc(hidden)]
133pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 {
134 const BASIS: u64 = 0xcbf29ce484222325;
135 const PRIME: u64 = 0x100000001b3;
136
137 let bytes = s.as_bytes();
138 let mut hash = BASIS;
139 let mut i = 0;
140 while i < bytes.len() {
141 hash ^= bytes[i] as u64;
142 hash = hash.wrapping_mul(PRIME);
143 i += 1;
144 }
145 hash
146}
147
148#[macro_export]
161macro_rules! text {
162 (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }};
163 ($text:expr) => {{
164 const ID: &'static str = concat!(file!(), "/", line!(), ":", column!());
165 const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID);
166 $crate::Text::new($crate::ElementId::Integer(HASH), $text.into())
167 }};
168}
169
170impl IntoElement for Text {
171 type Element = Self;
172 #[inline]
173 fn into_element(self) -> Self::Element {
174 self
175 }
176}
177
178impl Element for Text {
179 type RequestLayoutState = TextLayout;
180 type PrepaintState = ();
181
182 fn id(&self) -> Option<ElementId> {
183 self.id.clone()
184 }
185
186 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
187 None
188 }
189
190 fn a11y_role(&self) -> Option<accesskit::Role> {
191 if self.id.is_some() {
192 Some(accesskit::Role::Label)
193 } else {
194 None
195 }
196 }
197
198 fn write_a11y_info(&self, node: &mut accesskit::Node) {
199 node.set_value(self.text.to_string());
200 }
201
202 fn request_layout(
203 &mut self,
204 id: Option<&GlobalElementId>,
205 window: &mut Window,
206 cx: &mut App,
207 ) -> (LayoutId, Self::RequestLayoutState) {
208 <SharedString as Element>::request_layout(&mut self.text, id, window, cx)
209 }
210
211 fn prepaint(
212 &mut self,
213 id: Option<&GlobalElementId>,
214 bounds: Bounds<Pixels>,
215 request_layout: &mut Self::RequestLayoutState,
216 window: &mut Window,
217 cx: &mut App,
218 ) -> Self::PrepaintState {
219 <SharedString as Element>::prepaint(&mut self.text, id, bounds, request_layout, window, cx)
220 }
221
222 fn paint(
223 &mut self,
224 id: Option<&GlobalElementId>,
225 bounds: Bounds<Pixels>,
226 request_layout: &mut Self::RequestLayoutState,
227 prepaint: &mut Self::PrepaintState,
228 window: &mut Window,
229 cx: &mut App,
230 ) {
231 <SharedString as Element>::paint(
232 &mut self.text,
233 id,
234 bounds,
235 request_layout,
236 prepaint,
237 window,
238 cx,
239 );
240 }
241}
242
243impl Element for &'static str {
244 type RequestLayoutState = TextLayout;
245 type PrepaintState = ();
246
247 fn id(&self) -> Option<ElementId> {
248 None
249 }
250
251 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
252 None
253 }
254
255 fn request_layout(
256 &mut self,
257 _id: Option<&GlobalElementId>,
258 window: &mut Window,
259 cx: &mut App,
260 ) -> (LayoutId, Self::RequestLayoutState) {
261 let mut state = TextLayout::default();
262 let layout_id = state.layout(SharedString::from(*self), None, window, cx);
263 (layout_id, state)
264 }
265
266 fn prepaint(
267 &mut self,
268 _id: Option<&GlobalElementId>,
269 bounds: Bounds<Pixels>,
270 text_layout: &mut Self::RequestLayoutState,
271 _window: &mut Window,
272 _cx: &mut App,
273 ) {
274 text_layout.prepaint(bounds, self)
275 }
276
277 fn paint(
278 &mut self,
279 _id: Option<&GlobalElementId>,
280 _bounds: Bounds<Pixels>,
281 text_layout: &mut TextLayout,
282 _: &mut (),
283 window: &mut Window,
284 cx: &mut App,
285 ) {
286 text_layout.paint(self, window, cx)
287 }
288}
289
290impl IntoElement for &'static str {
291 type Element = Self;
292
293 fn into_element(self) -> Self::Element {
294 self
295 }
296}
297
298impl IntoElement for String {
299 type Element = SharedString;
300
301 fn into_element(self) -> Self::Element {
302 self.into()
303 }
304}
305
306impl IntoElement for Cow<'static, str> {
307 type Element = SharedString;
308
309 fn into_element(self) -> Self::Element {
310 self.into()
311 }
312}
313
314impl Element for SharedString {
315 type RequestLayoutState = TextLayout;
316 type PrepaintState = ();
317
318 fn id(&self) -> Option<ElementId> {
319 None
320 }
321
322 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
323 None
324 }
325
326 fn request_layout(
327 &mut self,
328 _id: Option<&GlobalElementId>,
329 window: &mut Window,
330 cx: &mut App,
331 ) -> (LayoutId, Self::RequestLayoutState) {
332 let mut state = TextLayout::default();
333 let layout_id = state.layout(self.clone(), None, window, cx);
334 (layout_id, state)
335 }
336
337 fn prepaint(
338 &mut self,
339 _id: Option<&GlobalElementId>,
340 bounds: Bounds<Pixels>,
341 text_layout: &mut Self::RequestLayoutState,
342 _window: &mut Window,
343 _cx: &mut App,
344 ) {
345 text_layout.prepaint(bounds, self.as_ref())
346 }
347
348 fn paint(
349 &mut self,
350 _id: Option<&GlobalElementId>,
351 _bounds: Bounds<Pixels>,
352 text_layout: &mut Self::RequestLayoutState,
353 _: &mut Self::PrepaintState,
354 window: &mut Window,
355 cx: &mut App,
356 ) {
357 text_layout.paint(self.as_ref(), window, cx)
358 }
359}
360
361impl IntoElement for SharedString {
362 type Element = Self;
363
364 fn into_element(self) -> Self::Element {
365 self
366 }
367}
368
369pub struct StyledText {
375 text: SharedString,
376 runs: Option<Vec<TextRun>>,
377 delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
378 delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
379 layout: TextLayout,
380}
381
382impl StyledText {
383 pub fn new(text: impl Into<SharedString>) -> Self {
385 StyledText {
386 text: text.into(),
387 runs: None,
388 delayed_highlights: None,
389 delayed_font_family_overrides: None,
390 layout: TextLayout::default(),
391 }
392 }
393
394 pub fn layout(&self) -> &TextLayout {
396 &self.layout
397 }
398
399 pub fn with_default_highlights(
402 mut self,
403 default_style: &TextStyle,
404 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
405 ) -> Self {
406 debug_assert!(
407 self.delayed_highlights.is_none(),
408 "Can't use `with_default_highlights` and `with_highlights`"
409 );
410 let runs = Self::compute_runs(&self.text, default_style, highlights);
411 self.with_runs(runs)
412 }
413
414 pub fn with_highlights(
417 mut self,
418 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
419 ) -> Self {
420 debug_assert!(
421 self.runs.is_none(),
422 "Can't use `with_highlights` and `with_default_highlights`"
423 );
424 self.delayed_highlights = Some(
425 highlights
426 .into_iter()
427 .inspect(|(run, _)| {
428 debug_assert!(self.text.is_char_boundary(run.start));
429 debug_assert!(self.text.is_char_boundary(run.end));
430 })
431 .collect::<Vec<_>>(),
432 );
433 self
434 }
435
436 fn compute_runs(
437 text: &str,
438 default_style: &TextStyle,
439 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
440 ) -> Vec<TextRun> {
441 let mut runs = Vec::new();
442 let mut ix = 0;
443 for (range, highlight) in highlights {
444 if ix < range.start {
445 debug_assert!(text.is_char_boundary(range.start));
446 runs.push(default_style.clone().to_run(range.start - ix));
447 }
448 debug_assert!(text.is_char_boundary(range.end));
449 runs.push(
450 default_style
451 .clone()
452 .highlight(highlight)
453 .to_run(range.len()),
454 );
455 ix = range.end;
456 }
457 if ix < text.len() {
458 runs.push(default_style.to_run(text.len() - ix));
459 }
460 runs
461 }
462
463 pub fn with_font_family_overrides(
472 mut self,
473 overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
474 ) -> Self {
475 self.delayed_font_family_overrides = Some(
476 overrides
477 .into_iter()
478 .inspect(|(range, _)| {
479 debug_assert!(self.text.is_char_boundary(range.start));
480 debug_assert!(self.text.is_char_boundary(range.end));
481 })
482 .collect(),
483 );
484 self
485 }
486
487 fn apply_font_family_overrides(
488 runs: &mut [TextRun],
489 overrides: &[(Range<usize>, SharedString)],
490 ) {
491 let mut byte_offset = 0;
492 let mut override_idx = 0;
493 for run in runs.iter_mut() {
494 let run_end = byte_offset + run.len;
495 while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
496 override_idx += 1;
497 }
498 if override_idx < overrides.len() {
499 let (ref range, ref family) = overrides[override_idx];
500 if byte_offset >= range.start && run_end <= range.end {
501 run.font.family = family.clone();
502 }
503 }
504 byte_offset = run_end;
505 }
506 }
507
508 pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
510 let mut text = &*self.text;
511 for run in &runs {
512 text = text.get(run.len..).unwrap_or_else(|| {
513 #[cfg(debug_assertions)]
514 panic!("invalid text run. Text: '{text}', run: {run:?}");
515 #[cfg(not(debug_assertions))]
516 panic!("invalid text run");
517 });
518 }
519 assert!(text.is_empty(), "invalid text run");
520 self.runs = Some(runs);
521 self
522 }
523}
524
525impl Element for StyledText {
526 type RequestLayoutState = ();
527 type PrepaintState = ();
528
529 fn id(&self) -> Option<ElementId> {
530 None
531 }
532
533 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
534 None
535 }
536
537 fn request_layout(
538 &mut self,
539 _id: Option<&GlobalElementId>,
540 window: &mut Window,
541 cx: &mut App,
542 ) -> (LayoutId, Self::RequestLayoutState) {
543 let font_family_overrides = self.delayed_font_family_overrides.take();
544 let mut runs = self.runs.take().or_else(|| {
545 self.delayed_highlights.take().map(|delayed_highlights| {
546 Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
547 })
548 });
549
550 if let Some(ref overrides) = font_family_overrides {
551 let runs =
552 runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
553 Self::apply_font_family_overrides(runs, overrides);
554 }
555
556 let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
557 (layout_id, ())
558 }
559
560 fn prepaint(
561 &mut self,
562 _id: Option<&GlobalElementId>,
563 bounds: Bounds<Pixels>,
564 _: &mut Self::RequestLayoutState,
565 _window: &mut Window,
566 _cx: &mut App,
567 ) {
568 self.layout.prepaint(bounds, &self.text)
569 }
570
571 fn paint(
572 &mut self,
573 _id: Option<&GlobalElementId>,
574 _bounds: Bounds<Pixels>,
575 _: &mut Self::RequestLayoutState,
576 _: &mut Self::PrepaintState,
577 window: &mut Window,
578 cx: &mut App,
579 ) {
580 self.layout.paint(&self.text, window, cx)
581 }
582}
583
584impl IntoElement for StyledText {
585 type Element = Self;
586
587 fn into_element(self) -> Self::Element {
588 self
589 }
590}
591
592#[derive(Default, Clone)]
594pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
595
596#[derive(Clone, Copy)]
597enum TextPaintPass {
598 All,
599 Background,
600 Foreground,
601}
602
603struct TextLayoutInner {
604 len: usize,
605 lines: SmallVec<[WrappedLine; 1]>,
606 line_height: Pixels,
607 wrap_width: Option<Pixels>,
608 truncate_width: Option<Pixels>,
609 size: Option<Size<Pixels>>,
610 bounds: Option<Bounds<Pixels>>,
611}
612
613impl TextLayout {
614 fn layout(
615 &self,
616 text: SharedString,
617 runs: Option<Vec<TextRun>>,
618 window: &mut Window,
619 _: &mut App,
620 ) -> LayoutId {
621 let text_style = window.text_style();
622 let font_size = text_style.font_size.to_pixels(window.rem_size());
623 let line_height = window.pixel_snap(
624 text_style
625 .line_height
626 .to_pixels(font_size.into(), window.rem_size()),
627 );
628
629 let runs = if let Some(runs) = runs {
630 runs
631 } else {
632 vec![text_style.to_run(text.len())]
633 };
634 window.request_measured_layout(Default::default(), {
635 let element_state = self.clone();
636
637 move |known_dimensions, available_space, window, cx| {
638 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
639 known_dimensions.width.or(match available_space.width {
640 crate::AvailableSpace::Definite(x) => Some(x),
641 _ => None,
642 })
643 } else {
644 None
645 };
646
647 let (truncate_width, truncation_affix, truncate_from) =
648 if let Some(text_overflow) = text_style.text_overflow.clone() {
649 let width = known_dimensions.width.or(match available_space.width {
650 crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
651 Some(max_lines) => Some(x * max_lines),
652 None => Some(x),
653 },
654 _ => None,
655 });
656
657 match text_overflow {
658 TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
659 TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
660 TextOverflow::TruncateMiddle(s) => (width, s, TruncateFrom::Middle),
661 }
662 } else {
663 (None, "".into(), TruncateFrom::End)
664 };
665
666 if let Some(text_layout) = element_state.0.borrow().as_ref()
675 && let Some(size) = text_layout.size
676 && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
677 && truncate_width.is_none()
678 && text_layout.truncate_width.is_none()
679 {
680 return size;
681 }
682
683 let mut line_wrapper = cx
684 .text_system()
685 .clone()
686 .line_wrapper(text_style.font(), font_size);
687 let (text, runs) = if let Some(truncate_width) = truncate_width {
688 if let Some(max_lines) = text_style.line_clamp
689 && let Some(wrap_width) = wrap_width
690 {
691 line_wrapper.truncate_wrapped_line(
692 text.clone(),
693 wrap_width,
694 max_lines,
695 &truncation_affix,
696 &runs,
697 truncate_from,
698 )
699 } else if let Some(unclipped) = window
700 .text_system()
701 .shape_text(text.clone(), font_size, &runs, None, None)
702 .log_err()
703 && unclipped
704 .iter()
705 .all(|line| line.size(line_height).width <= truncate_width)
706 {
707 (text.clone(), Cow::Borrowed(&*runs))
714 } else {
715 line_wrapper.truncate_line(
716 text.clone(),
717 truncate_width,
718 &truncation_affix,
719 &runs,
720 truncate_from,
721 )
722 }
723 } else {
724 (text.clone(), Cow::Borrowed(&*runs))
725 };
726 let len = text.len();
727
728 let Some(lines) = window
729 .text_system()
730 .shape_text(
731 text,
732 font_size,
733 &runs,
734 wrap_width, text_style.line_clamp, )
737 .log_err()
738 else {
739 element_state.0.borrow_mut().replace(TextLayoutInner {
740 lines: Default::default(),
741 len: 0,
742 line_height,
743 wrap_width,
744 truncate_width,
745 size: Some(Size::default()),
746 bounds: None,
747 });
748 return Size::default();
749 };
750
751 let mut size: Size<Pixels> = Size::default();
752 for line in &lines {
753 let line_size = line.size(line_height);
754 size.height += line_size.height;
755 size.width = size.width.max(line_size.width).ceil();
756 }
757
758 element_state.0.borrow_mut().replace(TextLayoutInner {
759 lines,
760 len,
761 line_height,
762 wrap_width,
763 truncate_width,
764 size: Some(size),
765 bounds: None,
766 });
767
768 size
769 }
770 })
771 }
772
773 fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
774 let mut element_state = self.0.borrow_mut();
775 let element_state = element_state
776 .as_mut()
777 .with_context(|| format!("measurement has not been performed on {text}"))
778 .unwrap();
779 element_state.bounds = Some(bounds);
780 }
781
782 fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
783 self.paint_lines(TextPaintPass::All, window, cx)
784 .with_context(|| format!("failed to paint {text}"))
785 .unwrap();
786 }
787
788 pub fn paint_background(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
792 self.paint_lines(TextPaintPass::Background, window, cx)
793 }
794
795 pub fn paint_foreground(&self, window: &mut Window, cx: &mut App) -> anyhow::Result<()> {
797 self.paint_lines(TextPaintPass::Foreground, window, cx)
798 }
799
800 fn paint_lines(
801 &self,
802 pass: TextPaintPass,
803 window: &mut Window,
804 cx: &mut App,
805 ) -> anyhow::Result<()> {
806 let element_state = self.0.borrow();
807 let element_state = element_state
808 .as_ref()
809 .context("measurement has not been performed")?;
810 let bounds = element_state
811 .bounds
812 .context("prepaint has not been performed")?;
813
814 let line_height = element_state.line_height;
815 let mut line_origin = bounds.origin;
816 let text_style = window.text_style();
817 for line in &element_state.lines {
818 if matches!(pass, TextPaintPass::All | TextPaintPass::Background) {
819 line.paint_background(
820 line_origin,
821 line_height,
822 text_style.text_align,
823 Some(bounds),
824 window,
825 cx,
826 )
827 .log_err();
828 }
829 if matches!(pass, TextPaintPass::All | TextPaintPass::Foreground) {
830 line.paint(
831 line_origin,
832 line_height,
833 text_style.text_align,
834 Some(bounds),
835 window,
836 cx,
837 )
838 .log_err();
839 }
840 line_origin.y += line.size(line_height).height;
841 }
842 Ok(())
843 }
844
845 pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
847 let element_state = self.0.borrow();
848 let element_state = element_state
849 .as_ref()
850 .expect("measurement has not been performed");
851 let bounds = element_state
852 .bounds
853 .expect("prepaint has not been performed");
854
855 if position.y < bounds.top() {
856 return Err(0);
857 }
858
859 let line_height = element_state.line_height;
860 let mut line_origin = bounds.origin;
861 let mut line_start_ix = 0;
862 for line in &element_state.lines {
863 let line_bottom = line_origin.y + line.size(line_height).height;
864 if position.y > line_bottom {
865 line_origin.y = line_bottom;
866 line_start_ix += line.len() + 1;
867 } else {
868 let position_within_line = position - line_origin;
869 match line.index_for_position(position_within_line, line_height) {
870 Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
871 Err(index_within_line) => return Err(line_start_ix + index_within_line),
872 }
873 }
874 }
875
876 Err(line_start_ix.saturating_sub(1))
877 }
878
879 pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
881 let element_state = self.0.borrow();
882 let element_state = element_state
883 .as_ref()
884 .expect("measurement has not been performed");
885 let bounds = element_state
886 .bounds
887 .expect("prepaint has not been performed");
888 let line_height = element_state.line_height;
889
890 let mut line_origin = bounds.origin;
891 let mut line_start_ix = 0;
892
893 for line in &element_state.lines {
894 let line_end_ix = line_start_ix + line.len();
895 if index < line_start_ix {
896 break;
897 } else if index > line_end_ix {
898 line_origin.y += line.size(line_height).height;
899 line_start_ix = line_end_ix + 1;
900 continue;
901 } else {
902 let ix_within_line = index - line_start_ix;
903 return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
904 }
905 }
906
907 None
908 }
909
910 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
912 let element_state = self.0.borrow();
913 let element_state = element_state
914 .as_ref()
915 .expect("measurement has not been performed");
916 let mut line_start_ix = 0;
917
918 for line in &element_state.lines {
919 let line_end_ix = line_start_ix + line.len();
920 if index < line_start_ix {
921 break;
922 } else if index > line_end_ix {
923 line_start_ix = line_end_ix + 1;
924 continue;
925 } else {
926 return Some(line.layout.clone());
927 }
928 }
929
930 None
931 }
932
933 pub fn line_layouts(&self) -> SmallVec<[Arc<WrappedLineLayout>; 1]> {
935 self.0
936 .borrow()
937 .as_ref()
938 .expect("measurement has not been performed")
939 .lines
940 .iter()
941 .map(|line| line.layout.clone())
942 .collect()
943 }
944
945 pub fn bounds(&self) -> Bounds<Pixels> {
947 self.0.borrow().as_ref().unwrap().bounds.unwrap()
948 }
949
950 pub fn line_height(&self) -> Pixels {
952 self.0.borrow().as_ref().unwrap().line_height
953 }
954
955 pub fn len(&self) -> usize {
957 self.0.borrow().as_ref().unwrap().len
958 }
959
960 pub fn text(&self) -> String {
962 self.0
963 .borrow()
964 .as_ref()
965 .unwrap()
966 .lines
967 .iter()
968 .map(|s| &s.text)
969 .join("\n")
970 }
971
972 pub fn wrapped_text(&self) -> String {
974 let mut accumulator = String::new();
975
976 for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
977 let mut seen = 0;
978 for boundary in wrapped.layout.wrap_boundaries.iter() {
979 let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
980 [boundary.glyph_ix]
981 .index;
982
983 accumulator.push_str(&wrapped.text[seen..index]);
984 accumulator.push('\n');
985 seen = index;
986 }
987 accumulator.push_str(&wrapped.text[seen..]);
988 accumulator.push('\n');
989 }
990 accumulator.pop();
992 accumulator
993 }
994}
995
996pub struct InteractiveText {
998 element_id: ElementId,
999 text: StyledText,
1000 click_listener:
1001 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
1002 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
1003 tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
1004 tooltip_id: Option<TooltipId>,
1005 clickable_ranges: Vec<Range<usize>>,
1006}
1007
1008struct InteractiveTextClickEvent {
1009 mouse_down_index: usize,
1010 mouse_up_index: usize,
1011}
1012
1013#[doc(hidden)]
1014#[derive(Default)]
1015pub struct InteractiveTextState {
1016 mouse_down_index: Rc<Cell<Option<usize>>>,
1017 hovered_index: Rc<Cell<Option<usize>>>,
1018 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
1019 long_press_tooltip_active: Rc<Cell<bool>>,
1020}
1021
1022impl InteractiveText {
1024 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
1026 Self {
1027 element_id: id.into(),
1028 text,
1029 click_listener: None,
1030 hover_listener: None,
1031 tooltip_builder: None,
1032 tooltip_id: None,
1033 clickable_ranges: Vec::new(),
1034 }
1035 }
1036
1037 pub fn on_click(
1040 mut self,
1041 ranges: Vec<Range<usize>>,
1042 listener: impl Fn(usize, &mut Window, &mut App) + 'static,
1043 ) -> Self {
1044 self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
1045 for (range_ix, range) in ranges.iter().enumerate() {
1046 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
1047 {
1048 listener(range_ix, window, cx);
1049 }
1050 }
1051 }));
1052 self.clickable_ranges = ranges;
1053 self
1054 }
1055
1056 pub fn on_hover(
1059 mut self,
1060 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
1061 ) -> Self {
1062 self.hover_listener = Some(Box::new(listener));
1063 self
1064 }
1065
1066 pub fn tooltip(
1068 mut self,
1069 builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
1070 ) -> Self {
1071 self.tooltip_builder = Some(Rc::new(builder));
1072 self
1073 }
1074}
1075
1076impl Element for InteractiveText {
1077 type RequestLayoutState = ();
1078 type PrepaintState = Hitbox;
1079
1080 fn id(&self) -> Option<ElementId> {
1081 Some(self.element_id.clone())
1082 }
1083
1084 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1085 None
1086 }
1087
1088 fn a11y_role(&self) -> Option<accesskit::Role> {
1089 Some(accesskit::Role::Label)
1090 }
1091
1092 fn write_a11y_info(&self, node: &mut accesskit::Node) {
1093 node.set_value(self.text.text.to_string());
1094 }
1095
1096 fn request_layout(
1097 &mut self,
1098 _id: Option<&GlobalElementId>,
1099 window: &mut Window,
1100 cx: &mut App,
1101 ) -> (LayoutId, Self::RequestLayoutState) {
1102 self.text.request_layout(None, window, cx)
1103 }
1104
1105 fn prepaint(
1106 &mut self,
1107 global_id: Option<&GlobalElementId>,
1108 bounds: Bounds<Pixels>,
1109 state: &mut Self::RequestLayoutState,
1110 window: &mut Window,
1111 cx: &mut App,
1112 ) -> Hitbox {
1113 window.with_optional_element_state::<InteractiveTextState, _>(
1114 global_id,
1115 |interactive_state, window| {
1116 let mut interactive_state = interactive_state
1117 .map(|interactive_state| interactive_state.unwrap_or_default());
1118
1119 if let Some(interactive_state) = interactive_state.as_mut() {
1120 if self.tooltip_builder.is_some() {
1121 self.tooltip_id =
1122 set_tooltip_on_window(&interactive_state.active_tooltip, window);
1123 } else {
1124 interactive_state.long_press_tooltip_active.set(false);
1126 interactive_state.active_tooltip.take();
1127 }
1128 }
1129
1130 self.text.prepaint(None, bounds, state, window, cx);
1131 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1132 (hitbox, interactive_state)
1133 },
1134 )
1135 }
1136
1137 fn paint(
1138 &mut self,
1139 global_id: Option<&GlobalElementId>,
1140 bounds: Bounds<Pixels>,
1141 _: &mut Self::RequestLayoutState,
1142 hitbox: &mut Hitbox,
1143 window: &mut Window,
1144 cx: &mut App,
1145 ) {
1146 let current_view = window.current_view();
1147 let text_layout = self.text.layout().clone();
1148 window.with_element_state::<InteractiveTextState, _>(
1149 global_id.unwrap(),
1150 |interactive_state, window| {
1151 let mut interactive_state = interactive_state.unwrap_or_default();
1152 if let Some(click_listener) = self.click_listener.take() {
1153 let mouse_position = window.mouse_position();
1154 if let Ok(ix) = text_layout.index_for_position(mouse_position)
1155 && self
1156 .clickable_ranges
1157 .iter()
1158 .any(|range| range.contains(&ix))
1159 {
1160 window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
1161 }
1162
1163 let text_layout = text_layout.clone();
1164 let mouse_down = interactive_state.mouse_down_index.clone();
1165 if let Some(mouse_down_index) = mouse_down.get() {
1166 let hitbox = hitbox.clone();
1167 let clickable_ranges = mem::take(&mut self.clickable_ranges);
1168 window.on_mouse_event(
1169 move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
1170 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1171 if let Ok(mouse_up_index) =
1172 text_layout.index_for_position(event.position)
1173 {
1174 click_listener(
1175 &clickable_ranges,
1176 InteractiveTextClickEvent {
1177 mouse_down_index,
1178 mouse_up_index,
1179 },
1180 window,
1181 cx,
1182 )
1183 }
1184
1185 mouse_down.take();
1186 window.refresh();
1187 }
1188 },
1189 );
1190 } else {
1191 let hitbox = hitbox.clone();
1192 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
1193 if phase == DispatchPhase::Bubble
1194 && hitbox.is_hovered(window)
1195 && let Ok(mouse_down_index) =
1196 text_layout.index_for_position(event.position)
1197 {
1198 mouse_down.set(Some(mouse_down_index));
1199 window.refresh();
1200 }
1201 });
1202 }
1203 }
1204
1205 window.on_mouse_event({
1206 let mut hover_listener = self.hover_listener.take();
1207 let hitbox = hitbox.clone();
1208 let text_layout = text_layout.clone();
1209 let hovered_index = interactive_state.hovered_index.clone();
1210 move |event: &MouseMoveEvent, phase, window, cx| {
1211 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1212 let current = hovered_index.get();
1213 let updated = text_layout.index_for_position(event.position).ok();
1214 if current != updated {
1215 hovered_index.set(updated);
1216 if let Some(hover_listener) = hover_listener.as_ref() {
1217 hover_listener(updated, event.clone(), window, cx);
1218 }
1219 cx.notify(current_view);
1220 }
1221 }
1222 }
1223 });
1224
1225 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
1226 let active_tooltip = interactive_state.active_tooltip.clone();
1227 let build_tooltip = Rc::new({
1228 let tooltip_is_hoverable = false;
1229 let text_layout = text_layout.clone();
1230 move |window: &mut Window, cx: &mut App| {
1231 text_layout
1232 .index_for_position(window.mouse_position())
1233 .ok()
1234 .and_then(|position| tooltip_builder(position, window, cx))
1235 .map(|view| (view, tooltip_is_hoverable))
1236 }
1237 });
1238
1239 let check_is_hovered_during_prepaint = Rc::new({
1241 let source_bounds = hitbox.bounds;
1242 let text_layout = text_layout.clone();
1243 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1244 move |window: &Window| {
1245 text_layout
1246 .index_for_position(window.mouse_position())
1247 .is_ok()
1248 && source_bounds.contains(&window.mouse_position())
1249 && pending_mouse_down.get().is_none()
1250 }
1251 });
1252
1253 let check_is_hovered = Rc::new({
1254 let hitbox = hitbox.clone();
1255 let text_layout = text_layout.clone();
1256 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1257 move |window: &Window| {
1258 text_layout
1259 .index_for_position(window.mouse_position())
1260 .is_ok()
1261 && hitbox.is_hovered(window)
1262 && pending_mouse_down.get().is_none()
1263 }
1264 });
1265
1266 register_tooltip_mouse_handlers(
1267 &active_tooltip,
1268 self.tooltip_id,
1269 build_tooltip,
1270 check_is_hovered,
1271 check_is_hovered_during_prepaint,
1272 interactive_state.long_press_tooltip_active.clone(),
1273 None,
1274 window,
1275 );
1276 }
1277
1278 self.text.paint(None, bounds, &mut (), &mut (), window, cx);
1279
1280 ((), interactive_state)
1281 },
1282 );
1283 }
1284}
1285
1286impl IntoElement for InteractiveText {
1287 type Element = Self;
1288
1289 fn into_element(self) -> Self::Element {
1290 self
1291 }
1292}
1293
1294#[cfg(test)]
1295mod tests {
1296 use super::*;
1297
1298 #[test]
1299 fn test_into_element_for() {
1300 use crate::{ParentElement as _, SharedString, div};
1301 use std::borrow::Cow;
1302
1303 let _ = div().child("static str");
1304 let _ = div().child("String".to_string());
1305 let _ = div().child(Cow::Borrowed("Cow"));
1306 let _ = div().child(SharedString::from("SharedString"));
1307 }
1308
1309 #[test]
1310 fn text_macro_id() {
1311 fn make_text_stable_id(happy: bool) -> Text {
1313 text!(if happy { "happy" } else { "sad" })
1314 }
1315
1316 fn make_text_unstable_id(happy: bool) -> Text {
1318 if happy { text!("happy") } else { text!("sad") }
1319 }
1320
1321 assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
1322 assert_ne!(
1323 make_text_unstable_id(false).id,
1324 make_text_unstable_id(true).id
1325 );
1326 }
1327}