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