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