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