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 for line in &element_state.lines {
248 let line_bottom = line_origin.y + line.size(line_height).height;
249 if position.y > line_bottom {
250 line_origin.y = line_bottom;
251 } else {
252 let position_within_line = position - line_origin;
253 return line.index_for_position(position_within_line, line_height);
254 }
255 }
256
257 None
258 }
259}
260
261pub struct InteractiveText {
262 element_id: ElementId,
263 text: StyledText,
264 click_listener: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
265}
266
267struct InteractiveTextClickEvent {
268 mouse_down_index: usize,
269 mouse_up_index: usize,
270}
271
272pub struct InteractiveTextState {
273 text_state: TextState,
274 mouse_down_index: Rc<Cell<Option<usize>>>,
275}
276
277impl InteractiveText {
278 pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
279 Self {
280 element_id: id.into(),
281 text,
282 click_listener: None,
283 }
284 }
285
286 pub fn on_click(
287 mut self,
288 ranges: Vec<Range<usize>>,
289 listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
290 ) -> Self {
291 self.click_listener = Some(Box::new(move |event, cx| {
292 for (range_ix, range) in ranges.iter().enumerate() {
293 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
294 {
295 listener(range_ix, cx);
296 }
297 }
298 }));
299 self
300 }
301}
302
303impl Element for InteractiveText {
304 type State = InteractiveTextState;
305
306 fn layout(
307 &mut self,
308 state: Option<Self::State>,
309 cx: &mut WindowContext,
310 ) -> (LayoutId, Self::State) {
311 if let Some(InteractiveTextState {
312 mouse_down_index, ..
313 }) = state
314 {
315 let (layout_id, text_state) = self.text.layout(None, cx);
316 let element_state = InteractiveTextState {
317 text_state,
318 mouse_down_index,
319 };
320 (layout_id, element_state)
321 } else {
322 let (layout_id, text_state) = self.text.layout(None, cx);
323 let element_state = InteractiveTextState {
324 text_state,
325 mouse_down_index: Rc::default(),
326 };
327 (layout_id, element_state)
328 }
329 }
330
331 fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
332 if let Some(click_listener) = self.click_listener {
333 let text_state = state.text_state.clone();
334 let mouse_down = state.mouse_down_index.clone();
335 if let Some(mouse_down_index) = mouse_down.get() {
336 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
337 if phase == DispatchPhase::Bubble {
338 if let Some(mouse_up_index) =
339 text_state.index_for_position(bounds, event.position)
340 {
341 click_listener(
342 InteractiveTextClickEvent {
343 mouse_down_index,
344 mouse_up_index,
345 },
346 cx,
347 )
348 }
349
350 mouse_down.take();
351 cx.notify();
352 }
353 });
354 } else {
355 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
356 if phase == DispatchPhase::Bubble {
357 if let Some(mouse_down_index) =
358 text_state.index_for_position(bounds, event.position)
359 {
360 mouse_down.set(Some(mouse_down_index));
361 cx.notify();
362 }
363 }
364 });
365 }
366 }
367
368 self.text.paint(bounds, &mut state.text_state, cx)
369 }
370}
371
372impl IntoElement for InteractiveText {
373 type Element = Self;
374
375 fn element_id(&self) -> Option<ElementId> {
376 Some(self.element_id.clone())
377 }
378
379 fn into_element(self) -> Self::Element {
380 self
381 }
382}