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 cx.with_element_state::<InteractiveTextState, _>(
419 Some(self.element_id.clone()),
420 |interactive_state, cx| {
421 let interactive_state = interactive_state
422 .map(|interactive_state| interactive_state.unwrap_or_default());
423
424 if let Some(interactive_state) = interactive_state.as_ref() {
425 if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
426 {
427 if let Some(tooltip) = active_tooltip.tooltip.clone() {
428 cx.set_tooltip(tooltip);
429 }
430 }
431 }
432
433 self.text.after_layout(bounds, state, cx);
434 let hitbox = cx.insert_hitbox(bounds, false);
435 (hitbox, interactive_state)
436 },
437 )
438 }
439
440 fn paint(
441 &mut self,
442 bounds: Bounds<Pixels>,
443 text_state: &mut Self::BeforeLayout,
444 hitbox: &mut Hitbox,
445 cx: &mut ElementContext,
446 ) {
447 cx.with_element_state::<InteractiveTextState, _>(
448 Some(self.element_id.clone()),
449 |interactive_state, cx| {
450 let mut interactive_state = interactive_state.unwrap().unwrap_or_default();
451 if let Some(click_listener) = self.click_listener.take() {
452 let mouse_position = cx.mouse_position();
453 if let Some(ix) = text_state.index_for_position(bounds, mouse_position) {
454 if self
455 .clickable_ranges
456 .iter()
457 .any(|range| range.contains(&ix))
458 {
459 cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
460 }
461 }
462
463 let text_state = text_state.clone();
464 let mouse_down = interactive_state.mouse_down_index.clone();
465 if let Some(mouse_down_index) = mouse_down.get() {
466 let hitbox = hitbox.clone();
467 let clickable_ranges = mem::take(&mut self.clickable_ranges);
468 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
469 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
470 if let Some(mouse_up_index) =
471 text_state.index_for_position(bounds, event.position)
472 {
473 click_listener(
474 &clickable_ranges,
475 InteractiveTextClickEvent {
476 mouse_down_index,
477 mouse_up_index,
478 },
479 cx,
480 )
481 }
482
483 mouse_down.take();
484 cx.refresh();
485 }
486 });
487 } else {
488 let hitbox = hitbox.clone();
489 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
490 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
491 if let Some(mouse_down_index) =
492 text_state.index_for_position(bounds, event.position)
493 {
494 mouse_down.set(Some(mouse_down_index));
495 cx.refresh();
496 }
497 }
498 });
499 }
500 }
501
502 cx.on_mouse_event({
503 let mut hover_listener = self.hover_listener.take();
504 let hitbox = hitbox.clone();
505 let text_state = text_state.clone();
506 let hovered_index = interactive_state.hovered_index.clone();
507 move |event: &MouseMoveEvent, phase, cx| {
508 if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
509 let current = hovered_index.get();
510 let updated = text_state.index_for_position(bounds, event.position);
511 if current != updated {
512 hovered_index.set(updated);
513 if let Some(hover_listener) = hover_listener.as_ref() {
514 hover_listener(updated, event.clone(), cx);
515 }
516 cx.refresh();
517 }
518 }
519 }
520 });
521
522 if let Some(tooltip_builder) = self.tooltip_builder.clone() {
523 let hitbox = hitbox.clone();
524 let active_tooltip = interactive_state.active_tooltip.clone();
525 let pending_mouse_down = interactive_state.mouse_down_index.clone();
526 let text_state = text_state.clone();
527
528 cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
529 let position = text_state.index_for_position(bounds, event.position);
530 let is_hovered = position.is_some()
531 && hitbox.is_hovered(cx)
532 && pending_mouse_down.get().is_none();
533 if !is_hovered {
534 active_tooltip.take();
535 return;
536 }
537 let position = position.unwrap();
538
539 if phase != DispatchPhase::Bubble {
540 return;
541 }
542
543 if active_tooltip.borrow().is_none() {
544 let task = cx.spawn({
545 let active_tooltip = active_tooltip.clone();
546 let tooltip_builder = tooltip_builder.clone();
547
548 move |mut cx| async move {
549 cx.background_executor().timer(TOOLTIP_DELAY).await;
550 cx.update(|cx| {
551 let new_tooltip =
552 tooltip_builder(position, cx).map(|tooltip| {
553 ActiveTooltip {
554 tooltip: Some(AnyTooltip {
555 view: tooltip,
556 cursor_offset: cx.mouse_position(),
557 }),
558 _task: None,
559 }
560 });
561 *active_tooltip.borrow_mut() = new_tooltip;
562 cx.refresh();
563 })
564 .ok();
565 }
566 });
567 *active_tooltip.borrow_mut() = Some(ActiveTooltip {
568 tooltip: None,
569 _task: Some(task),
570 });
571 }
572 });
573
574 let active_tooltip = interactive_state.active_tooltip.clone();
575 cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
576 active_tooltip.take();
577 });
578 }
579
580 self.text.paint(bounds, text_state, &mut (), cx);
581
582 ((), Some(interactive_state))
583 },
584 );
585 }
586}
587
588impl IntoElement for InteractiveText {
589 type Element = Self;
590
591 fn into_element(self) -> Self::Element {
592 self
593 }
594}