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