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