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: Option<Box<dyn Fn(InteractiveTextClickEvent, &mut WindowContext<'_>)>>,
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 }
288 }
289
290 pub fn on_click(
291 mut self,
292 ranges: Vec<Range<usize>>,
293 listener: impl Fn(usize, &mut WindowContext<'_>) + 'static,
294 ) -> Self {
295 self.click_listener = Some(Box::new(move |event, cx| {
296 for (range_ix, range) in ranges.iter().enumerate() {
297 if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
298 {
299 listener(range_ix, cx);
300 }
301 }
302 }));
303 self
304 }
305}
306
307impl Element for InteractiveText {
308 type State = InteractiveTextState;
309
310 fn layout(
311 &mut self,
312 state: Option<Self::State>,
313 cx: &mut WindowContext,
314 ) -> (LayoutId, Self::State) {
315 if let Some(InteractiveTextState {
316 mouse_down_index, ..
317 }) = state
318 {
319 let (layout_id, text_state) = self.text.layout(None, cx);
320 let element_state = InteractiveTextState {
321 text_state,
322 mouse_down_index,
323 };
324 (layout_id, element_state)
325 } else {
326 let (layout_id, text_state) = self.text.layout(None, cx);
327 let element_state = InteractiveTextState {
328 text_state,
329 mouse_down_index: Rc::default(),
330 };
331 (layout_id, element_state)
332 }
333 }
334
335 fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
336 if let Some(click_listener) = self.click_listener {
337 let text_state = state.text_state.clone();
338 let mouse_down = state.mouse_down_index.clone();
339 if let Some(mouse_down_index) = mouse_down.get() {
340 cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
341 if phase == DispatchPhase::Bubble {
342 if let Some(mouse_up_index) =
343 text_state.index_for_position(bounds, event.position)
344 {
345 click_listener(
346 InteractiveTextClickEvent {
347 mouse_down_index,
348 mouse_up_index,
349 },
350 cx,
351 )
352 }
353
354 mouse_down.take();
355 cx.notify();
356 }
357 });
358 } else {
359 cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
360 if phase == DispatchPhase::Bubble {
361 if let Some(mouse_down_index) =
362 text_state.index_for_position(bounds, event.position)
363 {
364 mouse_down.set(Some(mouse_down_index));
365 cx.notify();
366 }
367 }
368 });
369 }
370 }
371
372 self.text.paint(bounds, &mut state.text_state, cx)
373 }
374}
375
376impl IntoElement for InteractiveText {
377 type Element = Self;
378
379 fn element_id(&self) -> Option<ElementId> {
380 Some(self.element_id.clone())
381 }
382
383 fn into_element(self) -> Self::Element {
384 self
385 }
386}