text.rs

  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}