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_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}