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