1mod rich;
20
21use iced_widget::graphics::text::Paragraph;
22use iced_widget::graphics::text::cosmic_text;
23pub use rich::Rich;
24use text::{Alignment, Ellipsis, LineHeight, Shaping, Wrapping};
25pub use text::{Fragment, Highlighter, IntoFragment, Span};
26
27use crate::click;
28use crate::core::alignment;
29use crate::core::keyboard::{self, key};
30use crate::core::layout;
31use crate::core::mouse;
32use crate::core::renderer;
33use crate::core::text;
34use crate::core::text::Paragraph as _;
35use crate::core::time::Duration;
36use crate::core::touch;
37use crate::core::widget::Operation;
38use crate::core::widget::text::Format;
39use crate::core::widget::tree::{self, Tree};
40use crate::core::{
41 self, Color, Element, Event, Font, Layout, Length, Pixels, Point, Size,
42 Theme, Widget,
43};
44use crate::selection::{Selection, SelectionEnd};
45
46#[must_use]
63pub struct Text<
64 'a,
65 Theme = iced_widget::Theme,
66 Renderer = iced_widget::Renderer,
67> where
68 Theme: Catalog,
69 Renderer: text::Renderer,
70{
71 fragment: Fragment<'a>,
72 format: Format<Renderer::Font>,
73 click_interval: Option<Duration>,
74 class: Theme::Class<'a>,
75}
76
77impl<'a, Theme, Renderer> Text<'a, Theme, Renderer>
78where
79 Theme: Catalog,
80 Renderer: text::Renderer,
81{
82 pub fn new(fragment: impl IntoFragment<'a>) -> Self {
84 Self {
85 fragment: fragment.into_fragment(),
86 format: Format::default(),
87 click_interval: None,
88 class: Theme::default(),
89 }
90 }
91
92 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
94 self.format.size = Some(size.into());
95 self
96 }
97
98 pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
100 self.format.line_height = line_height.into();
101 self
102 }
103
104 pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
106 self.format.font = Some(font.into());
107 self
108 }
109
110 pub fn width(mut self, width: impl Into<Length>) -> Self {
112 self.format.width = width.into();
113 self
114 }
115
116 pub fn height(mut self, height: impl Into<Length>) -> Self {
118 self.format.height = height.into();
119 self
120 }
121
122 pub fn center(mut self) -> Self {
124 self.format.align_x = Alignment::Center;
125 self.format.align_y = alignment::Vertical::Center;
126 self
127 }
128
129 pub fn align_x(mut self, alignment: impl Into<text::Alignment>) -> Self {
131 self.format.align_x = alignment.into();
132 self
133 }
134
135 pub fn align_y(
137 mut self,
138 alignment: impl Into<alignment::Vertical>,
139 ) -> Self {
140 self.format.align_y = alignment.into();
141 self
142 }
143
144 pub fn shaping(mut self, shaping: Shaping) -> Self {
146 self.format.shaping = shaping;
147 self
148 }
149
150 pub fn wrapping(mut self, wrapping: Wrapping) -> Self {
152 self.format.wrapping = wrapping;
153 self
154 }
155
156 pub fn ellipsis(mut self, ellipsis: Ellipsis) -> Self {
158 self.format.ellipsis = ellipsis;
159 self
160 }
161
162 pub fn click_interval(mut self, click_interval: Duration) -> Self {
167 self.click_interval = Some(click_interval);
168 self
169 }
170
171 pub fn style(mut self, style: impl Fn(&Theme) -> Style + 'a) -> Self
173 where
174 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
175 {
176 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
177 self
178 }
179
180 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
182 self.class = class.into();
183 self
184 }
185}
186
187#[derive(Debug, Default, Clone)]
189pub struct State {
190 paragraph: Paragraph,
191 content: String,
192 is_hovered: bool,
193 selection: Selection,
194 dragging: Option<Dragging>,
195 last_click: Option<click::Click>,
196 keyboard_modifiers: keyboard::Modifiers,
197 visual_lines_bounds: Vec<core::Rectangle>,
198}
199
200#[derive(Debug, Clone, Copy, PartialEq, Eq)]
202#[allow(missing_docs)]
203pub enum Dragging {
204 Grapheme,
205 Word,
206 Line,
207}
208
209impl State {
210 fn grapheme_line_and_index(
211 &self,
212 point: Point,
213 bounds: core::Rectangle,
214 ) -> Option<(usize, usize)> {
215 let bounded_x = if point.y < bounds.y {
216 bounds.x
217 } else if point.y > bounds.y + bounds.height {
218 bounds.x + bounds.width
219 } else {
220 point.x.max(bounds.x).min(bounds.x + bounds.width)
221 };
222 let bounded_y = point.y.max(bounds.y).min(bounds.y + bounds.height);
223 let bounded_point = Point::new(bounded_x, bounded_y);
224 let mut relative_point =
225 bounded_point - core::Vector::new(bounds.x, bounds.y);
226
227 let buffer = self.paragraph.buffer();
228 let line_height = buffer.metrics().line_height;
229 let visual_line = (relative_point.y / line_height).floor() as usize;
230 let visual_line_start_offset = self
231 .visual_lines_bounds
232 .get(visual_line)
233 .map(|r| r.x)
234 .unwrap_or_default();
235 let visual_line_end = self
236 .visual_lines_bounds
237 .get(visual_line)
238 .map(|r| r.x + r.width)
239 .unwrap_or_default();
240
241 if relative_point.x < visual_line_start_offset {
242 relative_point.x = visual_line_start_offset;
243 }
244
245 if relative_point.x > visual_line_end {
246 relative_point.x = visual_line_end;
247 }
248
249 let cursor = buffer.hit(relative_point.x, relative_point.y)?;
250 let value = buffer.lines[cursor.line].text();
251
252 Some((
253 cursor.line,
254 unicode_segmentation::UnicodeSegmentation::graphemes(
255 &value[..cursor.index.min(value.len())],
256 true,
257 )
258 .count(),
259 ))
260 }
261
262 fn selection(&self) -> Vec<core::Rectangle> {
263 let Selection { start, end, .. } = self.selection;
264
265 let buffer = self.paragraph.buffer();
266 let line_height = self.paragraph.buffer().metrics().line_height;
267 let selected_lines = end.line - start.line + 1;
268
269 let visual_lines_offset = visual_lines_offset(start.line, buffer);
270
271 buffer
272 .lines
273 .iter()
274 .skip(start.line)
275 .take(selected_lines)
276 .enumerate()
277 .flat_map(|(i, line)| {
278 highlight_line(
279 line,
280 if i == 0 { start.index } else { 0 },
281 if i == selected_lines - 1 {
282 end.index
283 } else {
284 line.text().len()
285 },
286 )
287 })
288 .enumerate()
289 .filter_map(|(visual_line, (x, width))| {
290 if width > 0.0 {
291 Some(core::Rectangle {
292 x,
293 width,
294 y: (visual_line as i32 + visual_lines_offset) as f32
295 * line_height
296 - buffer.scroll().vertical,
297 height: line_height,
298 })
299 } else {
300 None
301 }
302 })
303 .collect()
304 }
305
306 fn update(&mut self, text: text::Text<&str, Font>) {
307 if self.content != text.content {
308 text.content.clone_into(&mut self.content);
309 self.paragraph = Paragraph::with_text(text);
310 self.update_visual_bounds();
311 return;
312 }
313
314 match self.paragraph.compare(text.with_content(())) {
315 text::Difference::None => {}
316 text::Difference::Bounds => {
317 self.paragraph.resize(text.bounds);
318 self.update_visual_bounds();
319 }
320 text::Difference::Shape => {
321 self.paragraph = Paragraph::with_text(text);
322 self.update_visual_bounds();
323 }
324 }
325 }
326
327 fn update_visual_bounds(&mut self) {
328 let buffer = self.paragraph.buffer();
329 let line_height = buffer.metrics().line_height;
330 self.visual_lines_bounds = buffer
331 .lines
332 .iter()
333 .flat_map(|line| highlight_line(line, 0, line.text().len()))
334 .enumerate()
335 .map(|(visual_line, (x, width))| core::Rectangle {
336 x,
337 width,
338 y: visual_line as f32 * line_height - buffer.scroll().vertical,
339 height: line_height,
340 })
341 .collect();
342 }
343}
344
345impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
346 for Text<'_, Theme, Renderer>
347where
348 Theme: Catalog,
349 Renderer: text::Renderer<Paragraph = Paragraph, Font = Font>,
350{
351 fn tag(&self) -> tree::Tag {
352 tree::Tag::of::<State>()
353 }
354
355 fn state(&self) -> tree::State {
356 tree::State::new(State::default())
357 }
358
359 fn size(&self) -> Size<Length> {
360 Size {
361 width: self.format.width,
362 height: self.format.height,
363 }
364 }
365
366 fn layout(
367 &mut self,
368 tree: &mut Tree,
369 renderer: &Renderer,
370 limits: &layout::Limits,
371 ) -> layout::Node {
372 layout(
373 tree.state.downcast_mut::<State>(),
374 renderer,
375 limits,
376 &self.fragment,
377 self.format,
378 )
379 }
380
381 fn update(
382 &mut self,
383 tree: &mut Tree,
384 event: &Event,
385 layout: Layout<'_>,
386 cursor: mouse::Cursor,
387 _renderer: &Renderer,
388 shell: &mut core::Shell<'_, Message>,
389 viewport: &core::Rectangle,
390 ) {
391 let state = tree.state.downcast_mut::<State>();
392
393 let bounds = layout.bounds();
394 let click_position = cursor.position_in(bounds);
395
396 if viewport.intersection(&bounds).is_none()
397 && state.selection.is_empty()
398 && state.dragging.is_none()
399 {
400 return;
401 }
402
403 let was_hovered = state.is_hovered;
404 let selection_before = state.selection;
405 state.is_hovered = click_position.is_some();
406
407 match event {
408 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
409 | Event::Touch(touch::Event::FingerPressed { .. }) => {
410 if let Some(position) = cursor.position_over(bounds) {
411 let click = click::Click::new(
412 position,
413 mouse::Button::Left,
414 state.last_click,
415 self.click_interval,
416 );
417
418 let (line, index) = state
419 .grapheme_line_and_index(position, bounds)
420 .unwrap_or((0, 0));
421
422 match click.kind() {
423 click::Kind::Single => {
424 let new_end = SelectionEnd { line, index };
425
426 if state.keyboard_modifiers.shift() {
427 state.selection.change_selection(new_end);
428 } else {
429 state.selection.select_range(new_end, new_end);
430 }
431
432 state.dragging = Some(Dragging::Grapheme);
433 }
434 click::Kind::Double => {
435 state.selection.select_word(
436 line,
437 index,
438 &state.paragraph,
439 );
440
441 state.dragging = Some(Dragging::Word);
442 }
443 click::Kind::Triple => {
444 state.selection.select_line(line, &state.paragraph);
445 state.dragging = Some(Dragging::Line);
446 }
447 }
448
449 state.last_click = Some(click);
450
451 shell.capture_event();
452 } else {
453 state.selection = Selection::default();
454 }
455 }
456 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
457 | Event::Touch(touch::Event::FingerLifted { .. })
458 | Event::Touch(touch::Event::FingerLost { .. }) => {
459 state.dragging = None;
460 }
461 Event::Mouse(mouse::Event::CursorMoved { .. })
462 | Event::Touch(touch::Event::FingerMoved { .. }) => {
463 if let Some(position) = cursor.land().position()
464 && let Some(dragging) = state.dragging
465 {
466 let (line, index) = state
467 .grapheme_line_and_index(position, bounds)
468 .unwrap_or((0, 0));
469
470 match dragging {
471 Dragging::Grapheme => {
472 let new_end = SelectionEnd { line, index };
473
474 state.selection.change_selection(new_end);
475 }
476 Dragging::Word => {
477 let new_end = SelectionEnd { line, index };
478
479 state.selection.change_selection_by_word(
480 new_end,
481 &state.paragraph,
482 );
483 }
484 Dragging::Line => {
485 state.selection.change_selection_by_line(
486 line,
487 &state.paragraph,
488 );
489 }
490 };
491 }
492 }
493 Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => {
494 match key.as_ref() {
495 keyboard::Key::Character("c")
496 if state.keyboard_modifiers.command()
497 && !state.selection.is_empty() =>
498 {
499 shell.write_clipboard(
500 state.selection.text(&state.paragraph).into(),
501 );
502
503 shell.capture_event();
504 }
505 keyboard::Key::Character("a")
506 if state.keyboard_modifiers.command()
507 && !state.selection.is_empty() =>
508 {
509 state.selection.select_all(&state.paragraph);
510
511 shell.capture_event();
512 }
513 keyboard::Key::Named(key::Named::Home)
514 if state.keyboard_modifiers.shift()
515 && !state.selection.is_empty() =>
516 {
517 if state.keyboard_modifiers.jump() {
518 state.selection.select_beginning();
519 } else {
520 state.selection.select_line_beginning();
521 }
522
523 shell.capture_event();
524 }
525 keyboard::Key::Named(key::Named::End)
526 if state.keyboard_modifiers.shift()
527 && !state.selection.is_empty() =>
528 {
529 if state.keyboard_modifiers.jump() {
530 state.selection.select_end(&state.paragraph);
531 } else {
532 state.selection.select_line_end(&state.paragraph);
533 }
534
535 shell.capture_event();
536 }
537 keyboard::Key::Named(key::Named::ArrowLeft)
538 if state.keyboard_modifiers.shift()
539 && !state.selection.is_empty() =>
540 {
541 if state.keyboard_modifiers.macos_command() {
542 state.selection.select_line_beginning();
543 } else if state.keyboard_modifiers.jump() {
544 state
545 .selection
546 .select_left_by_words(&state.paragraph);
547 } else {
548 state.selection.select_left(&state.paragraph);
549 }
550
551 shell.capture_event();
552 }
553 keyboard::Key::Named(key::Named::ArrowRight)
554 if state.keyboard_modifiers.shift()
555 && !state.selection.is_empty() =>
556 {
557 if state.keyboard_modifiers.macos_command() {
558 state.selection.select_line_end(&state.paragraph);
559 } else if state.keyboard_modifiers.jump() {
560 state
561 .selection
562 .select_right_by_words(&state.paragraph);
563 } else {
564 state.selection.select_right(&state.paragraph);
565 }
566
567 shell.capture_event();
568 }
569 keyboard::Key::Named(key::Named::ArrowUp)
570 if state.keyboard_modifiers.shift()
571 && !state.selection.is_empty() =>
572 {
573 if state.keyboard_modifiers.macos_command() {
574 state.selection.select_beginning();
575 } else if state.keyboard_modifiers.jump() {
576 state.selection.select_line_beginning();
577 } else {
578 state.selection.select_up(&state.paragraph);
579 }
580
581 shell.capture_event();
582 }
583 keyboard::Key::Named(key::Named::ArrowDown)
584 if state.keyboard_modifiers.shift()
585 && !state.selection.is_empty() =>
586 {
587 if state.keyboard_modifiers.macos_command() {
588 state.selection.select_end(&state.paragraph);
589 } else if state.keyboard_modifiers.jump() {
590 state.selection.select_line_end(&state.paragraph);
591 } else {
592 state.selection.select_down(&state.paragraph);
593 }
594
595 shell.capture_event();
596 }
597 keyboard::Key::Named(key::Named::Escape) => {
598 state.dragging = None;
599 state.selection = Selection::default();
600
601 state.keyboard_modifiers =
602 keyboard::Modifiers::default();
603
604 if state.selection != selection_before {
605 shell.capture_event();
606 }
607 }
608 _ => {}
609 }
610 }
611 Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => {
612 state.keyboard_modifiers = *modifiers;
613 }
614 _ => {}
615 }
616
617 if state.is_hovered != was_hovered
618 || state.selection != selection_before
619 {
620 shell.request_redraw();
621 }
622 }
623
624 fn draw(
625 &self,
626 tree: &Tree,
627 renderer: &mut Renderer,
628 theme: &Theme,
629 defaults: &renderer::Style,
630 layout: Layout<'_>,
631 _cursor_position: mouse::Cursor,
632 viewport: &core::Rectangle,
633 ) {
634 if !layout.bounds().intersects(viewport) {
635 return;
636 }
637
638 let state = tree.state.downcast_ref::<State>();
639 let style = theme.style(&self.class);
640
641 if !state.selection.is_empty() {
642 let bounds = layout.bounds();
643 let translation = bounds.position() - Point::ORIGIN;
644 let ranges = state.selection();
645
646 for range in ranges
647 .into_iter()
648 .filter_map(|range| bounds.intersection(&(range + translation)))
649 {
650 renderer.fill_quad(
651 renderer::Quad {
652 bounds: range,
653 ..renderer::Quad::default()
654 },
655 style.selection,
656 );
657 }
658 }
659
660 draw(
661 renderer,
662 defaults,
663 layout.bounds(),
664 &state.paragraph,
665 style,
666 viewport,
667 );
668 }
669
670 fn operate(
671 &mut self,
672 _state: &mut Tree,
673 layout: Layout<'_>,
674 _renderer: &Renderer,
675 operation: &mut dyn Operation,
676 ) {
677 operation.text(None, layout.bounds(), &self.fragment);
678 }
679
680 fn mouse_interaction(
681 &self,
682 tree: &Tree,
683 _layout: Layout<'_>,
684 _cursor: mouse::Cursor,
685 _viewport: &core::Rectangle,
686 _renderer: &Renderer,
687 ) -> mouse::Interaction {
688 let state = tree.state.downcast_ref::<State>();
689
690 if state.is_hovered || state.dragging.is_some() {
691 mouse::Interaction::Text
692 } else {
693 mouse::Interaction::default()
694 }
695 }
696}
697
698pub fn layout<Renderer>(
702 state: &mut State,
703 renderer: &Renderer,
704 limits: &layout::Limits,
705 content: &str,
706 format: Format<Font>,
707) -> layout::Node
708where
709 Renderer: text::Renderer<Paragraph = Paragraph, Font = Font>,
710{
711 layout::sized(limits, format.width, format.height, |limits| {
712 let bounds = limits.max();
713
714 let size = format.size.unwrap_or_else(|| renderer.default_size());
715 let font = format.font.unwrap_or_else(|| renderer.default_font());
716
717 state.update(text::Text {
718 content,
719 bounds,
720 size,
721 line_height: format.line_height,
722 font,
723 align_x: format.align_x,
724 align_y: format.align_y,
725 shaping: format.shaping,
726 wrapping: format.wrapping,
727 ellipsis: format.ellipsis,
728 hint_factor: renderer.scale_factor(),
729 });
730
731 state.paragraph.min_bounds()
732 })
733}
734
735pub fn draw<Renderer>(
737 renderer: &mut Renderer,
738 style: &renderer::Style,
739 bounds: core::Rectangle,
740 paragraph: &Paragraph,
741 appearance: Style,
742 viewport: &core::Rectangle,
743) where
744 Renderer: text::Renderer<Paragraph = Paragraph, Font = Font>,
745{
746 let anchor = bounds.anchor(
747 paragraph.min_bounds(),
748 paragraph.align_x(),
749 paragraph.align_y(),
750 );
751
752 renderer.fill_paragraph(
753 paragraph,
754 anchor,
755 appearance.color.unwrap_or(style.text_color),
756 *viewport,
757 );
758}
759
760impl<'a, Message, Theme, Renderer> From<Text<'a, Theme, Renderer>>
761 for Element<'a, Message, Theme, Renderer>
762where
763 Theme: Catalog + 'a,
764 Renderer: text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
765{
766 fn from(
767 text: Text<'a, Theme, Renderer>,
768 ) -> Element<'a, Message, Theme, Renderer> {
769 Element::new(text)
770 }
771}
772
773impl<'a, Theme, Renderer> From<&'a str> for Text<'a, Theme, Renderer>
774where
775 Theme: Catalog + 'a,
776 Renderer: text::Renderer<Paragraph = Paragraph, Font = Font> + 'a,
777{
778 fn from(content: &'a str) -> Self {
779 Self::new(content)
780 }
781}
782
783#[derive(Debug, Clone, Copy, PartialEq, Default)]
785pub struct Style {
786 pub color: Option<Color>,
790 pub selection: Color,
792}
793
794pub trait Catalog: Sized {
796 type Class<'a>;
798
799 fn default<'a>() -> Self::Class<'a>;
801
802 fn style(&self, item: &Self::Class<'_>) -> Style;
804}
805
806pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme) -> Style + 'a>;
810
811impl Catalog for Theme {
812 type Class<'a> = StyleFn<'a, Self>;
813
814 fn default<'a>() -> Self::Class<'a> {
815 Box::new(default)
816 }
817
818 fn style(&self, class: &Self::Class<'_>) -> Style {
819 class(self)
820 }
821}
822
823pub fn default(theme: &Theme) -> Style {
825 Style {
826 color: None,
827 selection: theme.palette().primary.weak.color,
828 }
829}
830
831fn highlight_line(
832 line: &cosmic_text::BufferLine,
833 from: usize,
834 to: usize,
835) -> impl Iterator<Item = (f32, f32)> + '_ {
836 let layout = line.layout_opt().map(Vec::as_slice).unwrap_or_default();
837
838 let mut previous_diff = 0;
840 let previous_lines_diff = line
841 .layout_opt()
842 .map(Vec::as_slice)
843 .unwrap_or_default()
844 .iter()
845 .enumerate()
846 .map(move |(line_nr, visual_line)| {
847 if line_nr == 0 {
848 let current_diff = previous_diff
849 + visual_line.glyphs.iter().fold(0, |d, g| {
850 (d + g.start.abs_diff(g.end)).saturating_sub(1)
851 });
852 previous_diff = current_diff;
853 0
854 } else {
855 let current_diff = previous_diff
856 + visual_line
857 .glyphs
858 .iter()
859 .fold(0, |d, g| d + g.start.abs_diff(g.end) - 1);
860 let previous_diff_temp = previous_diff;
861 previous_diff = current_diff;
862 previous_diff_temp
863 }
864 });
865
866 layout.iter().zip(previous_lines_diff).map(
867 move |(visual_line, previous_lines_diff)| {
868 let start = visual_line
869 .glyphs
870 .first()
871 .map(|glyph| glyph.start)
872 .unwrap_or(0);
873 let end = visual_line
874 .glyphs
875 .last()
876 .map(|glyph| glyph.end)
877 .unwrap_or(0);
878
879 let to = to + previous_lines_diff;
880 let mut range = start.max(from)..end.min(to);
881
882 let x_offset = visual_line
883 .glyphs
884 .first()
885 .map(|glyph| glyph.x)
886 .unwrap_or_default();
887
888 if range.is_empty() {
889 (x_offset, 0.0)
890 } else if range.start == start && range.end == end {
891 (x_offset, visual_line.w)
892 } else {
893 let mut x = 0.0;
894 let mut width = 0.0;
895 for glyph in &visual_line.glyphs {
896 let glyph_count = glyph.start.abs_diff(glyph.end);
897
898 if glyph_count > 1 {
900 if range.start > glyph.start {
901 range.start += glyph_count - 1;
902 range.end += glyph_count - 1;
903 } else if range.end > glyph.start {
904 range.end += glyph_count - 1;
905 }
906 }
907
908 if range.start > glyph.start {
909 x += glyph.w;
910 }
911
912 if range.start <= glyph.start && range.end > glyph.start {
913 width += glyph.w;
914 } else if range.end <= glyph.start {
915 break;
916 }
917 }
918
919 (x_offset + x, width)
920 }
921 },
922 )
923}
924
925fn visual_lines_offset(line: usize, buffer: &cosmic_text::Buffer) -> i32 {
926 let scroll = buffer.scroll();
927
928 let start = scroll.line.min(line);
929 let end = scroll.line.max(line);
930
931 let visual_lines_offset: usize = buffer.lines[start..]
932 .iter()
933 .take(end - start)
934 .map(|line| line.layout_opt().map(Vec::len).unwrap_or_default())
935 .sum();
936
937 visual_lines_offset as i32 * if scroll.line < line { 1 } else { -1 }
938}