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