1use std::{cell::RefCell, ops::Range, rc::Rc};
11
12use gpui::{
13 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
14 InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString, StrikethroughStyle,
15 StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window, canvas, div, font,
16 img, point, prelude::*, px, quad, size,
17};
18use theme::{TextStyle, Theme, Typeset};
19
20use crate::{
21 block,
22 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
23 layout::Layout,
24 preview,
25 select::{Cursor, Selection},
26 typography::Typography,
27};
28
29const BLOCK_GAP: f32 = 12.0;
31const LIST_GAP: f32 = 4.0;
32const INDENT_WIDTH: f32 = 22.0;
34const MARKER_WIDTH: f32 = 18.0;
36const MARKER_GAP: f32 = 8.0;
37const CODE_PADDING_X: f32 = 12.0;
39const CODE_PADDING_Y: f32 = 10.0;
40pub const PLAIN_LANGUAGE: &str = "Plain";
43const CARET_WIDTH: f32 = 1.5;
46const INLINE_CODE_RADIUS: f32 = 4.5;
49const INLINE_CODE_PAD_X: f32 = 2.0;
50const INLINE_CODE_INSET_Y: f32 = 2.0;
51const CHIP_PAD_X: f32 = 4.0;
54const CHIP_INSET_Y: f32 = 1.0;
55const CHIP_BLOCK_PAD_X: f32 = 8.0;
58const CHIP_BLOCK_PAD_Y: f32 = 3.0;
59const CHIP_ICON: f32 = 15.0;
60const CARD_HEIGHT: f32 = 116.0;
64const CARD_IMAGE_WIDTH: f32 = 180.0;
65const CARD_COVER_HEIGHT: f32 = 200.0;
66const CARD_PADDING: f32 = 14.0;
67const CARD_BORDER: f32 = 1.0;
68const CARD_ICON: f32 = 16.0;
69const CARD_COVER: f32 = 44.0;
70const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
72const CAPTION_GAP: f32 = 4.0;
73const IMAGE_EMPTY: &str = "Add an image";
75const CAPTION_HINT: &str = "Write a caption";
76const TABLE_CELL_PADDING: f32 = 12.0;
79const TABLE_DIVIDER: f32 = 1.0;
80const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
83const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
85
86#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
97pub enum Caption {
98 #[default]
100 Shown,
101 Hidden,
103}
104
105#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
109pub enum CopyButton {
110 #[default]
112 Shown,
113 Hidden,
116}
117
118#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
125pub enum Annotation {
126 #[default]
128 Open,
129 Resolved,
131 Active,
133}
134
135impl Annotation {
136 fn wash(self, theme: &Theme) -> Hsla {
137 match self {
138 Self::Open => theme.warning.opacity(0.20),
139 Self::Resolved => theme.warning.opacity(0.08),
140 Self::Active => theme.warning.opacity(0.38),
141 }
142 }
143}
144
145pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
150
151#[derive(Clone)]
157pub enum Toggle {
158 Handled(OnToggle),
161 HitTested,
167}
168
169#[derive(Clone)]
175pub struct Editing<'a> {
176 pub selection: Option<Selection>,
179 pub caret_on: bool,
182 pub layouts: Option<&'a BlockLayouts>,
184 pub annotations: &'a [(Selection, Annotation)],
186 pub placeholder: Option<SharedString>,
188 pub caption: Caption,
189 pub typography: Option<Typography>,
193 pub toggle: Option<Toggle>,
196 pub copy: CopyButton,
198}
199
200impl Default for Editing<'_> {
201 fn default() -> Self {
202 Self {
203 selection: None,
204 caret_on: true,
207 layouts: None,
208 annotations: &[],
209 placeholder: None,
210 caption: Caption::default(),
211 typography: None,
212 toggle: None,
213 copy: CopyButton::default(),
214 }
215 }
216}
217
218#[derive(Clone, Default)]
225pub struct BlockLayouts(Rc<RefCell<Frames>>);
226
227#[derive(Default)]
228struct Frames {
229 texts: Vec<Painted>,
230 rows: Vec<PaintedRow>,
231 blocks: Vec<(usize, Bounds<Pixels>)>,
234 languages: Vec<(usize, Bounds<Pixels>)>,
237 pictures: Vec<(usize, Bounds<Pixels>)>,
241 checkboxes: Vec<(usize, Bounds<Pixels>)>,
244}
245
246struct Painted {
253 block: usize,
254 part: Part,
255 range: Range<usize>,
256 layout: TextLayout,
257}
258
259struct PaintedRow {
260 painted: usize,
261 block: usize,
262 part: Part,
263 range: Range<usize>,
264 bounds: Bounds<Pixels>,
265}
266
267impl BlockLayouts {
268 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
274 let frames = self.0.borrow();
275 if let Some(row) = frames.rows.iter().find(|row| row.bounds.contains(&point)) {
276 return Some(cursor_in_row(&frames, row, point.x));
277 }
278 frames
279 .rows
280 .iter()
281 .min_by_key(|row| {
282 let bounds = row.bounds;
283 let above = (bounds.origin.y - point.y).abs();
284 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
285 f32::from(above.min(below)) as i64
286 })
287 .map(|row| cursor_in_row(&frames, row, point.x))
288 }
289
290 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
296 let frames = self.0.borrow();
297 let painted = frames.texts.iter().find(|painted| {
298 painted.block == at.block
299 && painted.part == at.part
300 && painted.range.start <= at.offset
301 && at.offset <= painted.range.end
302 })?;
303 let point = painted
304 .layout
305 .position_for_index(at.offset - painted.range.start)?;
306 Some((point, painted.layout.line_height()))
307 }
308
309 pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
317 let (start, end) = selection.ordered();
318 self.0
319 .borrow()
320 .texts
321 .iter()
322 .filter_map(|painted| {
323 let here = Cursor::new(painted.block, painted.part, 0);
324 let (from, to) = (
325 Cursor::new(start.block, start.part, 0),
326 Cursor::new(end.block, end.part, 0),
327 );
328 if here < from || here > to {
329 return None;
330 }
331 let len = painted.range.len();
334 let first = if here == from { start.offset } else { 0 };
335 let last = if here == to { end.offset } else { usize::MAX };
336 let range = first.saturating_sub(painted.range.start).min(len)
337 ..last.saturating_sub(painted.range.start).min(len);
338 (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
339 })
340 .flatten()
341 .collect()
342 }
343
344 pub fn step_row(
355 &self,
356 at: Cursor,
357 from: Point<Pixels>,
358 down: bool,
359 ) -> Option<(Cursor, Pixels)> {
360 let frames = self.0.borrow();
361 let ix = frames
362 .rows
363 .iter()
364 .position(|row| {
365 row.block == at.block
366 && row.part == at.part
367 && row_contains(row, at.offset)
368 && row.bounds.origin.y <= from.y
369 && from.y < row.bounds.origin.y + row.bounds.size.height
370 })
371 .or_else(|| {
372 frames.rows.iter().position(|row| {
373 row.block == at.block && row.part == at.part && row_contains(row, at.offset)
374 })
375 })?;
376 let next = match down {
377 true => frames.rows.get(ix + 1)?,
378 false => frames.rows.get(ix.checked_sub(1)?)?,
379 };
380 Some((cursor_in_row(&frames, next, from.x), next.bounds.origin.y))
381 }
382
383 pub fn over_text(&self, point: Point<Pixels>) -> bool {
390 self.0
391 .borrow()
392 .texts
393 .iter()
394 .any(|painted| painted.layout.bounds().contains(&point))
395 }
396
397 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
399 let blocks = &self.0.borrow().blocks;
400 blocks
401 .iter()
402 .find(|(_, bounds)| bounds.contains(&point))
403 .or_else(|| {
404 blocks.iter().min_by_key(|(_, bounds)| {
405 let above = (bounds.origin.y - point.y).abs();
406 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
407 f32::from(above.min(below)) as i64
408 })
409 })
410 .map(|(ix, _)| *ix)
411 }
412
413 pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
422 let texts = &self.0.borrow().texts;
423 let painted = texts.iter().find(|painted| painted.block == ix)?;
424 Some((
425 painted.layout.bounds().origin.y,
426 painted.layout.line_height(),
427 ))
428 }
429
430 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
432 self.0
433 .borrow()
434 .blocks
435 .iter()
436 .find(|(block, _)| *block == ix)
437 .map(|(_, bounds)| *bounds)
438 }
439
440 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
445 self.0
446 .borrow()
447 .languages
448 .iter()
449 .find(|(block, _)| *block == ix)
450 .map(|(_, bounds)| *bounds)
451 }
452
453 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
458 self.0
459 .borrow()
460 .pictures
461 .iter()
462 .find(|(block, _)| *block == ix)
463 .map(|(_, bounds)| *bounds)
464 }
465
466 pub fn checkbox_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
470 self.0
471 .borrow()
472 .checkboxes
473 .iter()
474 .find(|(block, _)| *block == ix)
475 .map(|(_, bounds)| *bounds)
476 }
477
478 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
479 let mut frames = self.0.borrow_mut();
480 let painted = frames.texts.len();
481 record_rows(&mut frames.rows, painted, block, part, &range, &layout);
482 frames.texts.push(Painted {
483 block,
484 part,
485 range,
486 layout,
487 });
488 }
489
490 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
491 self.0.borrow_mut().blocks.push((ix, bounds));
492 }
493
494 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
495 self.0.borrow_mut().languages.push((ix, bounds));
496 }
497
498 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
499 self.0.borrow_mut().pictures.push((ix, bounds));
500 }
501
502 fn record_checkbox(&self, ix: usize, bounds: Bounds<Pixels>) {
503 self.0.borrow_mut().checkboxes.push((ix, bounds));
504 }
505
506 fn clear(&self) {
507 let mut frames = self.0.borrow_mut();
508 frames.texts.clear();
509 frames.rows.clear();
510 frames.blocks.clear();
511 frames.languages.clear();
512 frames.pictures.clear();
513 frames.checkboxes.clear();
514 }
515}
516
517fn row_contains(row: &PaintedRow, offset: usize) -> bool {
518 row.range.start <= offset && offset <= row.range.end
519}
520
521fn cursor_in_row(frames: &Frames, row: &PaintedRow, x: Pixels) -> Cursor {
522 let painted = &frames.texts[row.painted];
523 let y = row.bounds.origin.y + row.bounds.size.height / 2.0;
524 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(x, y));
525 Cursor::new(
526 painted.block,
527 painted.part,
528 painted.range.start + offset.min(painted.range.len()),
529 )
530}
531
532fn record_rows(
533 rows: &mut Vec<PaintedRow>,
534 painted: usize,
535 block: usize,
536 part: Part,
537 range: &Range<usize>,
538 layout: &TextLayout,
539) {
540 let line_height = layout.line_height();
541 let bounds = layout.bounds();
542 let mut origin = bounds.origin;
543 let mut line_start = range.start;
544 for line in layout.line_layouts() {
545 let shaped = &line.unwrapped_layout;
546 let row_ends = line
547 .wrap_boundaries()
548 .iter()
549 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
550 .chain([line.len()]);
551 let mut row_start = 0;
552 for (row, row_end) in row_ends.enumerate() {
553 rows.push(PaintedRow {
554 painted,
555 block,
556 part,
557 range: line_start + row_start..line_start + row_end,
558 bounds: Bounds::new(
559 origin + point(px(0.0), line_height * row as f32),
560 size(bounds.size.width, line_height),
561 ),
562 });
563 row_start = row_end;
564 }
565 origin.y += line.size(line_height).height;
566 line_start += line.len() + 1;
567 }
568}
569
570#[derive(Clone, Copy)]
576struct Overlay<'a> {
577 block: usize,
578 part: Part,
579 selection: Option<Selection>,
580 caret_on: bool,
581 layouts: Option<&'a BlockLayouts>,
582 annotations: &'a [(Selection, Annotation)],
584 placeholder: Option<&'a SharedString>,
587 caption: Caption,
588 toggle: Option<&'a Toggle>,
591 copy: CopyButton,
592}
593
594impl<'a> Overlay<'a> {
595 fn at(self, part: Part) -> Self {
596 Self { part, ..self }
597 }
598
599 fn here(&self) -> Cursor {
600 Cursor::new(self.block, self.part, 0)
601 }
602
603 fn caret_painted(&self) -> Option<usize> {
609 self.caret_on.then(|| self.caret()).flatten()
610 }
611
612 fn caret(&self) -> Option<usize> {
614 self.selection
615 .map(|selection| selection.head)
616 .filter(|head| head.block == self.block && head.part == self.part)
617 .map(|head| head.offset)
618 }
619
620 fn selected(&self, len: usize) -> Option<Range<usize>> {
622 self.clip(self.selection?, len)
623 }
624
625 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
628 self.annotations
629 .iter()
630 .filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
631 .collect()
632 }
633
634 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
640 if selection.is_collapsed() {
641 return None;
642 }
643 let (start, end) = selection.ordered();
644 let here = self.here();
645 let (first, last) = (
646 Cursor::new(start.block, start.part, 0),
647 Cursor::new(end.block, end.part, 0),
648 );
649 if here < first || here > last {
650 return None;
651 }
652 let from = if here == first { start.offset } else { 0 };
653 let to = if here == last { end.offset } else { len };
654 (from < to).then_some(from..to.min(len))
655 }
656
657 fn covers_block(&self) -> bool {
661 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
662 return false;
663 };
664 let (start, end) = selection.ordered();
665 start.block < self.block && self.block < end.block
666 }
667}
668
669pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
671 let doc = crate::parse_with(source, &crate::Marks::of(cx));
672 render(&doc, Caption::default(), window, cx)
673}
674
675pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
677 render_with(
678 doc,
679 Editing {
680 caption,
681 ..Editing::default()
682 },
683 window,
684 cx,
685 )
686}
687
688pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
696 let Editing {
697 selection,
698 caret_on,
699 layouts,
700 annotations,
701 placeholder,
702 caption,
703 typography,
704 toggle,
705 copy,
706 } = editing;
707 let reset = layouts.map(|layouts| {
713 let layouts = layouts.clone();
714 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
715 .absolute()
716 .size(px(0.0))
717 });
718 let theme = Theme::of(cx).clone();
721 let typography = typography.unwrap_or_else(|| Typography::of(cx));
722 let mut column = div().flex().flex_col().children(reset);
723
724 for (ix, block) in doc.blocks.iter().enumerate() {
725 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
726 None => 0.0,
727 Some(previous) if tight(previous, block) => LIST_GAP,
728 Some(_) => BLOCK_GAP,
729 };
730 let overlay = Overlay {
731 block: ix,
732 part: Part::Body,
733 selection,
734 caret_on,
735 layouts,
736 annotations,
737 placeholder: placeholder.as_ref(),
738 caption,
739 toggle: toggle.as_ref(),
740 copy,
741 };
742 let frame = layouts.map(|layouts| {
745 let layouts = layouts.clone();
746 canvas(
747 move |bounds, _, _| layouts.record_block(ix, bounds),
748 |_, _, _, _| (),
749 )
750 .absolute()
751 .size_full()
752 });
753 column = column.child(
754 div()
760 .mt(px(gap))
761 .pl(px(block.indent as f32 * INDENT_WIDTH))
762 .child(
763 div()
764 .w_full()
765 .relative()
766 .children(frame)
767 .when(overlay.covers_block() && block.opaque(), |el| {
772 el.rounded(px(4.0)).bg(theme.selection)
773 })
774 .child(block_element(
775 block,
776 overlay,
777 &typography,
778 &theme,
779 window,
780 cx,
781 )),
782 ),
783 );
784 }
785
786 column.into_any_element()
787}
788
789fn tight(previous: &Block, next: &Block) -> bool {
791 let marker = |block: &Block| {
792 matches!(
793 block.kind,
794 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
795 )
796 };
797 marker(previous) && (marker(next) || next.indent > previous.indent)
798}
799
800fn block_element(
801 block: &Block,
802 overlay: Overlay,
803 typography: &Typography,
804 theme: &Theme,
805 window: &mut Window,
806 cx: &mut App,
807) -> AnyElement {
808 let body = overlay.at(Part::Body);
809 match &block.kind {
810 BlockKind::Paragraph(text) => text_element(
811 text,
812 typography.body.size(),
813 typography.body.line_height(),
814 FontWeight::NORMAL,
815 body,
816 theme,
817 cx,
818 ),
819 BlockKind::Heading { level, text } => {
820 let heading = typography.heading(*level);
821 text_element(
822 text,
823 heading.size(),
824 heading.line_height(),
825 heading.weight,
826 body,
827 theme,
828 cx,
829 )
830 }
831 BlockKind::Bullet(text) => {
832 marker_row(disc(typography, theme), text, body, typography, theme, cx)
833 }
834 BlockKind::Ordered { number, text } => marker_row(
835 div()
836 .flex_none()
837 .w(px(MARKER_WIDTH))
838 .text_size(px(typography.body.size()))
839 .line_height(px(typography.body.line_height()))
840 .text_color(theme.text_muted)
841 .child(SharedString::from(format!("{number}.")))
842 .into_any_element(),
843 text,
844 body,
845 typography,
846 theme,
847 cx,
848 ),
849 BlockKind::Task { checked, text } => marker_row(
850 checkbox(*checked, overlay, typography, theme),
851 text,
852 body,
853 typography,
854 theme,
855 cx,
856 ),
857 BlockKind::Quote { kind, text } => div()
858 .border_l_2()
859 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
860 .pl(px(12.0))
861 .pr(px(10.0))
862 .py(px(2.0))
863 .text_color(theme.text_muted)
864 .children(kind.map(|kind| {
865 div()
866 .pb(px(2.0))
867 .text_size(px(typography.body.size()))
868 .line_height(px(typography.body.line_height()))
869 .font_weight(FontWeight::SEMIBOLD)
870 .text_color(alert_color(kind, theme))
871 .child(kind.label())
872 }))
873 .child(text_element(
874 text,
875 typography.body.size(),
876 typography.body.line_height(),
877 FontWeight::NORMAL,
878 body,
879 theme,
880 cx,
881 ))
882 .into_any_element(),
883 BlockKind::Code { language, code } => {
884 let overlay = overlay.at(Part::Code);
885 let painted = overlay
889 .caret()
890 .is_none()
891 .then(|| block::render(language.as_deref(), &code.text, window, cx))
892 .flatten();
893 match painted {
894 Some(element) => div()
897 .when(overlay.covers_block(), |el| {
898 el.rounded(px(4.0)).bg(theme.selection)
899 })
900 .child(element)
901 .into_any_element(),
902 None => code_block(
903 language.as_deref(),
904 &code.text,
905 overlay,
906 typography,
907 theme,
908 window,
909 cx,
910 ),
911 }
912 }
913 BlockKind::Image { url, alt, width } => {
914 image(url, alt, *width, overlay, typography, theme, cx)
915 }
916 BlockKind::Bookmark { url, form } => {
917 bookmark(overlay.block, url, *form, typography, theme, cx)
918 }
919 BlockKind::Table {
920 align,
921 header,
922 rows,
923 } => table(align, header, rows, overlay, typography, theme, window, cx),
924 BlockKind::Rule => div()
925 .h(px(1.0))
926 .w_full()
927 .bg(theme.border)
928 .into_any_element(),
929 }
930}
931
932fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
934 div()
935 .flex_none()
936 .w(px(MARKER_WIDTH))
937 .h(px(typography.body.line_height()))
938 .flex()
939 .items_center()
940 .child(
941 div()
942 .ml(px(1.0))
943 .w(px(5.0))
944 .h(px(5.0))
945 .rounded_full()
946 .bg(theme.text_faint),
947 )
948 .into_any_element()
949}
950
951fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
952 let ix = overlay.block;
953 let mut box_ = div()
954 .relative()
955 .w(px(13.0))
956 .h(px(13.0))
957 .rounded(px(3.5))
958 .border_1()
959 .flex()
960 .items_center()
961 .justify_center();
962 box_ = if checked {
963 box_.bg(theme.solid)
964 .border_color(theme.solid)
965 .text_style(TextStyle::Caption)
966 .text_color(theme.on_solid)
967 .child("✓")
968 } else {
969 box_.border_color(theme.border_strong)
970 };
971 box_ = box_.children(overlay.layouts.map(|layouts| {
974 let layouts = layouts.clone();
975 canvas(
976 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
977 |_, _, _, _| (),
978 )
979 .absolute()
980 .size_full()
981 }));
982 if overlay.toggle.is_some() {
985 box_ = box_.cursor_pointer();
986 }
987 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
988 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
989 cx.stop_propagation();
993 toggle(ix, window, cx);
994 });
995 }
996
997 div()
998 .flex_none()
999 .w(px(MARKER_WIDTH))
1000 .h(px(typography.body.line_height()))
1001 .flex()
1002 .items_center()
1003 .child(box_)
1004 .into_any_element()
1005}
1006
1007fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1009 match kind {
1010 QuoteKind::Note => theme.accent,
1011 QuoteKind::Tip => theme.success,
1012 QuoteKind::Important => theme.busy,
1013 QuoteKind::Warning => theme.warning,
1014 QuoteKind::Caution => theme.danger,
1015 }
1016}
1017
1018fn marker_row(
1019 marker: AnyElement,
1020 text: &Text,
1021 overlay: Overlay,
1022 typography: &Typography,
1023 theme: &Theme,
1024 cx: &App,
1025) -> AnyElement {
1026 div()
1027 .flex()
1028 .flex_row()
1029 .gap(px(MARKER_GAP))
1030 .child(marker)
1031 .child(div().flex_1().min_w_0().child(text_element(
1032 text,
1033 typography.body.size(),
1034 typography.body.line_height(),
1035 FontWeight::NORMAL,
1036 overlay,
1037 theme,
1038 cx,
1039 )))
1040 .into_any_element()
1041}
1042
1043pub struct Flat {
1046 pub text: SharedString,
1047 pub runs: Vec<TextRun>,
1048 pub links: Vec<(Range<usize>, String)>,
1049 pub code: Vec<Range<usize>>,
1050 pub chips: Vec<Range<usize>>,
1051}
1052
1053pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
1056 flatten_with(text, base_weight, theme, |_| None)
1057}
1058
1059pub fn flatten_with(
1062 text: &Text,
1063 base_weight: FontWeight,
1064 theme: &Theme,
1065 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
1066) -> Flat {
1067 let mut cuts: Vec<usize> = text
1068 .marks
1069 .iter()
1070 .flat_map(|span| [span.range.start, span.range.end])
1071 .chain([0, text.text.len()])
1072 .filter(|cut| *cut <= text.text.len())
1073 .collect();
1074 cuts.sort_unstable();
1075 cuts.dedup();
1076
1077 let mut runs = Vec::new();
1078 let mut links: Vec<(Range<usize>, String)> = Vec::new();
1079 let mut code: Vec<Range<usize>> = Vec::new();
1080 let mut chips: Vec<Range<usize>> = Vec::new();
1081
1082 for pair in cuts.windows(2) {
1083 let (start, end) = (pair[0], pair[1]);
1084 let covering = text
1085 .marks
1086 .iter()
1087 .filter(|span| span.range.start <= start && span.range.end >= end);
1088
1089 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
1090 let mut chip = false;
1091 let mut link = None;
1092 let mut custom = crate::MarkPaint::default();
1095 for span in covering {
1096 match &span.mark {
1097 Mark::Bold => bold = true,
1098 Mark::Italic => italic = true,
1099 Mark::Strike => strike = true,
1100 Mark::Code => mono = true,
1101 Mark::Mention { url, .. } => {
1102 chip = true;
1103 link = Some(url.clone());
1104 }
1105 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
1106 Mark::Custom(name) => {
1107 let Some(painted) = paint(name) else { continue };
1108 custom.color = painted.color.or(custom.color);
1109 custom.background = painted.background.or(custom.background);
1110 custom.weight = painted.weight.or(custom.weight);
1111 custom.italic |= painted.italic;
1112 custom.underline |= painted.underline;
1113 custom.strikethrough |= painted.strikethrough;
1114 }
1115 }
1116 }
1117 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
1118
1119 if mono {
1120 match code.last_mut() {
1121 Some(range) if range.end == start => range.end = end,
1122 _ => code.push(start..end),
1123 }
1124 }
1125 if chip {
1126 match chips.last_mut() {
1127 Some(range) if range.end == start => range.end = end,
1128 _ => chips.push(start..end),
1129 }
1130 }
1131 if let Some(url) = &link {
1132 match links.last_mut() {
1133 Some((range, last)) if range.end == start && last == url => range.end = end,
1134 _ => links.push((start..end, url.clone())),
1135 }
1136 }
1137
1138 let mut face = font(if mono {
1139 theme.font_mono.clone()
1140 } else {
1141 theme.font_body.clone()
1142 });
1143 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
1144 FontWeight::SEMIBOLD
1145 } else {
1146 custom.weight.unwrap_or(base_weight)
1147 };
1148 face.style = if italic {
1149 FontStyle::Italic
1150 } else {
1151 FontStyle::Normal
1152 };
1153
1154 runs.push(TextRun {
1155 len: end - start,
1156 font: face,
1157 color: match (mono, custom.color) {
1161 (_, Some(color)) => color,
1162 (true, None) => theme.code_text,
1163 (false, None) => theme.text,
1164 },
1165 background_color: custom.background,
1166 underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
1167 color: Some(theme.text_muted),
1168 thickness: px(1.0),
1169 wavy: false,
1170 }),
1171 strikethrough: strike.then_some(StrikethroughStyle {
1172 thickness: px(1.0),
1173 color: Some(theme.text_muted),
1174 }),
1175 });
1176 }
1177
1178 Flat {
1179 text: text.text.clone().into(),
1180 runs,
1181 links,
1182 code,
1183 chips,
1184 }
1185}
1186
1187fn text_element(
1188 text: &Text,
1189 size: f32,
1190 line_height: f32,
1191 weight: FontWeight,
1192 overlay: Overlay,
1193 theme: &Theme,
1194 cx: &App,
1195) -> AnyElement {
1196 let flat = flatten_with(text, weight, theme, |name| {
1197 crate::marks::paint_of(cx, name, theme)
1198 });
1199 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
1200}
1201
1202fn painted_text(
1208 flat: Flat,
1209 len: usize,
1210 size: f32,
1211 line_height: f32,
1212 overlay: Overlay,
1213 theme: &Theme,
1214) -> AnyElement {
1215 let (ix, part) = (overlay.block, overlay.part);
1216 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
1217 let span = 0..len;
1218 let hint = overlay
1221 .placeholder
1222 .filter(|_| len == 0 && overlay.caret().is_some())
1225 .map(|hint| {
1226 div()
1227 .absolute()
1228 .text_color(theme.text_faint)
1229 .child(hint.clone())
1230 });
1231 let styled = StyledText::new(flat.text).with_runs(flat.runs);
1232 let layout = styled.layout().clone();
1233
1234 let painted: AnyElement = if flat.links.is_empty() {
1235 styled.into_any_element()
1236 } else {
1237 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
1238 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
1239 .on_click(ranges, move |clicked, _window, cx| {
1240 if let Some(url) = urls.get(clicked) {
1241 cx.open_url(url);
1242 }
1243 })
1244 .into_any_element()
1245 };
1246
1247 let wash = theme.code_wash;
1251 let code_ranges = flat.code;
1252 let chip_wash = theme.element_hover;
1253 let chip_edge = theme.border;
1254 let chip_ranges = flat.chips;
1255 let caret_color = theme.caret;
1256 let selection_color = theme.selection;
1257 let annotated = overlay.annotated(len, theme);
1258 let layouts = overlay.layouts.cloned();
1259 let underlay = canvas(
1260 |_, _, _| (),
1261 move |_, _, window, _| {
1262 if let Some(layouts) = &layouts {
1263 layouts.record(ix, part, span.clone(), layout.clone());
1264 }
1265 for (range, wash) in &annotated {
1268 for rect in range_rects(&layout, range, 0.0, 0.0) {
1269 window.paint_quad(quad(
1270 rect,
1271 px(2.0),
1272 *wash,
1273 px(0.0),
1274 gpui::transparent_black(),
1275 BorderStyle::default(),
1276 ));
1277 }
1278 }
1279 if let Some(range) = &selected {
1283 for rect in range_rects(&layout, range, 0.0, 0.0) {
1284 window.paint_quad(quad(
1285 rect,
1286 px(2.0),
1287 selection_color,
1288 px(0.0),
1289 gpui::transparent_black(),
1290 BorderStyle::default(),
1291 ));
1292 }
1293 }
1294 if let Some(offset) = caret
1295 && let Some(head) = layout.position_for_index(offset)
1296 {
1297 window.paint_quad(quad(
1298 caret_quad(head, size, layout.line_height()),
1299 px(0.0),
1300 caret_color,
1301 px(0.0),
1302 gpui::transparent_black(),
1303 BorderStyle::default(),
1304 ));
1305 }
1306 for range in &code_ranges {
1307 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1308 window.paint_quad(quad(
1309 rect,
1310 px(INLINE_CODE_RADIUS),
1311 wash,
1312 px(0.0),
1313 gpui::transparent_black(),
1314 BorderStyle::default(),
1315 ));
1316 }
1317 }
1318 for range in &chip_ranges {
1321 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1322 window.paint_quad(quad(
1323 rect,
1324 px(Theme::control_radius()),
1325 chip_wash,
1326 px(1.0),
1327 chip_edge,
1328 BorderStyle::Solid,
1329 ));
1330 }
1331 }
1332 },
1333 )
1334 .absolute()
1335 .size_full();
1336
1337 div()
1338 .text_size(px(size))
1339 .line_height(px(line_height))
1340 .relative()
1341 .child(underlay)
1342 .children(hint)
1343 .child(painted)
1344 .into_any_element()
1345}
1346
1347fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1353 let inset = (line_height - px(size)) / 2.0;
1354 Bounds::new(
1355 head + point(px(0.0), inset),
1356 gpui::size(px(CARET_WIDTH), px(size)),
1357 )
1358}
1359
1360fn range_rects(
1362 layout: &gpui::TextLayout,
1363 range: &Range<usize>,
1364 pad_x: f32,
1365 inset_y: f32,
1366) -> Vec<Bounds<Pixels>> {
1367 let mut rects = Vec::new();
1368 let line_height = layout.line_height();
1369 let mut origin = layout.bounds().origin;
1370 let mut line_start = 0;
1371 for line in layout.line_layouts() {
1372 let shaped = &line.unwrapped_layout;
1373 let row_ends = line
1376 .wrap_boundaries()
1377 .iter()
1378 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
1379 .chain([line.len()]);
1380 let mut row_start = 0;
1381 for (row, row_end) in row_ends.enumerate() {
1382 let from = range
1383 .start
1384 .saturating_sub(line_start)
1385 .clamp(row_start, row_end);
1386 let to = range.end.saturating_sub(line_start).min(row_end);
1387 let row_x = shaped.x_for_index(row_start);
1388 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
1389 if from < to && right > left {
1390 rects.push(Bounds::new(
1391 origin
1392 + point(
1393 left - row_x - px(pad_x),
1394 line_height * row as f32 + px(inset_y),
1395 ),
1396 size(
1397 right - left + px(2.0 * pad_x),
1398 line_height - px(2.0 * inset_y),
1399 ),
1400 ));
1401 }
1402 row_start = row_end;
1403 }
1404 origin.y += line.size(line_height).height;
1405 line_start += line.len() + 1;
1407 }
1408 rects
1409}
1410
1411pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
1419 let Editing {
1420 selection,
1421 caret_on,
1422 layouts,
1423 annotations,
1424 typography,
1425 ..
1426 } = editing;
1427 let reset = layouts.map(|layouts| {
1431 let layouts = layouts.clone();
1432 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
1433 .absolute()
1434 .size(px(0.0))
1435 });
1436 let theme = Theme::of(cx).clone();
1437 let typography = typography.unwrap_or_else(|| Typography::of(cx));
1438 let overlay = Overlay {
1439 block: 0,
1440 part: Part::Code,
1441 selection,
1442 caret_on,
1443 layouts,
1444 annotations,
1445 placeholder: None,
1446 caption: Caption::default(),
1447 toggle: None,
1449 copy: CopyButton::Hidden,
1451 };
1452 let (underlay, lines) = code_lines(
1453 Some(crate::source::LANGUAGES[0]),
1454 code,
1455 overlay,
1456 &typography,
1457 &theme,
1458 cx,
1459 );
1460 let style = crate::SourceStyle::of(cx);
1462 let digits = lines.len().to_string().len().max(style.gutter_min_digits);
1463 let gap = style.gutter_gap.max(0.0) * typography.code.size();
1464 let gutter_width = digits as f32 * typography.code.size() + gap;
1465 let lines = lines
1466 .into_iter()
1467 .enumerate()
1468 .map(|(index, line)| {
1469 if !style.line_numbers {
1470 return line;
1471 }
1472 div()
1473 .flex()
1474 .items_start()
1475 .child(
1476 div()
1477 .w(px(gutter_width))
1478 .flex_shrink_0()
1479 .pr(px(gap))
1480 .font_family(theme.font_mono.clone())
1481 .text_color(style.gutter_color.unwrap_or(theme.text_faint))
1482 .text_right()
1483 .child((index + 1).to_string()),
1484 )
1485 .child(div().flex_1().min_w_0().child(line))
1486 .into_any_element()
1487 })
1488 .collect();
1489 div()
1490 .flex()
1491 .flex_col()
1492 .children(reset)
1493 .child(code_body(0, underlay, lines, &typography, true))
1494 .into_any_element()
1495}
1496
1497fn code_lines(
1500 language: Option<&str>,
1501 code: &str,
1502 overlay: Overlay,
1503 typography: &Typography,
1504 theme: &Theme,
1505 cx: &App,
1506) -> (AnyElement, Vec<AnyElement>) {
1507 let ix = overlay.block;
1508 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
1513 language
1514 .filter(|language| crate::source::is_markdown(language))
1515 .map(|_| crate::source::spans(code))
1516 });
1517 let mono = font(theme.font_mono.clone());
1518 let run = |len: usize, color: Hsla| TextRun {
1519 len,
1520 font: mono.clone(),
1521 color,
1522 background_color: None,
1523 underline: None,
1524 strikethrough: None,
1525 };
1526 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1531 let mut offset = 0usize;
1532 let lines: Vec<AnyElement> = code
1533 .split('\n')
1534 .map(|line| {
1535 let start = offset;
1536 offset += line.len() + 1;
1537 let mut runs = Vec::new();
1538 let mut pos = 0usize;
1541 if let Some(spans) = &spans {
1542 let end = start + line.len();
1543 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1544 let s = range.start.clamp(start, end) - start;
1545 let e = range.end.min(end) - start;
1546 if s > pos {
1547 runs.push(run(s - pos, theme.text));
1548 }
1549 runs.push(run(e - s, theme.syntax.color(*kind)));
1550 pos = e;
1551 }
1552 }
1553 if pos < line.len() {
1554 runs.push(run(line.len() - pos, theme.text));
1555 }
1556 if runs.is_empty() {
1557 runs.push(run(0, theme.text));
1558 }
1559 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1560 rows.push((start..start + line.len(), styled.layout().clone()));
1561 styled.into_any_element()
1562 })
1563 .collect();
1564
1565 let caret = overlay.caret_painted();
1566 let selected = overlay.selected(code.len());
1567 let sink = overlay.layouts.cloned();
1568 let code_size = typography.code.size();
1569 let annotated = overlay.annotated(code.len(), theme);
1570 let (caret_color, selection_color) = (theme.caret, theme.selection);
1571 let underlay = canvas(
1572 |_, _, _| (),
1573 move |_, _, window, _| {
1574 for (span, layout) in &rows {
1575 if let Some(sink) = &sink {
1576 sink.record(ix, Part::Code, span.clone(), layout.clone());
1577 }
1578 for (range, wash) in &annotated {
1579 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1580 if from < to {
1581 for rect in
1582 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1583 {
1584 window.paint_quad(quad(
1585 rect,
1586 px(2.0),
1587 *wash,
1588 px(0.0),
1589 gpui::transparent_black(),
1590 BorderStyle::default(),
1591 ));
1592 }
1593 }
1594 }
1595 if let Some(range) = &selected {
1596 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1597 if from < to {
1598 for rect in
1599 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1600 {
1601 window.paint_quad(quad(
1602 rect,
1603 px(2.0),
1604 selection_color,
1605 px(0.0),
1606 gpui::transparent_black(),
1607 BorderStyle::default(),
1608 ));
1609 }
1610 }
1611 }
1612 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1613 && let Some(head) = layout.position_for_index(offset - span.start)
1614 {
1615 window.paint_quad(quad(
1616 caret_quad(head, code_size, layout.line_height()),
1617 px(0.0),
1618 caret_color,
1619 px(0.0),
1620 gpui::transparent_black(),
1621 BorderStyle::default(),
1622 ));
1623 }
1624 }
1625 },
1626 )
1627 .absolute()
1628 .size_full();
1629
1630 (underlay.into_any_element(), lines)
1631}
1632
1633fn code_block(
1634 language: Option<&str>,
1635 code: &str,
1636 overlay: Overlay,
1637 typography: &Typography,
1638 theme: &Theme,
1639 window: &mut Window,
1640 cx: &mut App,
1641) -> AnyElement {
1642 let ix = overlay.block;
1643 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
1644 let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
1645
1646 div()
1647 .rounded(px(Theme::panel_radius()))
1648 .bg(theme.ink(0.035))
1649 .border_1()
1650 .border_color(theme.border)
1651 .overflow_hidden()
1652 .relative()
1653 .child(
1657 div()
1658 .relative()
1659 .flex()
1660 .flex_row()
1661 .items_center()
1662 .px(px(CODE_PADDING_X))
1663 .py(px(5.0))
1664 .border_b_1()
1665 .border_color(theme.border)
1666 .bg(theme.ink(0.02))
1667 .text_style(TextStyle::Subheadline)
1668 .text_color(match language {
1669 Some(_) => theme.text_muted,
1670 None => theme.text_faint,
1671 })
1672 .child(
1676 div()
1677 .relative()
1678 .children(overlay.layouts.map(|layouts| {
1679 let layouts = layouts.clone();
1680 canvas(
1681 move |bounds, _, _| layouts.record_language(ix, bounds),
1682 |_, _, _, _| (),
1683 )
1684 .absolute()
1685 .size_full()
1686 }))
1687 .child(SharedString::from(
1688 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1689 )),
1690 ),
1691 )
1692 .child(body)
1693 .children(
1694 (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
1695 )
1696 .into_any_element()
1697}
1698
1699fn code_body(
1701 ix: usize,
1702 underlay: AnyElement,
1703 lines: Vec<AnyElement>,
1704 typography: &Typography,
1705 wrap: bool,
1706) -> AnyElement {
1707 let column = div()
1708 .flex()
1709 .flex_col()
1710 .px(px(CODE_PADDING_X))
1711 .children(lines);
1712 let body = div()
1713 .id(ElementId::named_usize("md-code", ix))
1714 .relative()
1715 .py(px(CODE_PADDING_Y))
1716 .text_size(px(typography.code.size()))
1717 .line_height(px(typography.code.line_height()))
1718 .child(underlay);
1719 if wrap {
1720 body.child(column.w_full()).into_any_element()
1723 } else {
1724 ui::scroll::Viewport::new(
1725 format!("md-code-scroll-{ix}"),
1726 body.flex()
1727 .flex_row()
1728 .whitespace_nowrap()
1729 .child(column.items_start()),
1736 gpui::Axis::Horizontal,
1737 )
1738 .into_any_element()
1739 }
1740}
1741
1742fn copy_button(
1749 code: &str,
1750 ix: usize,
1751 theme: &Theme,
1752 window: &mut Window,
1753 cx: &mut App,
1754) -> AnyElement {
1755 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1756 let showing = *copied.read(cx);
1757 let text: SharedString = code.to_string().into();
1758
1759 div()
1760 .id(ElementId::named_usize("md-copy", ix))
1761 .absolute()
1762 .top(px(3.0))
1763 .right(px(5.0))
1764 .h(px(20.0))
1765 .px(px(6.0))
1766 .rounded(px(5.0))
1767 .flex()
1768 .items_center()
1769 .cursor_pointer()
1770 .text_style(TextStyle::Caption)
1771 .text_color(theme.text_muted)
1772 .hover(|el| el.bg(theme.element_hover))
1773 .child(if showing { "Copied" } else { "Copy" })
1774 .on_click({
1775 let copied = copied.clone();
1776 move |_, _, cx| {
1777 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1778 copied.update(cx, |state, cx| {
1779 *state = true;
1780 cx.notify();
1781 });
1782 }
1783 })
1784 .on_hover(move |hovering, _, cx| {
1785 if !*hovering && *copied.read(cx) {
1786 copied.update(cx, |state, cx| {
1787 *state = false;
1788 cx.notify();
1789 });
1790 }
1791 })
1792 .into_any_element()
1793}
1794
1795fn image(
1802 url: &str,
1803 alt: &Text,
1804 width: Option<u32>,
1805 overlay: Overlay,
1806 typography: &Typography,
1807 theme: &Theme,
1808 cx: &App,
1809) -> AnyElement {
1810 let hint = SharedString::new_static(CAPTION_HINT);
1811 let overlay = Overlay {
1812 placeholder: Some(&hint),
1813 ..overlay.at(Part::Caption)
1814 };
1815 let picture = if url.is_empty() {
1816 div()
1817 .h(px(IMAGE_EMPTY_HEIGHT))
1818 .flex()
1819 .items_center()
1820 .px(px(CARD_PADDING))
1821 .rounded(px(Theme::button_radius()))
1822 .border_1()
1823 .border_dashed()
1824 .border_color(theme.border)
1825 .text_size(px(typography.body.size()))
1826 .text_color(theme.text_muted)
1827 .child(IMAGE_EMPTY)
1828 } else {
1829 let picture = match url.contains("://") {
1833 true => img(SharedString::from(url.to_string())),
1834 false => img(std::path::PathBuf::from(url)),
1835 };
1836 let box_ = div()
1837 .relative()
1838 .rounded(px(Theme::button_radius()))
1839 .overflow_hidden()
1840 .border_1()
1841 .border_color(theme.border)
1842 .children(overlay.layouts.map(|layouts| {
1843 let layouts = layouts.clone();
1844 let ix = overlay.block;
1845 canvas(
1846 move |bounds, _, _| layouts.record_picture(ix, bounds),
1847 |_, _, _, _| (),
1848 )
1849 .absolute()
1850 .size_full()
1851 }));
1852 match width {
1853 Some(width) => box_
1858 .self_start()
1859 .max_w_full()
1860 .w(px(width as f32))
1861 .child(picture.w(px(width as f32)).max_w_full()),
1862 None => box_.child(picture.max_w_full()),
1865 }
1866 };
1867 div()
1868 .flex()
1869 .flex_col()
1870 .gap(px(CAPTION_GAP))
1871 .child(picture)
1872 .when(
1875 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1876 |el| {
1877 el.child(text_element(
1878 alt,
1879 typography.caption.size(),
1880 typography.caption.line_height(),
1881 FontWeight::NORMAL,
1882 overlay,
1883 theme,
1884 cx,
1885 ))
1886 },
1887 )
1888 .into_any_element()
1889}
1890
1891fn bookmark(
1903 ix: usize,
1904 url: &str,
1905 form: Form,
1906 typography: &Typography,
1907 theme: &Theme,
1908 cx: &App,
1909) -> AnyElement {
1910 let preview = preview::of(cx, url).unwrap_or_default();
1911 let host = SharedString::from(preview::host(url).to_string());
1912 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1913 let title = preview
1914 .title
1915 .clone()
1916 .unwrap_or_else(|| SharedString::from(url.to_string()));
1917
1918 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1921 let site = host.clone();
1922 let mark = move |size: f32| {
1923 let host = site.clone();
1924 match icon.clone() {
1925 Some(icon) => img(icon)
1926 .size(px(size))
1927 .rounded(px(size / 4.0))
1928 .with_fallback(move || initial(&host, size, muted, wash))
1929 .into_any_element(),
1930 None => initial(&host, size, muted, wash),
1931 }
1932 };
1933
1934 if form == Form::Chip {
1935 let open = url.to_string();
1936 let pill = div()
1937 .id(ElementId::named_usize("md-chip", ix))
1938 .flex()
1939 .flex_row()
1940 .items_center()
1941 .gap(px(6.0))
1942 .px(px(CHIP_BLOCK_PAD_X))
1943 .py(px(CHIP_BLOCK_PAD_Y))
1944 .rounded(px(Theme::control_radius()))
1945 .border_1()
1946 .border_color(theme.border)
1947 .bg(theme.element_hover)
1948 .text_size(px(typography.body.size()))
1949 .line_height(px(typography.body.line_height()))
1950 .text_color(theme.text)
1951 .cursor(CursorStyle::PointingHand)
1952 .hover(|el| el.bg(theme.element_active))
1953 .on_click(move |_, _, cx| cx.open_url(&open))
1954 .child(mark(CHIP_ICON))
1955 .child(
1958 div()
1959 .min_w_0()
1960 .truncate()
1961 .child(preview.title.unwrap_or(label)),
1962 );
1963 return div().flex().flex_row().child(pill).into_any_element();
1966 }
1967
1968 let words = div()
1969 .flex()
1970 .flex_col()
1971 .min_w_0()
1972 .px(px(CARD_PADDING))
1973 .py(px(CARD_PADDING - 2.0))
1974 .child(
1975 div()
1976 .truncate()
1977 .text_size(px(typography.body.size()))
1978 .line_height(px(typography.body.line_height()))
1979 .text_color(theme.text)
1980 .child(title),
1981 )
1982 .children(preview.description.map(|blurb| {
1983 div()
1984 .line_clamp(2)
1985 .text_size(px(typography.card.size()))
1986 .line_height(px(typography.card.line_height()))
1987 .text_color(theme.text_muted)
1988 .child(blurb)
1989 }))
1990 .child(
1991 div()
1992 .mt_auto()
1993 .pt(px(6.0))
1994 .flex()
1995 .items_center()
1996 .gap(px(6.0))
1997 .text_size(px(typography.card.size()))
1998 .text_color(theme.text_muted)
1999 .child(mark(CARD_ICON))
2000 .child(div().truncate().child(label)),
2001 );
2002
2003 let picture = corners(div(), form)
2004 .bg(theme.surface)
2005 .flex()
2006 .items_center()
2007 .justify_center()
2008 .overflow_hidden()
2009 .child(match preview.image {
2010 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
2011 .with_fallback(move || mark(CARD_COVER))
2012 .into_any_element(),
2013 None => mark(CARD_COVER),
2014 });
2015
2016 let open = url.to_string();
2017 let card = div()
2018 .id(ElementId::named_usize("md-bookmark", ix))
2019 .flex()
2020 .w_full()
2021 .overflow_hidden()
2022 .rounded(px(Theme::button_radius()))
2023 .border(px(CARD_BORDER))
2024 .border_color(theme.border)
2025 .bg(theme.surface_card)
2026 .cursor(CursorStyle::PointingHand)
2027 .hover(|el| el.bg(theme.element_hover))
2028 .on_click(move |_, _, cx| cx.open_url(&open));
2029
2030 if form == Form::Embed {
2031 card.flex_col()
2032 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
2033 .child(words.w_full())
2034 } else {
2035 card.h(px(CARD_HEIGHT))
2036 .child(words.flex_1())
2037 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
2038 }
2039 .into_any_element()
2040}
2041
2042fn corners<T: Styled>(element: T, form: Form) -> T {
2046 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
2047 match form {
2048 Form::Embed => element.rounded_t(corner),
2049 _ => element.rounded_r(corner),
2050 }
2051}
2052
2053fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
2056 div()
2057 .flex_none()
2058 .size(px(size))
2059 .rounded(px(size / 4.0))
2060 .bg(wash)
2061 .flex()
2062 .items_center()
2063 .justify_center()
2064 .text_size(px(size * 0.55))
2065 .text_color(color)
2066 .child(SharedString::from(
2067 host.chars()
2068 .next()
2069 .unwrap_or('?')
2070 .to_uppercase()
2071 .to_string(),
2072 ))
2073 .into_any_element()
2074}
2075
2076#[expect(
2083 clippy::too_many_arguments,
2084 reason = "a table, its overlay, and what paints them"
2085)]
2086fn table(
2087 align: &[Align],
2088 header: &[Text],
2089 rows: &[Vec<Text>],
2090 overlay: Overlay,
2091 typography: &Typography,
2092 theme: &Theme,
2093 window: &mut Window,
2094 cx: &App,
2095) -> AnyElement {
2096 let ix = overlay.block;
2097 let all: Vec<&[Text]> = std::iter::once(header)
2098 .filter(|row| !row.is_empty())
2099 .chain(rows.iter().map(|row| row.as_slice()))
2100 .collect();
2101 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
2102 if columns == 0 {
2103 return gpui::Empty.into_any_element();
2104 }
2105 let has_header = !header.is_empty();
2106
2107 let text_system = window.text_system();
2108 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
2109 let mut content = vec![0.0f32; columns];
2110 for (r, row) in all.iter().enumerate() {
2111 let weight = if has_header && r == 0 {
2112 FontWeight::BOLD
2113 } else {
2114 FontWeight::NORMAL
2115 };
2116 let mut out = Vec::with_capacity(columns);
2117 for (c, natural) in content.iter_mut().enumerate() {
2118 let Some(cell) = row.get(c) else {
2119 out.push(None);
2120 continue;
2121 };
2122 let flat = flatten_with(cell, weight, theme, |name| {
2123 crate::marks::paint_of(cx, name, theme)
2124 });
2125 if !flat.text.is_empty() {
2126 let width = f32::from(
2127 text_system
2128 .shape_line(
2129 flat.text.clone(),
2130 px(typography.body.size()),
2131 &flat.runs,
2132 None,
2133 )
2134 .width(),
2135 );
2136 *natural = natural.max(width);
2137 }
2138 out.push(Some(flat));
2139 }
2140 flats.push(out);
2141 }
2142
2143 let naturals: Vec<f32> = content
2144 .iter()
2145 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
2146 .collect();
2147 let minimums: Vec<f32> = naturals
2148 .iter()
2149 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
2150 .collect();
2151 let hairline = theme.hairline(0.10);
2152
2153 let mut inner = div()
2154 .flex()
2155 .flex_col()
2156 .w_full()
2157 .min_w(px(minimums.iter().sum::<f32>()));
2158 for (r, row) in flats.into_iter().enumerate() {
2159 if r > 0 {
2160 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
2161 }
2162 let mut row_el = div().flex().flex_row();
2163 for (c, cell) in row.into_iter().enumerate() {
2164 let mut cell_el = div()
2165 .flex_grow(naturals[c])
2166 .flex_shrink(naturals[c])
2167 .flex_basis(px(0.0))
2168 .min_w(px(minimums[c]))
2169 .p(px(TABLE_CELL_PADDING))
2170 .text_size(px(typography.body.size()))
2171 .line_height(px(typography.body.line_height()));
2172 cell_el = match align.get(c).copied().unwrap_or_default() {
2173 Align::Left => cell_el,
2174 Align::Center => cell_el.text_center(),
2175 Align::Right => cell_el.text_right(),
2176 };
2177 if let Some(flat) = cell {
2178 let row = if has_header { r } else { r + 1 };
2182 let len = flat.text.len();
2183 cell_el = cell_el.child(painted_text(
2184 flat,
2185 len,
2186 typography.body.size(),
2187 typography.body.line_height(),
2188 overlay.at(Part::Cell { row, column: c }),
2189 theme,
2190 ));
2191 }
2192 row_el = row_el.child(cell_el);
2193 }
2194 inner = inner.child(row_el);
2195 }
2196
2197 ui::scroll::Viewport::new(
2198 format!("md-table-scroll-{ix}"),
2199 div()
2200 .id(ElementId::named_usize("md-table", ix))
2201 .w_full()
2202 .child(inner),
2203 gpui::Axis::Horizontal,
2204 )
2205 .into_any_element()
2206}