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