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