1use crate::{
2 ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
3 HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
4 Pixels, Point, SharedString, Size, TextRun, TextStyle, WhiteSpace, WindowContext, WrappedLine,
5 TOOLTIP_DELAY,
6};
7use anyhow::anyhow;
8use parking_lot::{Mutex, MutexGuard};
9use smallvec::SmallVec;
10use std::{
11 cell::{Cell, RefCell},
12 mem,
13 ops::Range,
14 rc::Rc,
15 sync::Arc,
16};
17use util::ResultExt;
18
19impl Element for &'static str {
20 type RequestLayoutState = TextState;
21 type PrepaintState = ();
22
23 fn id(&self) -> Option<ElementId> {
24 None
25 }
26
27 fn request_layout(
28 &mut self,
29 _id: Option<&GlobalElementId>,
30 cx: &mut WindowContext,
31 ) -> (LayoutId, Self::RequestLayoutState) {
32 let mut state = TextState::default();
33 let layout_id = state.layout(SharedString::from(*self), None, cx);
34 (layout_id, state)
35 }
36
37 fn prepaint(
38 &mut self,
39 _id: Option<&GlobalElementId>,
40 _bounds: Bounds<Pixels>,
41 _text_state: &mut Self::RequestLayoutState,
42 _cx: &mut WindowContext,
43 ) {
44 }
45
46 fn paint(
47 &mut self,
48 _id: Option<&GlobalElementId>,
49 bounds: Bounds<Pixels>,
50 text_state: &mut TextState,
51 _: &mut (),
52 cx: &mut WindowContext,
53 ) {
54 text_state.paint(bounds, self, cx)
55 }
56}
57
58impl IntoElement for &'static str {
59 type Element = Self;
60
61 fn into_element(self) -> Self::Element {
62 self
63 }
64}
65
66impl IntoElement for String {
67 type Element = SharedString;
68
69 fn into_element(self) -> Self::Element {
70 self.into()
71 }
72}
73
74impl Element for SharedString {
75 type RequestLayoutState = TextState;
76 type PrepaintState = ();
77
78 fn id(&self) -> Option<ElementId> {
79 None
80 }
81
82 fn request_layout(
83 &mut self,
84
85 _id: Option<&GlobalElementId>,
86
87 cx: &mut WindowContext,
88 ) -> (LayoutId, Self::RequestLayoutState) {
89 let mut state = TextState::default();
90 let layout_id = state.layout(self.clone(), None, cx);
91 (layout_id, state)
92 }
93
94 fn prepaint(
95 &mut self,
96 _id: Option<&GlobalElementId>,
97 _bounds: Bounds<Pixels>,
98 _text_state: &mut Self::RequestLayoutState,
99 _cx: &mut WindowContext,
100 ) {
101 }
102
103 fn paint(
104 &mut self,
105 _id: Option<&GlobalElementId>,
106 bounds: Bounds<Pixels>,
107 text_state: &mut Self::RequestLayoutState,
108 _: &mut Self::PrepaintState,
109 cx: &mut WindowContext,
110 ) {
111 let text_str: &str = self.as_ref();
112 text_state.paint(bounds, text_str, cx)
113 }
114}
115
116impl IntoElement for SharedString {
117 type Element = Self;
118
119 fn into_element(self) -> Self::Element {
120 self
121 }
122}
123
124/// Renders text with runs of different styles.
125///
126/// Callers are responsible for setting the correct style for each run.
127/// For text with a uniform style, you can usually avoid calling this constructor
128/// and just pass text directly.
129pub struct StyledText {
130 text: SharedString,
131 runs: Option<Vec<TextRun>>,
132}
133
134impl StyledText {
135 /// Construct a new styled text element from the given string.
136 pub fn new(text: impl Into<SharedString>) -> Self {
137 StyledText {
138 text: text.into(),
139 runs: None,
140 }
141 }
142
143 /// Set the styling attributes for the given text, as well as
144 /// as any ranges of text that have had their style customized.
145 pub fn with_highlights(
146 mut self,
147 default_style: &TextStyle,
148 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
149 ) -> Self {
150 let mut runs = Vec::new();
151 let mut ix = 0;
152 for (range, highlight) in highlights {
153 if ix < range.start {
154 runs.push(default_style.clone().to_run(range.start - ix));
155 }
156 runs.push(
157 default_style
158 .clone()
159 .highlight(highlight)
160 .to_run(range.len()),
161 );
162 ix = range.end;
163 }
164 if ix < self.text.len() {
165 runs.push(default_style.to_run(self.text.len() - ix));
166 }
167 self.runs = Some(runs);
168 self
169 }
170}
171
172impl Element for StyledText {
173 type RequestLayoutState = TextState;
174 type PrepaintState = ();
175
176 fn id(&self) -> Option<ElementId> {
177 None
178 }
179
180 fn request_layout(
181 &mut self,
182
183 _id: Option<&GlobalElementId>,
184
185 cx: &mut WindowContext,
186 ) -> (LayoutId, Self::RequestLayoutState) {
187 let mut state = TextState::default();
188 let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
189 (layout_id, state)
190 }
191
192 fn prepaint(
193 &mut self,
194 _id: Option<&GlobalElementId>,
195 _bounds: Bounds<Pixels>,
196 _state: &mut Self::RequestLayoutState,
197 _cx: &mut WindowContext,
198 ) {
199 }
200
201 fn paint(
202 &mut self,
203 _id: Option<&GlobalElementId>,
204 bounds: Bounds<Pixels>,
205 text_state: &mut Self::RequestLayoutState,
206 _: &mut Self::PrepaintState,
207 cx: &mut WindowContext,
208 ) {
209 text_state.paint(bounds, &self.text, cx)
210 }
211}
212
213impl IntoElement for StyledText {
214 type Element = Self;
215
216 fn into_element(self) -> Self::Element {
217 self
218 }
219}
220
221#[doc(hidden)]
222#[derive(Default, Clone)]
223pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
224
225struct TextStateInner {
226 lines: SmallVec<[WrappedLine; 1]>,
227 line_height: Pixels,
228 wrap_width: Option<Pixels>,
229 size: Option<Size<Pixels>>,
230}
231
232impl TextState {
233 fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
234 self.0.lock()
235 }
236
237 fn layout(
238 &mut self,
239 text: SharedString,
240 runs: Option<Vec<TextRun>>,
241 cx: &mut WindowContext,
242 ) -> LayoutId {
243 let text_style = cx.text_style();
244 let font_size = text_style.font_size.to_pixels(cx.rem_size());
245 let line_height = text_style
246 .line_height
247 .to_pixels(font_size.into(), cx.rem_size());
248
249 let runs = if let Some(runs) = runs {
250 runs
251 } else {
252 vec![text_style.to_run(text.len())]
253 };
254
255 let layout_id = cx.request_measured_layout(Default::default(), {
256 let element_state = self.clone();
257
258 move |known_dimensions, available_space, cx| {
259 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
260 known_dimensions.width.or(match available_space.width {
261 crate::AvailableSpace::Definite(x) => Some(x),
262 _ => None,
263 })
264 } else {
265 None
266 };
267
268 if let Some(text_state) = element_state.0.lock().as_ref() {
269 if text_state.size.is_some()
270 && (wrap_width.is_none() || wrap_width == text_state.wrap_width)
271 {
272 return text_state.size.unwrap();
273 }
274 }
275
276 let Some(lines) = cx
277 .text_system()
278 .shape_text(
279 text.clone(),
280 font_size,
281 &runs,
282 wrap_width, // Wrap if we know the width.
283 )
284 .log_err()
285 else {
286 element_state.lock().replace(TextStateInner {
287 lines: Default::default(),
288 line_height,
289 wrap_width,
290 size: Some(Size::default()),
291 });
292 return Size::default();
293 };
294
295 let mut size: Size<Pixels> = Size::default();
296 for line in &lines {
297 let line_size = line.size(line_height);
298 size.height += line_size.height;
299 size.width = size.width.max(line_size.width).ceil();
300 }
301
302 element_state.lock().replace(TextStateInner {
303 lines,
304 line_height,
305 wrap_width,
306 size: Some(size),
307 });
308
309 size
310 }
311 });
312
313 layout_id
314 }
315
316 fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
317 let element_state = self.lock();
318 let element_state = element_state
319 .as_ref()
320 .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
321 .unwrap();
322
323 let line_height = element_state.line_height;
324 let mut line_origin = bounds.origin;
325 for line in &element_state.lines {
326 line.paint(line_origin, line_height, cx).log_err();
327 line_origin.y += line.size(line_height).height;
328 }
329 }
330
331 fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
332 if !bounds.contains(&position) {
333 return None;
334 }
335
336 let element_state = self.lock();
337 let element_state = element_state
338 .as_ref()
339 .expect("measurement has not been performed");
340
341 let line_height = element_state.line_height;
342 let mut line_origin = bounds.origin;
343 let mut line_start_ix = 0;
344 for line in &element_state.lines {
345 let line_bottom = line_origin.y + line.size(line_height).height;
346 if position.y > line_bottom {
347 line_origin.y = line_bottom;
348 line_start_ix += line.len() + 1;
349 } else {
350 let position_within_line = position - line_origin;
351 let index_within_line =
352 line.index_for_position(position_within_line, line_height)?;
353 return Some(line_start_ix + index_within_line);
354 }
355 }
356
357 None
358 }
359}
360
361/// A text element that can be interacted with.
362pub struct InteractiveText {
363 element_id: ElementId,
364 text: StyledText,
365 click_listener:
366 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
367 hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>)>>,
368 tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext<'_>) -> Option<AnyView>>>,
369 clickable_ranges: Vec<Range<usize>>,
370}
371
372struct InteractiveTextClickEvent {
373 mouse_down_index: usize,
374 mouse_up_index: usize,
375}
376
377#[doc(hidden)]
378#[derive(Default)]
379pub struct InteractiveTextState {
380 mouse_down_index: Rc<Cell<Option<usize>>>,
381 hovered_index: Rc<Cell<Option<usize>>>,
382 active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
383}
384
385/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
386impl InteractiveText {
387 /// Creates a new InteractiveText from the given text.
388 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
389 Self {
390 element_id: id.into(),
391 text,
392 click_listener: None,
393 hover_listener: None,
394 tooltip_builder: None,
395 clickable_ranges: Vec::new(),
396 }
397 }
398
399 /// on_click is called when the user clicks on one of the given ranges, passing the index of
400 /// the clicked range.
401 pub fn on_click(
402 mut self,
403 ranges: Vec<Range<usize>>,
404 listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
405 ) -> Self {
406 self.click_listener = Some(Box::new(move |ranges, event, cx| {
407 for (range_ix, range) in ranges.iter().enumerate() {
408 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
409 {
410 listener(range_ix, cx);
411 }
412 }
413 }));
414 self.clickable_ranges = ranges;
415 self
416 }
417
418 /// on_hover is called when the mouse moves over a character within the text, passing the
419 /// index of the hovered character, or None if the mouse leaves the text.
420 pub fn on_hover(
421 mut self,
422 listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext<'_>) + 'static,
423 ) -> Self {
424 self.hover_listener = Some(Box::new(listener));
425 self
426 }
427
428 /// tooltip lets you specify a tooltip for a given character index in the string.
429 pub fn tooltip(
430 mut self,
431 builder: impl Fn(usize, &mut WindowContext<'_>) -> Option<AnyView> + 'static,
432 ) -> Self {
433 self.tooltip_builder = Some(Rc::new(builder));
434 self
435 }
436}
437
438impl Element for InteractiveText {
439 type RequestLayoutState = TextState;
440 type PrepaintState = Hitbox;
441
442 fn id(&self) -> Option<ElementId> {
443 Some(self.element_id.clone())
444 }
445
446 fn request_layout(
447 &mut self,
448 _id: Option<&GlobalElementId>,
449 cx: &mut WindowContext,
450 ) -> (LayoutId, Self::RequestLayoutState) {
451 self.text.request_layout(None, cx)
452 }
453
454 fn prepaint(
455 &mut self,
456 global_id: Option<&GlobalElementId>,
457 bounds: Bounds<Pixels>,
458 state: &mut Self::RequestLayoutState,
459 cx: &mut WindowContext,
460 ) -> Hitbox {
461 cx.with_optional_element_state::<InteractiveTextState, _>(
462 global_id,
463 |interactive_state, cx| {
464 let interactive_state = interactive_state
465 .map(|interactive_state| interactive_state.unwrap_or_default());
466
467 if let Some(interactive_state) = interactive_state.as_ref() {
468 if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
469 {
470 if let Some(tooltip) = active_tooltip.tooltip.clone() {
471 cx.set_tooltip(tooltip);
472 }
473 }
474 }
475
476 self.text.prepaint(None, bounds, state, cx);
477 let hitbox = cx.insert_hitbox(bounds, false);
478 (hitbox, interactive_state)
479 },
480 )
481 }
482
483 fn paint(
484 &mut self,
485 global_id: Option<&GlobalElementId>,
486 bounds: Bounds<Pixels>,
487 text_state: &mut Self::RequestLayoutState,
488 hitbox: &mut Hitbox,
489 cx: &mut WindowContext,
490 ) {
491 cx.with_element_state::<InteractiveTextState, _>(
492 global_id.unwrap(),
493 |interactive_state, cx| {
494 let mut interactive_state = interactive_state.unwrap_or_default();
495 if let Some(click_listener) = self.click_listener.take() {
496 let mouse_position = cx.mouse_position();
497 if let Some(ix) = text_state.index_for_position(bounds, mouse_position) {
498 if self
499 .clickable_ranges
500 .iter()
501 .any(|range| range.contains(&ix))
502 {
503 cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
504 }
505 }
506
507 let text_state = text_state.clone();
508 let mouse_down = interactive_state.mouse_down_index.clone();
509 if let Some(mouse_down_index) = mouse_down.get() {
510 let hitbox = hitbox.clone();
511 let clickable_ranges = mem::take(&mut self.clickable_ranges);
512 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
513 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
514 if let Some(mouse_up_index) =
515 text_state.index_for_position(bounds, event.position)
516 {
517 click_listener(
518 &clickable_ranges,
519 InteractiveTextClickEvent {
520 mouse_down_index,
521 mouse_up_index,
522 },
523 cx,
524 )
525 }
526
527 mouse_down.take();
528 cx.refresh();
529 }
530 });
531 } else {
532 let hitbox = hitbox.clone();
533 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
534 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
535 if let Some(mouse_down_index) =
536 text_state.index_for_position(bounds, event.position)
537 {
538 mouse_down.set(Some(mouse_down_index));
539 cx.refresh();
540 }
541 }
542 });
543 }
544 }
545
546 cx.on_mouse_event({
547 let mut hover_listener = self.hover_listener.take();
548 let hitbox = hitbox.clone();
549 let text_state = text_state.clone();
550 let hovered_index = interactive_state.hovered_index.clone();
551 move |event: &MouseMoveEvent, phase, cx| {
552 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
553 let current = hovered_index.get();
554 let updated = text_state.index_for_position(bounds, event.position);
555 if current != updated {
556 hovered_index.set(updated);
557 if let Some(hover_listener) = hover_listener.as_ref() {
558 hover_listener(updated, event.clone(), cx);
559 }
560 cx.refresh();
561 }
562 }
563 }
564 });
565
566 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
567 let hitbox = hitbox.clone();
568 let active_tooltip = interactive_state.active_tooltip.clone();
569 let pending_mouse_down = interactive_state.mouse_down_index.clone();
570 let text_state = text_state.clone();
571
572 cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
573 let position = text_state.index_for_position(bounds, event.position);
574 let is_hovered = position.is_some()
575 && hitbox.is_hovered(cx)
576 && pending_mouse_down.get().is_none();
577 if !is_hovered {
578 active_tooltip.take();
579 return;
580 }
581 let position = position.unwrap();
582
583 if phase != DispatchPhase::Bubble {
584 return;
585 }
586
587 if active_tooltip.borrow().is_none() {
588 let task = cx.spawn({
589 let active_tooltip = active_tooltip.clone();
590 let tooltip_builder = tooltip_builder.clone();
591
592 move |mut cx| async move {
593 cx.background_executor().timer(TOOLTIP_DELAY).await;
594 cx.update(|cx| {
595 let new_tooltip =
596 tooltip_builder(position, cx).map(|tooltip| {
597 ActiveTooltip {
598 tooltip: Some(AnyTooltip {
599 view: tooltip,
600 mouse_position: cx.mouse_position(),
601 }),
602 _task: None,
603 }
604 });
605 *active_tooltip.borrow_mut() = new_tooltip;
606 cx.refresh();
607 })
608 .ok();
609 }
610 });
611 *active_tooltip.borrow_mut() = Some(ActiveTooltip {
612 tooltip: None,
613 _task: Some(task),
614 });
615 }
616 });
617
618 let active_tooltip = interactive_state.active_tooltip.clone();
619 cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
620 active_tooltip.take();
621 });
622 }
623
624 self.text.paint(None, bounds, text_state, &mut (), cx);
625
626 ((), interactive_state)
627 },
628 );
629 }
630}
631
632impl IntoElement for InteractiveText {
633 type Element = Self;
634
635 fn into_element(self) -> Self::Element {
636 self
637 }
638}