1use crate::{
2 Bounds, DispatchPhase, Element, ElementId, HighlightStyle, IntoElement, LayoutId,
3 MouseDownEvent, MouseUpEvent, Pixels, Point, SharedString, Size, TextRun, TextStyle,
4 WhiteSpace, WindowContext, WrappedLine,
5};
6use anyhow::anyhow;
7use parking_lot::{Mutex, MutexGuard};
8use smallvec::SmallVec;
9use std::{cell::Cell, mem, ops::Range, rc::Rc, sync::Arc};
10use util::ResultExt;
11
12impl Element for &'static str {
13 type State = TextState;
14
15 fn layout(
16 &mut self,
17 _: Option<Self::State>,
18 cx: &mut WindowContext,
19 ) -> (LayoutId, Self::State) {
20 let mut state = TextState::default();
21 let layout_id = state.layout(SharedString::from(*self), None, cx);
22 (layout_id, state)
23 }
24
25 fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
26 state.paint(bounds, self, cx)
27 }
28}
29
30impl IntoElement for &'static str {
31 type Element = Self;
32
33 fn element_id(&self) -> Option<ElementId> {
34 None
35 }
36
37 fn into_element(self) -> Self::Element {
38 self
39 }
40}
41
42impl Element for SharedString {
43 type State = TextState;
44
45 fn layout(
46 &mut self,
47 _: Option<Self::State>,
48 cx: &mut WindowContext,
49 ) -> (LayoutId, Self::State) {
50 let mut state = TextState::default();
51 let layout_id = state.layout(self.clone(), None, cx);
52 (layout_id, state)
53 }
54
55 fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut TextState, cx: &mut WindowContext) {
56 let text_str: &str = self.as_ref();
57 state.paint(bounds, text_str, cx)
58 }
59}
60
61impl IntoElement for SharedString {
62 type Element = Self;
63
64 fn element_id(&self) -> Option<ElementId> {
65 None
66 }
67
68 fn into_element(self) -> Self::Element {
69 self
70 }
71}
72
73/// Renders text with runs of different styles.
74///
75/// Callers are responsible for setting the correct style for each run.
76/// For text with a uniform style, you can usually avoid calling this constructor
77/// and just pass text directly.
78pub struct StyledText {
79 text: SharedString,
80 runs: Option<Vec<TextRun>>,
81}
82
83impl StyledText {
84 pub fn new(text: impl Into<SharedString>) -> Self {
85 StyledText {
86 text: text.into(),
87 runs: None,
88 }
89 }
90
91 pub fn with_highlights(
92 mut self,
93 default_style: &TextStyle,
94 highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
95 ) -> Self {
96 let mut runs = Vec::new();
97 let mut ix = 0;
98 for (range, highlight) in highlights {
99 if ix < range.start {
100 runs.push(default_style.clone().to_run(range.start - ix));
101 }
102 runs.push(
103 default_style
104 .clone()
105 .highlight(highlight)
106 .to_run(range.len()),
107 );
108 ix = range.end;
109 }
110 if ix < self.text.len() {
111 runs.push(default_style.to_run(self.text.len() - ix));
112 }
113 self.runs = Some(runs);
114 self
115 }
116}
117
118impl Element for StyledText {
119 type State = TextState;
120
121 fn layout(
122 &mut self,
123 _: Option<Self::State>,
124 cx: &mut WindowContext,
125 ) -> (LayoutId, Self::State) {
126 let mut state = TextState::default();
127 let layout_id = state.layout(self.text.clone(), self.runs.take(), cx);
128 (layout_id, state)
129 }
130
131 fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
132 state.paint(bounds, &self.text, cx)
133 }
134}
135
136impl IntoElement for StyledText {
137 type Element = Self;
138
139 fn element_id(&self) -> Option<crate::ElementId> {
140 None
141 }
142
143 fn into_element(self) -> Self::Element {
144 self
145 }
146}
147
148#[derive(Default, Clone)]
149pub struct TextState(Arc<Mutex<Option<TextStateInner>>>);
150
151struct TextStateInner {
152 lines: SmallVec<[WrappedLine; 1]>,
153 line_height: Pixels,
154 wrap_width: Option<Pixels>,
155 size: Option<Size<Pixels>>,
156}
157
158impl TextState {
159 fn lock(&self) -> MutexGuard<Option<TextStateInner>> {
160 self.0.lock()
161 }
162
163 fn layout(
164 &mut self,
165 text: SharedString,
166 runs: Option<Vec<TextRun>>,
167 cx: &mut WindowContext,
168 ) -> LayoutId {
169 let text_style = cx.text_style();
170 let font_size = text_style.font_size.to_pixels(cx.rem_size());
171 let line_height = text_style
172 .line_height
173 .to_pixels(font_size.into(), cx.rem_size());
174 let text = SharedString::from(text);
175
176 let runs = if let Some(runs) = runs {
177 runs
178 } else {
179 vec![text_style.to_run(text.len())]
180 };
181
182 let layout_id = cx.request_measured_layout(Default::default(), {
183 let element_state = self.clone();
184
185 move |known_dimensions, available_space, cx| {
186 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
187 known_dimensions.width.or(match available_space.width {
188 crate::AvailableSpace::Definite(x) => Some(x),
189 _ => None,
190 })
191 } else {
192 None
193 };
194
195 if let Some(text_state) = element_state.0.lock().as_ref() {
196 if text_state.size.is_some()
197 && (wrap_width.is_none() || wrap_width == text_state.wrap_width)
198 {
199 return text_state.size.unwrap();
200 }
201 }
202
203 let Some(lines) = cx
204 .text_system()
205 .shape_text(
206 &text, font_size, &runs, wrap_width, // Wrap if we know the width.
207 )
208 .log_err()
209 else {
210 element_state.lock().replace(TextStateInner {
211 lines: Default::default(),
212 line_height,
213 wrap_width,
214 size: Some(Size::default()),
215 });
216 return Size::default();
217 };
218
219 let mut size: Size<Pixels> = Size::default();
220 for line in &lines {
221 let line_size = line.size(line_height);
222 size.height += line_size.height;
223 size.width = size.width.max(line_size.width).ceil();
224 }
225
226 element_state.lock().replace(TextStateInner {
227 lines,
228 line_height,
229 wrap_width,
230 size: Some(size),
231 });
232
233 size
234 }
235 });
236
237 layout_id
238 }
239
240 fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
241 let element_state = self.lock();
242 let element_state = element_state
243 .as_ref()
244 .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
245 .unwrap();
246
247 let line_height = element_state.line_height;
248 let mut line_origin = bounds.origin;
249 for line in &element_state.lines {
250 line.paint(line_origin, line_height, cx).log_err();
251 line_origin.y += line.size(line_height).height;
252 }
253 }
254
255 fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
256 if !bounds.contains(&position) {
257 return None;
258 }
259
260 let element_state = self.lock();
261 let element_state = element_state
262 .as_ref()
263 .expect("measurement has not been performed");
264
265 let line_height = element_state.line_height;
266 let mut line_origin = bounds.origin;
267 let mut line_start_ix = 0;
268 for line in &element_state.lines {
269 let line_bottom = line_origin.y + line.size(line_height).height;
270 if position.y > line_bottom {
271 line_origin.y = line_bottom;
272 line_start_ix += line.len() + 1;
273 } else {
274 let position_within_line = position - line_origin;
275 let index_within_line =
276 line.index_for_position(position_within_line, line_height)?;
277 return Some(line_start_ix + index_within_line);
278 }
279 }
280
281 None
282 }
283}
284
285pub struct InteractiveText {
286 element_id: ElementId,
287 text: StyledText,
288 click_listener:
289 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
290 clickable_ranges: Vec<Range<usize>>,
291}
292
293struct InteractiveTextClickEvent {
294 mouse_down_index: usize,
295 mouse_up_index: usize,
296}
297
298pub struct InteractiveTextState {
299 text_state: TextState,
300 mouse_down_index: Rc<Cell<Option<usize>>>,
301}
302
303impl InteractiveText {
304 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
305 Self {
306 element_id: id.into(),
307 text,
308 click_listener: None,
309 clickable_ranges: Vec::new(),
310 }
311 }
312
313 pub fn on_click(
314 mut self,
315 ranges: Vec<Range<usize>>,
316 listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
317 ) -> Self {
318 self.click_listener = Some(Box::new(move |ranges, event, cx| {
319 for (range_ix, range) in ranges.iter().enumerate() {
320 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
321 {
322 listener(range_ix, cx);
323 }
324 }
325 }));
326 self.clickable_ranges = ranges;
327 self
328 }
329}
330
331impl Element for InteractiveText {
332 type State = InteractiveTextState;
333
334 fn layout(
335 &mut self,
336 state: Option<Self::State>,
337 cx: &mut WindowContext,
338 ) -> (LayoutId, Self::State) {
339 if let Some(InteractiveTextState {
340 mouse_down_index, ..
341 }) = state
342 {
343 let (layout_id, text_state) = self.text.layout(None, cx);
344 let element_state = InteractiveTextState {
345 text_state,
346 mouse_down_index,
347 };
348 (layout_id, element_state)
349 } else {
350 let (layout_id, text_state) = self.text.layout(None, cx);
351 let element_state = InteractiveTextState {
352 text_state,
353 mouse_down_index: Rc::default(),
354 };
355 (layout_id, element_state)
356 }
357 }
358
359 fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
360 if let Some(click_listener) = self.click_listener.take() {
361 let mouse_position = cx.mouse_position();
362 if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
363 if self
364 .clickable_ranges
365 .iter()
366 .any(|range| range.contains(&ix))
367 && cx.was_top_layer(&mouse_position, cx.stacking_order())
368 {
369 cx.set_cursor_style(crate::CursorStyle::PointingHand)
370 }
371 }
372
373 let text_state = state.text_state.clone();
374 let mouse_down = state.mouse_down_index.clone();
375 if let Some(mouse_down_index) = mouse_down.get() {
376 let clickable_ranges = mem::take(&mut self.clickable_ranges);
377 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
378 if phase == DispatchPhase::Bubble {
379 if let Some(mouse_up_index) =
380 text_state.index_for_position(bounds, event.position)
381 {
382 click_listener(
383 &clickable_ranges,
384 InteractiveTextClickEvent {
385 mouse_down_index,
386 mouse_up_index,
387 },
388 cx,
389 )
390 }
391
392 mouse_down.take();
393 cx.notify();
394 }
395 });
396 } else {
397 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
398 if phase == DispatchPhase::Bubble {
399 if let Some(mouse_down_index) =
400 text_state.index_for_position(bounds, event.position)
401 {
402 mouse_down.set(Some(mouse_down_index));
403 cx.notify();
404 }
405 }
406 });
407 }
408 }
409
410 self.text.paint(bounds, &mut state.text_state, cx)
411 }
412}
413
414impl IntoElement for InteractiveText {
415 type Element = Self;
416
417 fn element_id(&self) -> Option<ElementId> {
418 Some(self.element_id.clone())
419 }
420
421 fn into_element(self) -> Self::Element {
422 self
423 }
424}