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