text.rs

  1use crate::{
  2    ActiveTooltip, AnyTooltip, AnyView, Bounds, DispatchPhase, Element, ElementId, GlobalElementId,
  3    HighlightStyle, Hitbox, IntoElement, LayoutId, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
  4    Pixels, Point, SharedString, Size, TextRun, TextStyle, Truncate, WhiteSpace, WindowContext,
  5    WrappedLine, WrappedLineLayout, TOOLTIP_DELAY,
  6};
  7use anyhow::anyhow;
  8use parking_lot::{Mutex, MutexGuard};
  9use smallvec::SmallVec;
 10use std::{
 11    cell::{Cell, RefCell},
 12    mem,
 13    ops::Range,
 14    rc::Rc,
 15    sync::Arc,
 16};
 17use util::ResultExt;
 18
 19impl Element for &'static str {
 20    type RequestLayoutState = TextLayout;
 21    type PrepaintState = ();
 22
 23    fn id(&self) -> Option<ElementId> {
 24        None
 25    }
 26
 27    fn request_layout(
 28        &mut self,
 29        _id: Option<&GlobalElementId>,
 30        cx: &mut WindowContext,
 31    ) -> (LayoutId, Self::RequestLayoutState) {
 32        let mut state = TextLayout::default();
 33        let layout_id = state.layout(SharedString::from(*self), None, cx);
 34        (layout_id, state)
 35    }
 36
 37    fn prepaint(
 38        &mut self,
 39        _id: Option<&GlobalElementId>,
 40        bounds: Bounds<Pixels>,
 41        text_layout: &mut Self::RequestLayoutState,
 42        _cx: &mut WindowContext,
 43    ) {
 44        text_layout.prepaint(bounds, self)
 45    }
 46
 47    fn paint(
 48        &mut self,
 49        _id: Option<&GlobalElementId>,
 50        _bounds: Bounds<Pixels>,
 51        text_layout: &mut TextLayout,
 52        _: &mut (),
 53        cx: &mut WindowContext,
 54    ) {
 55        text_layout.paint(self, cx)
 56    }
 57}
 58
 59impl IntoElement for &'static str {
 60    type Element = Self;
 61
 62    fn into_element(self) -> Self::Element {
 63        self
 64    }
 65}
 66
 67impl IntoElement for String {
 68    type Element = SharedString;
 69
 70    fn into_element(self) -> Self::Element {
 71        self.into()
 72    }
 73}
 74
 75impl Element for SharedString {
 76    type RequestLayoutState = TextLayout;
 77    type PrepaintState = ();
 78
 79    fn id(&self) -> Option<ElementId> {
 80        None
 81    }
 82
 83    fn request_layout(
 84        &mut self,
 85
 86        _id: Option<&GlobalElementId>,
 87
 88        cx: &mut WindowContext,
 89    ) -> (LayoutId, Self::RequestLayoutState) {
 90        let mut state = TextLayout::default();
 91        let layout_id = state.layout(self.clone(), None, cx);
 92        (layout_id, state)
 93    }
 94
 95    fn prepaint(
 96        &mut self,
 97        _id: Option<&GlobalElementId>,
 98        bounds: Bounds<Pixels>,
 99        text_layout: &mut Self::RequestLayoutState,
100        _cx: &mut WindowContext,
101    ) {
102        text_layout.prepaint(bounds, self.as_ref())
103    }
104
105    fn paint(
106        &mut self,
107        _id: Option<&GlobalElementId>,
108        _bounds: Bounds<Pixels>,
109        text_layout: &mut Self::RequestLayoutState,
110        _: &mut Self::PrepaintState,
111        cx: &mut WindowContext,
112    ) {
113        text_layout.paint(self.as_ref(), cx)
114    }
115}
116
117impl IntoElement for SharedString {
118    type Element = Self;
119
120    fn into_element(self) -> Self::Element {
121        self
122    }
123}
124
125/// Renders text with runs of different styles.
126///
127/// Callers are responsible for setting the correct style for each run.
128/// For text with a uniform style, you can usually avoid calling this constructor
129/// and just pass text directly.
130pub struct StyledText {
131    text: SharedString,
132    runs: Option<Vec<TextRun>>,
133    layout: TextLayout,
134}
135
136impl StyledText {
137    /// Construct a new styled text element from the given string.
138    pub fn new(text: impl Into<SharedString>) -> Self {
139        StyledText {
140            text: text.into(),
141            runs: None,
142            layout: TextLayout::default(),
143        }
144    }
145
146    /// Get the layout for this element. This can be used to map indices to pixels and vice versa.
147    pub fn layout(&self) -> &TextLayout {
148        &self.layout
149    }
150
151    /// Set the styling attributes for the given text, as well as
152    /// as any ranges of text that have had their style customized.
153    pub fn with_highlights(
154        mut self,
155        default_style: &TextStyle,
156        highlights: impl IntoIterator<Item = (Range<usize>, HighlightStyle)>,
157    ) -> Self {
158        let mut runs = Vec::new();
159        let mut ix = 0;
160        for (range, highlight) in highlights {
161            if ix < range.start {
162                runs.push(default_style.clone().to_run(range.start - ix));
163            }
164            runs.push(
165                default_style
166                    .clone()
167                    .highlight(highlight)
168                    .to_run(range.len()),
169            );
170            ix = range.end;
171        }
172        if ix < self.text.len() {
173            runs.push(default_style.to_run(self.text.len() - ix));
174        }
175        self.runs = Some(runs);
176        self
177    }
178
179    /// Set the text runs for this piece of text.
180    pub fn with_runs(mut self, runs: Vec<TextRun>) -> Self {
181        self.runs = Some(runs);
182        self
183    }
184}
185
186impl Element for StyledText {
187    type RequestLayoutState = ();
188    type PrepaintState = ();
189
190    fn id(&self) -> Option<ElementId> {
191        None
192    }
193
194    fn request_layout(
195        &mut self,
196
197        _id: Option<&GlobalElementId>,
198
199        cx: &mut WindowContext,
200    ) -> (LayoutId, Self::RequestLayoutState) {
201        let layout_id = self.layout.layout(self.text.clone(), self.runs.take(), cx);
202        (layout_id, ())
203    }
204
205    fn prepaint(
206        &mut self,
207        _id: Option<&GlobalElementId>,
208        bounds: Bounds<Pixels>,
209        _: &mut Self::RequestLayoutState,
210        _cx: &mut WindowContext,
211    ) {
212        self.layout.prepaint(bounds, &self.text)
213    }
214
215    fn paint(
216        &mut self,
217        _id: Option<&GlobalElementId>,
218        _bounds: Bounds<Pixels>,
219        _: &mut Self::RequestLayoutState,
220        _: &mut Self::PrepaintState,
221        cx: &mut WindowContext,
222    ) {
223        self.layout.paint(&self.text, cx)
224    }
225}
226
227impl IntoElement for StyledText {
228    type Element = Self;
229
230    fn into_element(self) -> Self::Element {
231        self
232    }
233}
234
235/// The Layout for TextElement. This can be used to map indices to pixels and vice versa.
236#[derive(Default, Clone)]
237pub struct TextLayout(Arc<Mutex<Option<TextLayoutInner>>>);
238
239struct TextLayoutInner {
240    lines: SmallVec<[WrappedLine; 1]>,
241    line_height: Pixels,
242    wrap_width: Option<Pixels>,
243    size: Option<Size<Pixels>>,
244    bounds: Option<Bounds<Pixels>>,
245}
246
247const ELLIPSIS: &str = "";
248
249impl TextLayout {
250    fn lock(&self) -> MutexGuard<Option<TextLayoutInner>> {
251        self.0.lock()
252    }
253
254    fn layout(
255        &self,
256        text: SharedString,
257        runs: Option<Vec<TextRun>>,
258        cx: &mut WindowContext,
259    ) -> LayoutId {
260        let text_style = cx.text_style();
261        let font_size = text_style.font_size.to_pixels(cx.rem_size());
262        let line_height = text_style
263            .line_height
264            .to_pixels(font_size.into(), cx.rem_size());
265
266        let mut runs = if let Some(runs) = runs {
267            runs
268        } else {
269            vec![text_style.to_run(text.len())]
270        };
271
272        let layout_id = cx.request_measured_layout(Default::default(), {
273            let element_state = self.clone();
274
275            move |known_dimensions, available_space, cx| {
276                let wrap_width = if text_style.white_space == WhiteSpace::Normal {
277                    known_dimensions.width.or(match available_space.width {
278                        crate::AvailableSpace::Definite(x) => Some(x),
279                        _ => None,
280                    })
281                } else {
282                    None
283                };
284
285                let (truncate_width, ellipsis) = if let Some(truncate) = text_style.truncate {
286                    let width = known_dimensions.width.or(match available_space.width {
287                        crate::AvailableSpace::Definite(x) => Some(x),
288                        _ => None,
289                    });
290
291                    match truncate {
292                        Truncate::Truncate => (width, None),
293                        Truncate::Ellipsis => (width, Some(ELLIPSIS)),
294                    }
295                } else {
296                    (None, None)
297                };
298
299                if let Some(text_layout) = element_state.0.lock().as_ref() {
300                    if text_layout.size.is_some()
301                        && (wrap_width.is_none() || wrap_width == text_layout.wrap_width)
302                    {
303                        return text_layout.size.unwrap();
304                    }
305                }
306
307                let mut line_wrapper = cx.text_system().line_wrapper(text_style.font(), font_size);
308                let text = if let Some(truncate_width) = truncate_width {
309                    line_wrapper.truncate_line(text.clone(), truncate_width, ellipsis, &mut runs)
310                } else {
311                    text.clone()
312                };
313
314                let Some(lines) = cx
315                    .text_system()
316                    .shape_text(
317                        text, font_size, &runs, wrap_width, // Wrap if we know the width.
318                    )
319                    .log_err()
320                else {
321                    element_state.lock().replace(TextLayoutInner {
322                        lines: Default::default(),
323                        line_height,
324                        wrap_width,
325                        size: Some(Size::default()),
326                        bounds: None,
327                    });
328                    return Size::default();
329                };
330
331                let mut size: Size<Pixels> = Size::default();
332                for line in &lines {
333                    let line_size = line.size(line_height);
334                    size.height += line_size.height;
335                    size.width = size.width.max(line_size.width).ceil();
336                }
337
338                element_state.lock().replace(TextLayoutInner {
339                    lines,
340                    line_height,
341                    wrap_width,
342                    size: Some(size),
343                    bounds: None,
344                });
345
346                size
347            }
348        });
349
350        layout_id
351    }
352
353    fn prepaint(&self, bounds: Bounds<Pixels>, text: &str) {
354        let mut element_state = self.lock();
355        let element_state = element_state
356            .as_mut()
357            .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
358            .unwrap();
359        element_state.bounds = Some(bounds);
360    }
361
362    fn paint(&self, text: &str, cx: &mut WindowContext) {
363        let element_state = self.lock();
364        let element_state = element_state
365            .as_ref()
366            .ok_or_else(|| anyhow!("measurement has not been performed on {}", text))
367            .unwrap();
368        let bounds = element_state
369            .bounds
370            .ok_or_else(|| anyhow!("prepaint has not been performed on {:?}", text))
371            .unwrap();
372
373        let line_height = element_state.line_height;
374        let mut line_origin = bounds.origin;
375        for line in &element_state.lines {
376            line.paint(line_origin, line_height, cx).log_err();
377            line_origin.y += line.size(line_height).height;
378        }
379    }
380
381    /// Get the byte index into the input of the pixel position.
382    pub fn index_for_position(&self, mut position: Point<Pixels>) -> Result<usize, usize> {
383        let element_state = self.lock();
384        let element_state = element_state
385            .as_ref()
386            .expect("measurement has not been performed");
387        let bounds = element_state
388            .bounds
389            .expect("prepaint has not been performed");
390
391        if position.y < bounds.top() {
392            return Err(0);
393        }
394
395        let line_height = element_state.line_height;
396        let mut line_origin = bounds.origin;
397        let mut line_start_ix = 0;
398        for line in &element_state.lines {
399            let line_bottom = line_origin.y + line.size(line_height).height;
400            if position.y > line_bottom {
401                line_origin.y = line_bottom;
402                line_start_ix += line.len() + 1;
403            } else {
404                let position_within_line = position - line_origin;
405                match line.index_for_position(position_within_line, line_height) {
406                    Ok(index_within_line) => return Ok(line_start_ix + index_within_line),
407                    Err(index_within_line) => return Err(line_start_ix + index_within_line),
408                }
409            }
410        }
411
412        Err(line_start_ix.saturating_sub(1))
413    }
414
415    /// Get the pixel position for the given byte index.
416    pub fn position_for_index(&self, index: usize) -> Option<Point<Pixels>> {
417        let element_state = self.lock();
418        let element_state = element_state
419            .as_ref()
420            .expect("measurement has not been performed");
421        let bounds = element_state
422            .bounds
423            .expect("prepaint has not been performed");
424        let line_height = element_state.line_height;
425
426        let mut line_origin = bounds.origin;
427        let mut line_start_ix = 0;
428
429        for line in &element_state.lines {
430            let line_end_ix = line_start_ix + line.len();
431            if index < line_start_ix {
432                break;
433            } else if index > line_end_ix {
434                line_origin.y += line.size(line_height).height;
435                line_start_ix = line_end_ix + 1;
436                continue;
437            } else {
438                let ix_within_line = index - line_start_ix;
439                return Some(line_origin + line.position_for_index(ix_within_line, line_height)?);
440            }
441        }
442
443        None
444    }
445
446    /// Retrieve the layout for the line containing the given byte index.
447    pub fn line_layout_for_index(&self, index: usize) -> Option<Arc<WrappedLineLayout>> {
448        let element_state = self.lock();
449        let element_state = element_state
450            .as_ref()
451            .expect("measurement has not been performed");
452        let bounds = element_state
453            .bounds
454            .expect("prepaint has not been performed");
455        let line_height = element_state.line_height;
456
457        let mut line_origin = bounds.origin;
458        let mut line_start_ix = 0;
459
460        for line in &element_state.lines {
461            let line_end_ix = line_start_ix + line.len();
462            if index < line_start_ix {
463                break;
464            } else if index > line_end_ix {
465                line_origin.y += line.size(line_height).height;
466                line_start_ix = line_end_ix + 1;
467                continue;
468            } else {
469                return Some(line.layout.clone());
470            }
471        }
472
473        None
474    }
475
476    /// The bounds of this layout.
477    pub fn bounds(&self) -> Bounds<Pixels> {
478        self.0.lock().as_ref().unwrap().bounds.unwrap()
479    }
480
481    /// The line height for this layout.
482    pub fn line_height(&self) -> Pixels {
483        self.0.lock().as_ref().unwrap().line_height
484    }
485
486    /// The text for this layout.
487    pub fn text(&self) -> String {
488        self.0
489            .lock()
490            .as_ref()
491            .unwrap()
492            .lines
493            .iter()
494            .map(|s| s.text.to_string())
495            .collect::<Vec<_>>()
496            .join("\n")
497    }
498}
499
500/// A text element that can be interacted with.
501pub struct InteractiveText {
502    element_id: ElementId,
503    text: StyledText,
504    click_listener:
505        Option<Box<dyn Fn(&[Range<usize>], InteractiveTextClickEvent, &mut WindowContext)>>,
506    hover_listener: Option<Box<dyn Fn(Option<usize>, MouseMoveEvent, &mut WindowContext)>>,
507    tooltip_builder: Option<Rc<dyn Fn(usize, &mut WindowContext) -> Option<AnyView>>>,
508    clickable_ranges: Vec<Range<usize>>,
509}
510
511struct InteractiveTextClickEvent {
512    mouse_down_index: usize,
513    mouse_up_index: usize,
514}
515
516#[doc(hidden)]
517#[derive(Default)]
518pub struct InteractiveTextState {
519    mouse_down_index: Rc<Cell<Option<usize>>>,
520    hovered_index: Rc<Cell<Option<usize>>>,
521    active_tooltip: Rc<RefCell<Option<ActiveTooltip>>>,
522}
523
524/// InteractiveTest is a wrapper around StyledText that adds mouse interactions.
525impl InteractiveText {
526    /// Creates a new InteractiveText from the given text.
527    pub fn new(id: impl Into<ElementId>, text: StyledText) -> Self {
528        Self {
529            element_id: id.into(),
530            text,
531            click_listener: None,
532            hover_listener: None,
533            tooltip_builder: None,
534            clickable_ranges: Vec::new(),
535        }
536    }
537
538    /// on_click is called when the user clicks on one of the given ranges, passing the index of
539    /// the clicked range.
540    pub fn on_click(
541        mut self,
542        ranges: Vec<Range<usize>>,
543        listener: impl Fn(usize, &mut WindowContext) + 'static,
544    ) -> Self {
545        self.click_listener = Some(Box::new(move |ranges, event, cx| {
546            for (range_ix, range) in ranges.iter().enumerate() {
547                if range.contains(&event.mouse_down_index) && range.contains(&event.mouse_up_index)
548                {
549                    listener(range_ix, cx);
550                }
551            }
552        }));
553        self.clickable_ranges = ranges;
554        self
555    }
556
557    /// on_hover is called when the mouse moves over a character within the text, passing the
558    /// index of the hovered character, or None if the mouse leaves the text.
559    pub fn on_hover(
560        mut self,
561        listener: impl Fn(Option<usize>, MouseMoveEvent, &mut WindowContext) + 'static,
562    ) -> Self {
563        self.hover_listener = Some(Box::new(listener));
564        self
565    }
566
567    /// tooltip lets you specify a tooltip for a given character index in the string.
568    pub fn tooltip(
569        mut self,
570        builder: impl Fn(usize, &mut WindowContext) -> Option<AnyView> + 'static,
571    ) -> Self {
572        self.tooltip_builder = Some(Rc::new(builder));
573        self
574    }
575}
576
577impl Element for InteractiveText {
578    type RequestLayoutState = ();
579    type PrepaintState = Hitbox;
580
581    fn id(&self) -> Option<ElementId> {
582        Some(self.element_id.clone())
583    }
584
585    fn request_layout(
586        &mut self,
587        _id: Option<&GlobalElementId>,
588        cx: &mut WindowContext,
589    ) -> (LayoutId, Self::RequestLayoutState) {
590        self.text.request_layout(None, cx)
591    }
592
593    fn prepaint(
594        &mut self,
595        global_id: Option<&GlobalElementId>,
596        bounds: Bounds<Pixels>,
597        state: &mut Self::RequestLayoutState,
598        cx: &mut WindowContext,
599    ) -> Hitbox {
600        cx.with_optional_element_state::<InteractiveTextState, _>(
601            global_id,
602            |interactive_state, cx| {
603                let interactive_state = interactive_state
604                    .map(|interactive_state| interactive_state.unwrap_or_default());
605
606                if let Some(interactive_state) = interactive_state.as_ref() {
607                    if let Some(active_tooltip) = interactive_state.active_tooltip.borrow().as_ref()
608                    {
609                        if let Some(tooltip) = active_tooltip.tooltip.clone() {
610                            cx.set_tooltip(tooltip);
611                        }
612                    }
613                }
614
615                self.text.prepaint(None, bounds, state, cx);
616                let hitbox = cx.insert_hitbox(bounds, false);
617                (hitbox, interactive_state)
618            },
619        )
620    }
621
622    fn paint(
623        &mut self,
624        global_id: Option<&GlobalElementId>,
625        bounds: Bounds<Pixels>,
626        _: &mut Self::RequestLayoutState,
627        hitbox: &mut Hitbox,
628        cx: &mut WindowContext,
629    ) {
630        let text_layout = self.text.layout().clone();
631        cx.with_element_state::<InteractiveTextState, _>(
632            global_id.unwrap(),
633            |interactive_state, cx| {
634                let mut interactive_state = interactive_state.unwrap_or_default();
635                if let Some(click_listener) = self.click_listener.take() {
636                    let mouse_position = cx.mouse_position();
637                    if let Ok(ix) = text_layout.index_for_position(mouse_position) {
638                        if self
639                            .clickable_ranges
640                            .iter()
641                            .any(|range| range.contains(&ix))
642                        {
643                            cx.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
644                        }
645                    }
646
647                    let text_layout = text_layout.clone();
648                    let mouse_down = interactive_state.mouse_down_index.clone();
649                    if let Some(mouse_down_index) = mouse_down.get() {
650                        let hitbox = hitbox.clone();
651                        let clickable_ranges = mem::take(&mut self.clickable_ranges);
652                        cx.on_mouse_event(move |event: &MouseUpEvent, phase, cx| {
653                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
654                                if let Ok(mouse_up_index) =
655                                    text_layout.index_for_position(event.position)
656                                {
657                                    click_listener(
658                                        &clickable_ranges,
659                                        InteractiveTextClickEvent {
660                                            mouse_down_index,
661                                            mouse_up_index,
662                                        },
663                                        cx,
664                                    )
665                                }
666
667                                mouse_down.take();
668                                cx.refresh();
669                            }
670                        });
671                    } else {
672                        let hitbox = hitbox.clone();
673                        cx.on_mouse_event(move |event: &MouseDownEvent, phase, cx| {
674                            if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
675                                if let Ok(mouse_down_index) =
676                                    text_layout.index_for_position(event.position)
677                                {
678                                    mouse_down.set(Some(mouse_down_index));
679                                    cx.refresh();
680                                }
681                            }
682                        });
683                    }
684                }
685
686                cx.on_mouse_event({
687                    let mut hover_listener = self.hover_listener.take();
688                    let hitbox = hitbox.clone();
689                    let text_layout = text_layout.clone();
690                    let hovered_index = interactive_state.hovered_index.clone();
691                    move |event: &MouseMoveEvent, phase, cx| {
692                        if phase == DispatchPhase::Bubble && hitbox.is_hovered(cx) {
693                            let current = hovered_index.get();
694                            let updated = text_layout.index_for_position(event.position).ok();
695                            if current != updated {
696                                hovered_index.set(updated);
697                                if let Some(hover_listener) = hover_listener.as_ref() {
698                                    hover_listener(updated, event.clone(), cx);
699                                }
700                                cx.refresh();
701                            }
702                        }
703                    }
704                });
705
706                if let Some(tooltip_builder) = self.tooltip_builder.clone() {
707                    let hitbox = hitbox.clone();
708                    let source_bounds = hitbox.bounds;
709                    let active_tooltip = interactive_state.active_tooltip.clone();
710                    let pending_mouse_down = interactive_state.mouse_down_index.clone();
711                    let text_layout = text_layout.clone();
712
713                    cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
714                        let position = text_layout.index_for_position(event.position).ok();
715                        let is_hovered = position.is_some()
716                            && hitbox.is_hovered(cx)
717                            && pending_mouse_down.get().is_none();
718                        if !is_hovered {
719                            active_tooltip.take();
720                            return;
721                        }
722                        let position = position.unwrap();
723
724                        if phase != DispatchPhase::Bubble {
725                            return;
726                        }
727
728                        if active_tooltip.borrow().is_none() {
729                            let task = cx.spawn({
730                                let active_tooltip = active_tooltip.clone();
731                                let tooltip_builder = tooltip_builder.clone();
732
733                                move |mut cx| async move {
734                                    cx.background_executor().timer(TOOLTIP_DELAY).await;
735                                    cx.update(|cx| {
736                                        let new_tooltip =
737                                            tooltip_builder(position, cx).map(|tooltip| {
738                                                ActiveTooltip {
739                                                    tooltip: Some(AnyTooltip {
740                                                        view: tooltip,
741                                                        mouse_position: cx.mouse_position(),
742                                                        hoverable: true,
743                                                        origin_bounds: source_bounds,
744                                                    }),
745                                                    _task: None,
746                                                }
747                                            });
748                                        *active_tooltip.borrow_mut() = new_tooltip;
749                                        cx.refresh();
750                                    })
751                                    .ok();
752                                }
753                            });
754                            *active_tooltip.borrow_mut() = Some(ActiveTooltip {
755                                tooltip: None,
756                                _task: Some(task),
757                            });
758                        }
759                    });
760
761                    let active_tooltip = interactive_state.active_tooltip.clone();
762                    cx.on_mouse_event(move |_: &MouseDownEvent, _, _| {
763                        active_tooltip.take();
764                    });
765                }
766
767                self.text.paint(None, bounds, &mut (), &mut (), cx);
768
769                ((), interactive_state)
770            },
771        );
772    }
773}
774
775impl IntoElement for InteractiveText {
776    type Element = Self;
777
778    fn into_element(self) -> Self::Element {
779        self
780    }
781}