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