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 request_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 request_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 request_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
175 let runs = if let Some(runs) = runs {
176 runs
177 } else {
178 vec![text_style.to_run(text.len())]
179 };
180
181 let layout_id = cx.request_measured_layout(Default::default(), {
182 let element_state = self.clone();
183
184 move |known_dimensions, available_space, cx| {
185 let wrap_width = if text_style.white_space == WhiteSpace::Normal {
186 known_dimensions.width.or(match available_space.width {
187 crate::AvailableSpace::Definite(x) => Some(x),
188 _ => None,
189 })
190 } else {
191 None
192 };
193
194 if let Some(text_state) = element_state.0.lock().as_ref() {
195 if text_state.size.is_some()
196 && (wrap_width.is_none() || wrap_width == text_state.wrap_width)
197 {
198 return text_state.size.unwrap();
199 }
200 }
201
202 let Some(lines) = cx
203 .text_system()
204 .shape_text(
205 text.clone(),
206 font_size,
207 &runs,
208 wrap_width, // Wrap if we know the width.
209 )
210 .log_err()
211 else {
212 element_state.lock().replace(TextStateInner {
213 lines: Default::default(),
214 line_height,
215 wrap_width,
216 size: Some(Size::default()),
217 });
218 return Size::default();
219 };
220
221 let mut size: Size<Pixels> = Size::default();
222 for line in &lines {
223 let line_size = line.size(line_height);
224 size.height += line_size.height;
225 size.width = size.width.max(line_size.width).ceil();
226 }
227
228 element_state.lock().replace(TextStateInner {
229 lines,
230 line_height,
231 wrap_width,
232 size: Some(size),
233 });
234
235 size
236 }
237 });
238
239 layout_id
240 }
241
242 fn paint(&mut self, bounds: Bounds<Pixels>, text: &str, cx: &mut WindowContext) {
243 let element_state = self.lock();
244 let element_state = element_state
245 .as_ref()
246 .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
247 .unwrap();
248
249 let line_height = element_state.line_height;
250 let mut line_origin = bounds.origin;
251 for line in &element_state.lines {
252 line.paint(line_origin, line_height, cx).log_err();
253 line_origin.y += line.size(line_height).height;
254 }
255 }
256
257 fn index_for_position(&self, bounds: Bounds<Pixels>, position: Point<Pixels>) -> Option<usize> {
258 if !bounds.contains(&position) {
259 return None;
260 }
261
262 let element_state = self.lock();
263 let element_state = element_state
264 .as_ref()
265 .expect("measurement has not been performed");
266
267 let line_height = element_state.line_height;
268 let mut line_origin = bounds.origin;
269 let mut line_start_ix = 0;
270 for line in &element_state.lines {
271 let line_bottom = line_origin.y + line.size(line_height).height;
272 if position.y > line_bottom {
273 line_origin.y = line_bottom;
274 line_start_ix += line.len() + 1;
275 } else {
276 let position_within_line = position - line_origin;
277 let index_within_line =
278 line.index_for_position(position_within_line, line_height)?;
279 return Some(line_start_ix + index_within_line);
280 }
281 }
282
283 None
284 }
285}
286
287pub struct InteractiveText {
288 element_id: ElementId,
289 text: StyledText,
290 click_listener:
291 Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
292 clickable_ranges: Vec<Range<usize>>,
293}
294
295struct InteractiveTextClickEvent {
296 mouse_down_index: usize,
297 mouse_up_index: usize,
298}
299
300pub struct InteractiveTextState {
301 text_state: TextState,
302 mouse_down_index: Rc<Cell<Option<usize>>>,
303}
304
305impl InteractiveText {
306 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
307 Self {
308 element_id: id.into(),
309 text,
310 click_listener: None,
311 clickable_ranges: Vec::new(),
312 }
313 }
314
315 pub fn on_click(
316 mut self,
317 ranges: Vec<Range<usize>>,
318 listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
319 ) -> Self {
320 self.click_listener = Some(Box::new(move |ranges, event, cx| {
321 for (range_ix, range) in ranges.iter().enumerate() {
322 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
323 {
324 listener(range_ix, cx);
325 }
326 }
327 }));
328 self.clickable_ranges = ranges;
329 self
330 }
331}
332
333impl Element for InteractiveText {
334 type State = InteractiveTextState;
335
336 fn request_layout(
337 &mut self,
338 state: Option<Self::State>,
339 cx: &mut WindowContext,
340 ) -> (LayoutId, Self::State) {
341 if let Some(InteractiveTextState {
342 mouse_down_index, ..
343 }) = state
344 {
345 let (layout_id, text_state) = self.text.request_layout(None, cx);
346 let element_state = InteractiveTextState {
347 text_state,
348 mouse_down_index,
349 };
350 (layout_id, element_state)
351 } else {
352 let (layout_id, text_state) = self.text.request_layout(None, cx);
353 let element_state = InteractiveTextState {
354 text_state,
355 mouse_down_index: Rc::default(),
356 };
357 (layout_id, element_state)
358 }
359 }
360
361 fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
362 if let Some(click_listener) = self.click_listener.take() {
363 let mouse_position = cx.mouse_position();
364 if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
365 if self
366 .clickable_ranges
367 .iter()
368 .any(|range| range.contains(&ix))
369 && cx.was_top_layer(&mouse_position, cx.stacking_order())
370 {
371 cx.set_cursor_style(crate::CursorStyle::PointingHand)
372 }
373 }
374
375 let text_state = state.text_state.clone();
376 let mouse_down = state.mouse_down_index.clone();
377 if let Some(mouse_down_index) = mouse_down.get() {
378 let clickable_ranges = mem::take(&mut self.clickable_ranges);
379 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
380 if phase == DispatchPhase::Bubble {
381 if let Some(mouse_up_index) =
382 text_state.index_for_position(bounds, event.position)
383 {
384 click_listener(
385 &clickable_ranges,
386 InteractiveTextClickEvent {
387 mouse_down_index,
388 mouse_up_index,
389 },
390 cx,
391 )
392 }
393
394 mouse_down.take();
395 cx.refresh();
396 }
397 });
398 } else {
399 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
400 if phase == DispatchPhase::Bubble {
401 if let Some(mouse_down_index) =
402 text_state.index_for_position(bounds, event.position)
403 {
404 mouse_down.set(Some(mouse_down_index));
405 cx.refresh();
406 }
407 }
408 });
409 }
410 }
411
412 self.text.paint(bounds, &mut state.text_state, cx)
413 }
414}
415
416impl IntoElement for InteractiveText {
417 type Element = Self;
418
419 fn element_id(&self) -> Option<ElementId> {
420 Some(self.element_id.clone())
421 }
422
423 fn into_element(self) -> Self::Element {
424 self
425 }
426}