Skip to repository content1314 lines · 45.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:42:47.670Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
text.rs
1use crate::{
2 ActiveTooltip, AnyView, App, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
3 HighlightStyle, Hitbox, HitboxBehavior, InspectorElementId, IntoElement, LayoutId,
4 MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextOverflow,
5 TextRun, TextStyle, TooltipId, TruncateFrom, WhiteSpace, Window, WrappedLine,
6 WrappedLineLayout, 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/// An [`Element`] that renders text.
22///
23/// In general, [`Text`] objects should be created via the [`text`] macro:
24/// ```rust
25/// # use gpui::*;
26/// # fn render() -> impl IntoElement {
27/// div().child(text!("hello"))
28/// # }
29/// ```
30/// ## IDs and Accessibility
31///
32/// [`Text`] elements have an ID. This ID is primarily used to produce nodes in
33/// the accessibility tree, which allows the text to be visible to screen
34/// readers and other assistive technologies.
35///
36/// This ID is stable across frames. If the same text, with the same ID, is
37/// present in two consecutive frames, no updates are reported to the screen
38/// reader. If the text changes, but the ID stays the same, then the screen
39/// reader will be notified that a text node's content has changed. **However**,
40/// if the ID changes, then the screen reader will be notified that a node has
41/// been removed, and a new node has been added.
42///
43/// When using the [`text`] macro, each invocation of the macro will get a
44/// unique ID, derived from its position in the source code (filename, line, and
45/// column). For example:
46/// ```rust
47/// # use gpui::*;
48/// let x = text!("hello");
49/// let y = text!("hello");
50/// // not equal, because different `text!` invocations produced them
51/// assert_ne!(x.id(), y.id());
52///
53/// fn make_text(s: &str) -> Text { text!(s) }
54/// let x = make_text("hello");
55/// let y = make_text("hello");
56/// // equal, because the same `text!` invocation produced them
57/// assert_eq!(x.id(), y.id());
58/// ```
59/// When the contents of an invocation of [`text`] do not change, this
60/// distinction is less relevant (with the caveat that you still need to take
61/// care to ensure that duplicate IDs do not appear).
62///
63/// However, when a [`text`] invocation's argument *does* change, you should
64/// consider whether this change should be reported as a node "updating its
65/// contents", or an old node being destroyed and a new node being created.
66#[derive(Debug, Clone)]
67pub struct Text {
68 id: Option<ElementId>,
69 text: SharedString,
70}
71
72impl Text {
73 /// Create a new [`Text`] element with a specific ID.
74 ///
75 /// If you want a unique ID to be assigned automatically, use the [`text`]
76 /// macro. The docs for [`Text`] have more detail about choosing IDs.
77 #[inline]
78 pub const fn new(id: ElementId, text: SharedString) -> Self {
79 Self { id: Some(id), text }
80 }
81
82 /// Create a new [`Text`] element that is inaccessible to screen readers.
83 ///
84 /// In order for text to be accessible to screen readers, it must have an ID
85 /// provided. If you want text to be accessible, either use [`text`] to have
86 /// an ID automatically assigned, or use [`Text::new`] to manually assign an
87 /// ID.
88 ///
89 /// This function is intended for use inside custom UI components, where
90 /// accessible properties may be set on parent containers.
91 #[inline]
92 pub const fn new_inaccessible(text: SharedString) -> Self {
93 Self { id: None, text }
94 }
95
96 /// The ID of this [`Text`] element.
97 #[inline]
98 pub const fn id(&self) -> Option<&ElementId> {
99 self.id.as_ref()
100 }
101
102 /// Produce a new [`Text`] with the given `id`.
103 pub fn with_id(mut self, id: impl Into<ElementId>) -> Self {
104 self.id = Some(id.into());
105 self
106 }
107
108 /// The text that this [`Text`] element will display.
109 #[inline]
110 pub const fn text(&self) -> &SharedString {
111 &self.text
112 }
113}
114
115impl Deref for Text {
116 type Target = SharedString;
117 fn deref(&self) -> &Self::Target {
118 &self.text
119 }
120}
121
122impl DerefMut for Text {
123 fn deref_mut(&mut self) -> &mut Self::Target {
124 &mut self.text
125 }
126}
127
128/// Trivial hash function for the location information produced by the [`text`]
129/// macro. Not covered by semver guarantees. Performance is not particularly
130/// significant because it's only used on small strings in const contexts.
131#[doc(hidden)]
132pub const fn __hash_text_macro_location_unstable_do_not_use(s: &'static str) -> u64 {
133 const BASIS: u64 = 0xcbf29ce484222325;
134 const PRIME: u64 = 0x100000001b3;
135
136 let bytes = s.as_bytes();
137 let mut hash = BASIS;
138 let mut i = 0;
139 while i < bytes.len() {
140 hash ^= bytes[i] as u64;
141 hash = hash.wrapping_mul(PRIME);
142 i += 1;
143 }
144 hash
145}
146
147/// Create a new [`Text`] element.
148///
149/// ```rust
150/// # use gpui::*;
151/// let a = text!("hello");
152/// let b = text!(id = "farewell-message", "hello");
153///
154/// ```
155///
156/// Text created with this macro is *accessible*. The macro generates an ID
157/// based on the source location. See the docs for [`Text`] for a more in-depth
158/// explanation of the significance of the ID of a [`Text`] element.
159#[macro_export]
160macro_rules! text {
161 (id = $id:expr, $text:expr) => {{ $crate::Text::new($id.into(), $text.into()) }};
162 ($text:expr) => {{
163 const ID: &'static str = concat!(file!(), "/", line!(), ":", column!());
164 const HASH: u64 = $crate::__hash_text_macro_location_unstable_do_not_use(ID);
165 $crate::Text::new($crate::ElementId::Integer(HASH), $text.into())
166 }};
167}
168
169impl IntoElement for Text {
170 type Element = Self;
171 #[inline]
172 fn into_element(self) -> Self::Element {
173 self
174 }
175}
176
177impl Element for Text {
178 type RequestLayoutState = TextLayout;
179 type PrepaintState = ();
180
181 fn id(&self) -> Option<ElementId> {
182 self.id.clone()
183 }
184
185 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
186 None
187 }
188
189 fn a11y_role(&self) -> Option<accesskit::Role> {
190 if self.id.is_some() {
191 Some(accesskit::Role::Label)
192 } else {
193 None
194 }
195 }
196
197 fn write_a11y_info(&self, node: &mut accesskit::Node) {
198 node.set_value(self.text.to_string());
199 }
200
201 fn request_layout(
202 &mut self,
203 id: Option<&GlobalElementId>,
204 inspector_id: Option<&InspectorElementId>,
205 window: &mut Window,
206 cx: &mut App,
207 ) -> (LayoutId, Self::RequestLayoutState) {
208 <SharedString as Element>::request_layout(&mut self.text, id, inspector_id, window, cx)
209 }
210
211 fn prepaint(
212 &mut self,
213 id: Option<&GlobalElementId>,
214 inspector_id: Option<&InspectorElementId>,
215 bounds: Bounds<Pixels>,
216 request_layout: &mut Self::RequestLayoutState,
217 window: &mut Window,
218 cx: &mut App,
219 ) -> Self::PrepaintState {
220 <SharedString as Element>::prepaint(
221 &mut self.text,
222 id,
223 inspector_id,
224 bounds,
225 request_layout,
226 window,
227 cx,
228 )
229 }
230
231 fn paint(
232 &mut self,
233 id: Option<&GlobalElementId>,
234 inspector_id: Option<&InspectorElementId>,
235 bounds: Bounds<Pixels>,
236 request_layout: &mut Self::RequestLayoutState,
237 prepaint: &mut Self::PrepaintState,
238 window: &mut Window,
239 cx: &mut App,
240 ) {
241 <SharedString as Element>::paint(
242 &mut self.text,
243 id,
244 inspector_id,
245 bounds,
246 request_layout,
247 prepaint,
248 window,
249 cx,
250 );
251 }
252}
253
254impl Element for &'static str {
255 type RequestLayoutState = TextLayout;
256 type PrepaintState = ();
257
258 fn id(&self) -> Option<ElementId> {
259 None
260 }
261
262 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
263 None
264 }
265
266 fn request_layout(
267 &mut self,
268 _id: Option<&GlobalElementId>,
269 _inspector_id: Option<&InspectorElementId>,
270 window: &mut Window,
271 cx: &mut App,
272 ) -> (LayoutId, Self::RequestLayoutState) {
273 let mut state = TextLayout::default();
274 let layout_id = state.layout(SharedString::from(*self), None, window, cx);
275 (layout_id, state)
276 }
277
278 fn prepaint(
279 &mut self,
280 _id: Option<&GlobalElementId>,
281 _inspector_id: Option<&InspectorElementId>,
282 bounds: Bounds<Pixels>,
283 text_layout: &mut Self::RequestLayoutState,
284 _window: &mut Window,
285 _cx: &mut App,
286 ) {
287 text_layout.prepaint(bounds, self)
288 }
289
290 fn paint(
291 &mut self,
292 _id: Option<&GlobalElementId>,
293 _inspector_id: Option<&InspectorElementId>,
294 _bounds: Bounds<Pixels>,
295 text_layout: &mut TextLayout,
296 _: &mut (),
297 window: &mut Window,
298 cx: &mut App,
299 ) {
300 text_layout.paint(self, window, cx)
301 }
302}
303
304impl IntoElement for &'static str {
305 type Element = Self;
306
307 fn into_element(self) -> Self::Element {
308 self
309 }
310}
311
312impl IntoElement for String {
313 type Element = SharedString;
314
315 fn into_element(self) -> Self::Element {
316 self.into()
317 }
318}
319
320impl IntoElement for Cow<'static, str> {
321 type Element = SharedString;
322
323 fn into_element(self) -> Self::Element {
324 self.into()
325 }
326}
327
328impl Element for SharedString {
329 type RequestLayoutState = TextLayout;
330 type PrepaintState = ();
331
332 fn id(&self) -> Option<ElementId> {
333 None
334 }
335
336 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
337 None
338 }
339
340 fn request_layout(
341 &mut self,
342 _id: Option<&GlobalElementId>,
343 _inspector_id: Option<&InspectorElementId>,
344 window: &mut Window,
345 cx: &mut App,
346 ) -> (LayoutId, Self::RequestLayoutState) {
347 let mut state = TextLayout::default();
348 let layout_id = state.layout(self.clone(), None, window, cx);
349 (layout_id, state)
350 }
351
352 fn prepaint(
353 &mut self,
354 _id: Option<&GlobalElementId>,
355 _inspector_id: Option<&InspectorElementId>,
356 bounds: Bounds<Pixels>,
357 text_layout: &mut Self::RequestLayoutState,
358 _window: &mut Window,
359 _cx: &mut App,
360 ) {
361 text_layout.prepaint(bounds, self.as_ref())
362 }
363
364 fn paint(
365 &mut self,
366 _id: Option<&GlobalElementId>,
367 _inspector_id: Option<&InspectorElementId>,
368 _bounds: Bounds<Pixels>,
369 text_layout: &mut Self::RequestLayoutState,
370 _: &mut Self::PrepaintState,
371 window: &mut Window,
372 cx: &mut App,
373 ) {
374 text_layout.paint(self.as_ref(), window, cx)
375 }
376}
377
378impl IntoElement for SharedString {
379 type Element = Self;
380
381 fn into_element(self) -> Self::Element {
382 self
383 }
384}
385
386/// Renders text with runs of different styles.
387///
388/// Callers are responsible for setting the correct style for each run.
389/// For text with a uniform style, you can usually avoid calling this constructor
390/// and just pass text directly.
391pub struct StyledText {
392 text: SharedString,
393 runs: Option<Vec<TextRun>>,
394 delayed_highlights: Option<Vec<(Range<usize>, HighlightStyle)>>,
395 delayed_font_family_overrides: Option<Vec<(Range<usize>, SharedString)>>,
396 layout: TextLayout,
397}
398
399impl StyledText {
400 /// Construct a new styled text element from the given string.
401 pub fn new(text: impl Into<SharedString>) -> Self {
402 StyledText {
403 text: text.into(),
404 runs: None,
405 delayed_highlights: None,
406 delayed_font_family_overrides: None,
407 layout: TextLayout::default(),
408 }
409 }
410
411 /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
412 pub fn layout(&self) -> &TextLayout {
413 &self.layout
414 }
415
416 /// Set the styling attributes for the given text, as well as
417 /// as any ranges of text that have had their style customized.
418 pub fn with_default_highlights(
419 mut self,
420 default_style: &TextStyle,
421 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
422 ) -> Self {
423 debug_assert!(
424 self.delayed_highlights.is_none(),
425 "Can't use `with_default_highlights` and `with_highlights`"
426 );
427 let runs = Self::compute_runs(&self.text, default_style, highlights);
428 self.with_runs(runs)
429 }
430
431 /// Set the styling attributes for the given text, as well as
432 /// as any ranges of text that have had their style customized.
433 pub fn with_highlights(
434 mut self,
435 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
436 ) -> Self {
437 debug_assert!(
438 self.runs.is_none(),
439 "Can't use `with_highlights` and `with_default_highlights`"
440 );
441 self.delayed_highlights = Some(
442 highlights
443 .into_iter()
444 .inspect(|(run, _)| {
445 debug_assert!(self.text.is_char_boundary(run.start));
446 debug_assert!(self.text.is_char_boundary(run.end));
447 })
448 .collect::<Vec<_>>(),
449 );
450 self
451 }
452
453 fn compute_runs(
454 text: &str,
455 default_style: &TextStyle,
456 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
457 ) -> Vec<TextRun> {
458 let mut runs = Vec::new();
459 let mut ix = 0;
460 for (range, highlight) in highlights {
461 if ix < range.start {
462 debug_assert!(text.is_char_boundary(range.start));
463 runs.push(default_style.clone().to_run(range.start - ix));
464 }
465 debug_assert!(text.is_char_boundary(range.end));
466 runs.push(
467 default_style
468 .clone()
469 .highlight(highlight)
470 .to_run(range.len()),
471 );
472 ix = range.end;
473 }
474 if ix < text.len() {
475 runs.push(default_style.to_run(text.len() - ix));
476 }
477 runs
478 }
479
480 /// Override the font family for specific byte ranges of the text.
481 ///
482 /// This is resolved lazily at layout time, so the overrides are applied
483 /// on top of the inherited text style from the parent element.
484 /// Can be combined with [`with_highlights`](Self::with_highlights).
485 ///
486 /// The overrides must be sorted by range start and non-overlapping.
487 /// Each override range must fall on character boundaries.
488 pub fn with_font_family_overrides(
489 mut self,
490 overrides: impl IntoIterator<Item = (Range<usize>, SharedString)>,
491 ) -> Self {
492 self.delayed_font_family_overrides = Some(
493 overrides
494 .into_iter()
495 .inspect(|(range, _)| {
496 debug_assert!(self.text.is_char_boundary(range.start));
497 debug_assert!(self.text.is_char_boundary(range.end));
498 })
499 .collect(),
500 );
501 self
502 }
503
504 fn apply_font_family_overrides(
505 runs: &mut [TextRun],
506 overrides: &[(Range<usize>, SharedString)],
507 ) {
508 let mut byte_offset = 0;
509 let mut override_idx = 0;
510 for run in runs.iter_mut() {
511 let run_end = byte_offset + run.len;
512 while override_idx < overrides.len() && overrides[override_idx].0.end <= byte_offset {
513 override_idx += 1;
514 }
515 if override_idx < overrides.len() {
516 let (ref range, ref family) = overrides[override_idx];
517 if byte_offset >= range.start && run_end <= range.end {
518 run.font.family = family.clone();
519 }
520 }
521 byte_offset = run_end;
522 }
523 }
524
525 /// Set the text runs for this piece of text.
526 pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
527 let mut text = &*self.text;
528 for run in &runs {
529 text = text.get(run.len..).unwrap_or_else(|| {
530 #[cfg(debug_assertions)]
531 panic!("invalid text run. Text: '{text}', run: {run:?}");
532 #[cfg(not(debug_assertions))]
533 panic!("invalid text run");
534 });
535 }
536 assert!(text.is_empty(), "invalid text run");
537 self.runs = Some(runs);
538 self
539 }
540}
541
542impl Element for StyledText {
543 type RequestLayoutState = ();
544 type PrepaintState = ();
545
546 fn id(&self) -> Option<ElementId> {
547 None
548 }
549
550 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
551 None
552 }
553
554 fn request_layout(
555 &mut self,
556 _id: Option<&GlobalElementId>,
557 _inspector_id: Option<&InspectorElementId>,
558 window: &mut Window,
559 cx: &mut App,
560 ) -> (LayoutId, Self::RequestLayoutState) {
561 let font_family_overrides = self.delayed_font_family_overrides.take();
562 let mut runs = self.runs.take().or_else(|| {
563 self.delayed_highlights.take().map(|delayed_highlights| {
564 Self::compute_runs(&self.text, &window.text_style(), delayed_highlights)
565 })
566 });
567
568 if let Some(ref overrides) = font_family_overrides {
569 let runs =
570 runs.get_or_insert_with(|| vec![window.text_style().to_run(self.text.len())]);
571 Self::apply_font_family_overrides(runs, overrides);
572 }
573
574 let layout_id = self.layout.layout(self.text.clone(), runs, window, cx);
575 (layout_id, ())
576 }
577
578 fn prepaint(
579 &mut self,
580 _id: Option<&GlobalElementId>,
581 _inspector_id: Option<&InspectorElementId>,
582 bounds: Bounds<Pixels>,
583 _: &mut Self::RequestLayoutState,
584 _window: &mut Window,
585 _cx: &mut App,
586 ) {
587 self.layout.prepaint(bounds, &self.text)
588 }
589
590 fn paint(
591 &mut self,
592 _id: Option<&GlobalElementId>,
593 _inspector_id: Option<&InspectorElementId>,
594 _bounds: Bounds<Pixels>,
595 _: &mut Self::RequestLayoutState,
596 _: &mut Self::PrepaintState,
597 window: &mut Window,
598 cx: &mut App,
599 ) {
600 self.layout.paint(&self.text, window, cx)
601 }
602}
603
604impl IntoElement for StyledText {
605 type Element = Self;
606
607 fn into_element(self) -> Self::Element {
608 self
609 }
610}
611
612/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
613#[derive(Default, Clone)]
614pub struct TextLayout(Rc<RefCell<Option<TextLayoutInner>>>);
615
616struct TextLayoutInner {
617 len: usize,
618 lines: SmallVec<[WrappedLine; 1]>,
619 line_height: Pixels,
620 wrap_width: Option<Pixels>,
621 truncate_width: Option<Pixels>,
622 size: Option<Size<Pixels>>,
623 bounds: Option<Bounds<Pixels>>,
624}
625
626impl TextLayout {
627 fn layout(
628 &self,
629 text: SharedString,
630 runs: Option<Vec<TextRun>>,
631 window: &mut Window,
632 _: &mut App,
633 ) -> LayoutId {
634 let text_style = window.text_style();
635 let font_size = text_style.font_size.to_pixels(window.rem_size());
636 let line_height = window.pixel_snap(
637 text_style
638 .line_height
639 .to_pixels(font_size.into(), window.rem_size()),
640 );
641
642 let runs = if let Some(runs) = runs {
643 runs
644 } else {
645 vec![text_style.to_run(text.len())]
646 };
647 window.request_measured_layout(Default::default(), {
648 let element_state = self.clone();
649
650 move |known_dimensions, available_space, window, cx| {
651 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
652 known_dimensions.width.or(match available_space.width {
653 crate::AvailableSpace::Definite(x) => Some(x),
654 _ => None,
655 })
656 } else {
657 None
658 };
659
660 let (truncate_width, truncation_affix, truncate_from) =
661 if let Some(text_overflow) = text_style.text_overflow.clone() {
662 let width = known_dimensions.width.or(match available_space.width {
663 crate::AvailableSpace::Definite(x) => match text_style.line_clamp {
664 Some(max_lines) => Some(x * max_lines),
665 None => Some(x),
666 },
667 _ => None,
668 });
669
670 match text_overflow {
671 TextOverflow::Truncate(s) => (width, s, TruncateFrom::End),
672 TextOverflow::TruncateStart(s) => (width, s, TruncateFrom::Start),
673 TextOverflow::TruncateMiddle(s) => (width, s, TruncateFrom::Middle),
674 }
675 } else {
676 (None, "".into(), TruncateFrom::End)
677 };
678
679 // Only use cached layout if:
680 // 1. We have a cached size
681 // 2. wrap_width matches (or both are None)
682 // 3. truncate_width is None (if truncate_width is Some, we need to re-layout
683 // because the previous layout may have been computed without truncation)
684 // 4. the cached layout was not truncated (a truncated layout answers an
685 // unconstrained probe with the truncated size, which poisons intrinsic
686 // sizing with whatever width some earlier measure pass happened to use)
687 if let Some(text_layout) = element_state.0.borrow().as_ref()
688 && let Some(size) = text_layout.size
689 && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
690 && truncate_width.is_none()
691 && text_layout.truncate_width.is_none()
692 {
693 return size;
694 }
695
696 let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
697 let (text, runs) = if let Some(truncate_width) = truncate_width {
698 if let Some(max_lines) = text_style.line_clamp
699 && let Some(wrap_width) = wrap_width
700 {
701 line_wrapper.truncate_wrapped_line(
702 text.clone(),
703 wrap_width,
704 max_lines,
705 &truncation_affix,
706 &runs,
707 truncate_from,
708 )
709 } else if let Some(unclipped) = window
710 .text_system()
711 .shape_text(text.clone(), font_size, &runs, None, None)
712 .log_err()
713 && unclipped
714 .iter()
715 .all(|line| line.size(line_height).width <= truncate_width)
716 {
717 // The truncation decision below sums per-character advances,
718 // which overestimates the shaped width (no kerning), truncating
719 // text that fits exactly in its measured width. Skip truncation
720 // whenever the honestly-shaped text fits; the shaping result
721 // comes from the line layout cache when the same text was
722 // already measured untruncated this frame.
723 (text.clone(), Cow::Borrowed(&*runs))
724 } else {
725 line_wrapper.truncate_line(
726 text.clone(),
727 truncate_width,
728 &truncation_affix,
729 &runs,
730 truncate_from,
731 )
732 }
733 } else {
734 (text.clone(), Cow::Borrowed(&*runs))
735 };
736 let len = text.len();
737
738 let Some(lines) = window
739 .text_system()
740 .shape_text(
741 text,
742 font_size,
743 &runs,
744 wrap_width, // Wrap if we know the width.
745 text_style.line_clamp, // Limit the number of lines if line_clamp is set.
746 )
747 .log_err()
748 else {
749 element_state.0.borrow_mut().replace(TextLayoutInner {
750 lines: Default::default(),
751 len: 0,
752 line_height,
753 wrap_width,
754 truncate_width,
755 size: Some(Size::default()),
756 bounds: None,
757 });
758 return Size::default();
759 };
760
761 let mut size: Size<Pixels> = Size::default();
762 for line in &lines {
763 let line_size = line.size(line_height);
764 size.height += line_size.height;
765 size.width = size.width.max(line_size.width).ceil();
766 }
767
768 element_state.0.borrow_mut().replace(TextLayoutInner {
769 lines,
770 len,
771 line_height,
772 wrap_width,
773 truncate_width,
774 size: Some(size),
775 bounds: None,
776 });
777
778 size
779 }
780 })
781 }
782
783 fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
784 let mut element_state = self.0.borrow_mut();
785 let element_state = element_state
786 .as_mut()
787 .with_context(|| format!("measurement has not been performed on {text}"))
788 .unwrap();
789 element_state.bounds = Some(bounds);
790 }
791
792 fn paint(&self, text: &str, window: &mut Window, cx: &mut App) {
793 let element_state = self.0.borrow();
794 let element_state = element_state
795 .as_ref()
796 .with_context(|| format!("measurement has not been performed on {text}"))
797 .unwrap();
798 let bounds = element_state
799 .bounds
800 .with_context(|| format!("prepaint has not been performed on {text}"))
801 .unwrap();
802
803 let line_height = element_state.line_height;
804 let mut line_origin = bounds.origin;
805 let text_style = window.text_style();
806 for line in &element_state.lines {
807 line.paint_background(
808 line_origin,
809 line_height,
810 text_style.text_align,
811 Some(bounds),
812 window,
813 cx,
814 )
815 .log_err();
816 line.paint(
817 line_origin,
818 line_height,
819 text_style.text_align,
820 Some(bounds),
821 window,
822 cx,
823 )
824 .log_err();
825 line_origin.y += line.size(line_height).height;
826 }
827 }
828
829 /// Get the byte index into the input of the pixel position.
830 pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
831 let element_state = self.0.borrow();
832 let element_state = element_state
833 .as_ref()
834 .expect("measurement has not been performed");
835 let bounds = element_state
836 .bounds
837 .expect("prepaint has not been performed");
838
839 if position.y < bounds.top() {
840 return Err(0);
841 }
842
843 let line_height = element_state.line_height;
844 let mut line_origin = bounds.origin;
845 let mut line_start_ix = 0;
846 for line in &element_state.lines {
847 let line_bottom = line_origin.y + line.size(line_height).height;
848 if position.y > line_bottom {
849 line_origin.y = line_bottom;
850 line_start_ix += line.len() + 1;
851 } else {
852 let position_within_line = position - line_origin;
853 match line.index_for_position(position_within_line, line_height) {
854 Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
855 Err(index_within_line) => return Err(line_start_ix + index_within_line),
856 }
857 }
858 }
859
860 Err(line_start_ix.saturating_sub(1))
861 }
862
863 /// Get the pixel position for the given byte index.
864 pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
865 let element_state = self.0.borrow();
866 let element_state = element_state
867 .as_ref()
868 .expect("measurement has not been performed");
869 let bounds = element_state
870 .bounds
871 .expect("prepaint has not been performed");
872 let line_height = element_state.line_height;
873
874 let mut line_origin = bounds.origin;
875 let mut line_start_ix = 0;
876
877 for line in &element_state.lines {
878 let line_end_ix = line_start_ix + line.len();
879 if index < line_start_ix {
880 break;
881 } else if index > line_end_ix {
882 line_origin.y += line.size(line_height).height;
883 line_start_ix = line_end_ix + 1;
884 continue;
885 } else {
886 let ix_within_line = index - line_start_ix;
887 return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
888 }
889 }
890
891 None
892 }
893
894 /// Retrieve the layout for the line containing the given byte index.
895 pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
896 let element_state = self.0.borrow();
897 let element_state = element_state
898 .as_ref()
899 .expect("measurement has not been performed");
900 let mut line_start_ix = 0;
901
902 for line in &element_state.lines {
903 let line_end_ix = line_start_ix + line.len();
904 if index < line_start_ix {
905 break;
906 } else if index > line_end_ix {
907 line_start_ix = line_end_ix + 1;
908 continue;
909 } else {
910 return Some(line.layout.clone());
911 }
912 }
913
914 None
915 }
916
917 /// Retrieve all line layouts in source order.
918 pub fn line_layouts(&self) -> SmallVec<[Arc<WrappedLineLayout>; 1]> {
919 self.0
920 .borrow()
921 .as_ref()
922 .expect("measurement has not been performed")
923 .lines
924 .iter()
925 .map(|line| line.layout.clone())
926 .collect()
927 }
928
929 /// The bounds of this layout.
930 pub fn bounds(&self) -> Bounds<Pixels> {
931 self.0.borrow().as_ref().unwrap().bounds.unwrap()
932 }
933
934 /// The line height for this layout.
935 pub fn line_height(&self) -> Pixels {
936 self.0.borrow().as_ref().unwrap().line_height
937 }
938
939 /// The UTF-8 length of the underlying text.
940 pub fn len(&self) -> usize {
941 self.0.borrow().as_ref().unwrap().len
942 }
943
944 /// The text for this layout.
945 pub fn text(&self) -> String {
946 self.0
947 .borrow()
948 .as_ref()
949 .unwrap()
950 .lines
951 .iter()
952 .map(|s| &s.text)
953 .join("\n")
954 }
955
956 /// The text for this layout (with soft-wraps as newlines)
957 pub fn wrapped_text(&self) -> String {
958 let mut accumulator = String::new();
959
960 for wrapped in self.0.borrow().as_ref().unwrap().lines.iter() {
961 let mut seen = 0;
962 for boundary in wrapped.layout.wrap_boundaries.iter() {
963 let index = wrapped.layout.unwrapped_layout.runs[boundary.run_ix].glyphs
964 [boundary.glyph_ix]
965 .index;
966
967 accumulator.push_str(&wrapped.text[seen..index]);
968 accumulator.push('\n');
969 seen = index;
970 }
971 accumulator.push_str(&wrapped.text[seen..]);
972 accumulator.push('\n');
973 }
974 // Remove trailing newline
975 accumulator.pop();
976 accumulator
977 }
978}
979
980/// A text element that can be interacted with.
981pub struct InteractiveText {
982 element_id: ElementId,
983 text: StyledText,
984 click_listener:
985 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut Window, &mut App)>>,
986 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App)>>,
987 tooltip_builder: Option<Rc<dyn Fn(usize, &mut Window, &mut App) -> Option<AnyView>>>,
988 tooltip_id: Option<TooltipId>,
989 clickable_ranges: Vec<Range<usize>>,
990}
991
992struct InteractiveTextClickEvent {
993 mouse_down_index: usize,
994 mouse_up_index: usize,
995}
996
997#[doc(hidden)]
998#[derive(Default)]
999pub struct InteractiveTextState {
1000 mouse_down_index: Rc<Cell<Option<usize>>>,
1001 hovered_index: Rc<Cell<Option<usize>>>,
1002 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
1003}
1004
1005/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
1006impl InteractiveText {
1007 /// Creates a new InteractiveText from the given text.
1008 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
1009 Self {
1010 element_id: id.into(),
1011 text,
1012 click_listener: None,
1013 hover_listener: None,
1014 tooltip_builder: None,
1015 tooltip_id: None,
1016 clickable_ranges: Vec::new(),
1017 }
1018 }
1019
1020 /// on_click is called when the user clicks on one of the given ranges, passing the index of
1021 /// the clicked range.
1022 pub fn on_click(
1023 mut self,
1024 ranges: Vec<Range<usize>>,
1025 listener: impl Fn(usize, &mut Window, &mut App) + 'static,
1026 ) -> Self {
1027 self.click_listener = Some(Box::new(move |ranges, event, window, cx| {
1028 for (range_ix, range) in ranges.iter().enumerate() {
1029 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
1030 {
1031 listener(range_ix, window, cx);
1032 }
1033 }
1034 }));
1035 self.clickable_ranges = ranges;
1036 self
1037 }
1038
1039 /// on_hover is called when the mouse moves over a character within the text, passing the
1040 /// index of the hovered character, or None if the mouse leaves the text.
1041 pub fn on_hover(
1042 mut self,
1043 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut Window, &mut App) + 'static,
1044 ) -> Self {
1045 self.hover_listener = Some(Box::new(listener));
1046 self
1047 }
1048
1049 /// tooltip lets you specify a tooltip for a given character index in the string.
1050 pub fn tooltip(
1051 mut self,
1052 builder: impl Fn(usize, &mut Window, &mut App) -> Option<AnyView> + 'static,
1053 ) -> Self {
1054 self.tooltip_builder = Some(Rc::new(builder));
1055 self
1056 }
1057}
1058
1059impl Element for InteractiveText {
1060 type RequestLayoutState = ();
1061 type PrepaintState = Hitbox;
1062
1063 fn id(&self) -> Option<ElementId> {
1064 Some(self.element_id.clone())
1065 }
1066
1067 fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
1068 None
1069 }
1070
1071 fn a11y_role(&self) -> Option<accesskit::Role> {
1072 Some(accesskit::Role::Label)
1073 }
1074
1075 fn write_a11y_info(&self, node: &mut accesskit::Node) {
1076 node.set_value(self.text.text.to_string());
1077 }
1078
1079 fn request_layout(
1080 &mut self,
1081 _id: Option<&GlobalElementId>,
1082 inspector_id: Option<&InspectorElementId>,
1083 window: &mut Window,
1084 cx: &mut App,
1085 ) -> (LayoutId, Self::RequestLayoutState) {
1086 self.text.request_layout(None, inspector_id, window, cx)
1087 }
1088
1089 fn prepaint(
1090 &mut self,
1091 global_id: Option<&GlobalElementId>,
1092 inspector_id: Option<&InspectorElementId>,
1093 bounds: Bounds<Pixels>,
1094 state: &mut Self::RequestLayoutState,
1095 window: &mut Window,
1096 cx: &mut App,
1097 ) -> Hitbox {
1098 window.with_optional_element_state::<InteractiveTextState, _>(
1099 global_id,
1100 |interactive_state, window| {
1101 let mut interactive_state = interactive_state
1102 .map(|interactive_state| interactive_state.unwrap_or_default());
1103
1104 if let Some(interactive_state) = interactive_state.as_mut() {
1105 if self.tooltip_builder.is_some() {
1106 self.tooltip_id =
1107 set_tooltip_on_window(&interactive_state.active_tooltip, window);
1108 } else {
1109 // If there is no longer a tooltip builder, remove the active tooltip.
1110 interactive_state.active_tooltip.take();
1111 }
1112 }
1113
1114 self.text
1115 .prepaint(None, inspector_id, bounds, state, window, cx);
1116 let hitbox = window.insert_hitbox(bounds, HitboxBehavior::Normal);
1117 (hitbox, interactive_state)
1118 },
1119 )
1120 }
1121
1122 fn paint(
1123 &mut self,
1124 global_id: Option<&GlobalElementId>,
1125 inspector_id: Option<&InspectorElementId>,
1126 bounds: Bounds<Pixels>,
1127 _: &mut Self::RequestLayoutState,
1128 hitbox: &mut Hitbox,
1129 window: &mut Window,
1130 cx: &mut App,
1131 ) {
1132 let current_view = window.current_view();
1133 let text_layout = self.text.layout().clone();
1134 window.with_element_state::<InteractiveTextState, _>(
1135 global_id.unwrap(),
1136 |interactive_state, window| {
1137 let mut interactive_state = interactive_state.unwrap_or_default();
1138 if let Some(click_listener) = self.click_listener.take() {
1139 let mouse_position = window.mouse_position();
1140 if let Ok(ix) = text_layout.index_for_position(mouse_position)
1141 && self
1142 .clickable_ranges
1143 .iter()
1144 .any(|range| range.contains(&ix))
1145 {
1146 window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
1147 }
1148
1149 let text_layout = text_layout.clone();
1150 let mouse_down = interactive_state.mouse_down_index.clone();
1151 if let Some(mouse_down_index) = mouse_down.get() {
1152 let hitbox = hitbox.clone();
1153 let clickable_ranges = mem::take(&mut self.clickable_ranges);
1154 window.on_mouse_event(
1155 move |event: &MouseUpEvent, phase, window: &mut Window, cx| {
1156 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1157 if let Ok(mouse_up_index) =
1158 text_layout.index_for_position(event.position)
1159 {
1160 click_listener(
1161 &clickable_ranges,
1162 InteractiveTextClickEvent {
1163 mouse_down_index,
1164 mouse_up_index,
1165 },
1166 window,
1167 cx,
1168 )
1169 }
1170
1171 mouse_down.take();
1172 window.refresh();
1173 }
1174 },
1175 );
1176 } else {
1177 let hitbox = hitbox.clone();
1178 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| {
1179 if phase == DispatchPhase::Bubble
1180 && hitbox.is_hovered(window)
1181 && let Ok(mouse_down_index) =
1182 text_layout.index_for_position(event.position)
1183 {
1184 mouse_down.set(Some(mouse_down_index));
1185 window.refresh();
1186 }
1187 });
1188 }
1189 }
1190
1191 window.on_mouse_event({
1192 let mut hover_listener = self.hover_listener.take();
1193 let hitbox = hitbox.clone();
1194 let text_layout = text_layout.clone();
1195 let hovered_index = interactive_state.hovered_index.clone();
1196 move |event: &MouseMoveEvent, phase, window, cx| {
1197 if phase == DispatchPhase::Bubble && hitbox.is_hovered(window) {
1198 let current = hovered_index.get();
1199 let updated = text_layout.index_for_position(event.position).ok();
1200 if current != updated {
1201 hovered_index.set(updated);
1202 if let Some(hover_listener) = hover_listener.as_ref() {
1203 hover_listener(updated, event.clone(), window, cx);
1204 }
1205 cx.notify(current_view);
1206 }
1207 }
1208 }
1209 });
1210
1211 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
1212 let active_tooltip = interactive_state.active_tooltip.clone();
1213 let build_tooltip = Rc::new({
1214 let tooltip_is_hoverable = false;
1215 let text_layout = text_layout.clone();
1216 move |window: &mut Window, cx: &mut App| {
1217 text_layout
1218 .index_for_position(window.mouse_position())
1219 .ok()
1220 .and_then(|position| tooltip_builder(position, window, cx))
1221 .map(|view| (view, tooltip_is_hoverable))
1222 }
1223 });
1224
1225 // Use bounds instead of testing hitbox since this is called during prepaint.
1226 let check_is_hovered_during_prepaint = Rc::new({
1227 let source_bounds = hitbox.bounds;
1228 let text_layout = text_layout.clone();
1229 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1230 move |window: &Window| {
1231 text_layout
1232 .index_for_position(window.mouse_position())
1233 .is_ok()
1234 && source_bounds.contains(&window.mouse_position())
1235 && pending_mouse_down.get().is_none()
1236 }
1237 });
1238
1239 let check_is_hovered = Rc::new({
1240 let hitbox = hitbox.clone();
1241 let text_layout = text_layout.clone();
1242 let pending_mouse_down = interactive_state.mouse_down_index.clone();
1243 move |window: &Window| {
1244 text_layout
1245 .index_for_position(window.mouse_position())
1246 .is_ok()
1247 && hitbox.is_hovered(window)
1248 && pending_mouse_down.get().is_none()
1249 }
1250 });
1251
1252 register_tooltip_mouse_handlers(
1253 &active_tooltip,
1254 self.tooltip_id,
1255 build_tooltip,
1256 check_is_hovered,
1257 check_is_hovered_during_prepaint,
1258 None,
1259 window,
1260 );
1261 }
1262
1263 self.text
1264 .paint(None, inspector_id, bounds, &mut (), &mut (), window, cx);
1265
1266 ((), interactive_state)
1267 },
1268 );
1269 }
1270}
1271
1272impl IntoElement for InteractiveText {
1273 type Element = Self;
1274
1275 fn into_element(self) -> Self::Element {
1276 self
1277 }
1278}
1279
1280#[cfg(test)]
1281mod tests {
1282 use super::*;
1283
1284 #[test]
1285 fn test_into_element_for() {
1286 use crate::{ParentElement as _, SharedString, div};
1287 use std::borrow::Cow;
1288
1289 let _ = div().child("static str");
1290 let _ = div().child("String".to_string());
1291 let _ = div().child(Cow::Borrowed("Cow"));
1292 let _ = div().child(SharedString::from("SharedString"));
1293 }
1294
1295 #[test]
1296 fn text_macro_id() {
1297 // one call to `text!` = one id
1298 fn make_text_stable_id(happy: bool) -> Text {
1299 text!(if happy { "happy" } else { "sad" })
1300 }
1301
1302 // two calls to `text!` = two ids
1303 fn make_text_unstable_id(happy: bool) -> Text {
1304 if happy { text!("happy") } else { text!("sad") }
1305 }
1306
1307 assert_eq!(make_text_stable_id(false).id, make_text_stable_id(true).id);
1308 assert_ne!(
1309 make_text_unstable_id(false).id,
1310 make_text_unstable_id(true).id
1311 );
1312 }
1313}
1314