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 blocks: Vec<(usize, Bounds<Pixels>)>,
233 languages: Vec<(usize, Bounds<Pixels>)>,
236 pictures: Vec<(usize, Bounds<Pixels>)>,
240 checkboxes: Vec<(usize, Bounds<Pixels>)>,
243}
244
245struct Painted {
252 block: usize,
253 part: Part,
254 range: Range<usize>,
255 layout: TextLayout,
256}
257
258impl BlockLayouts {
259 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
265 let entries = &self.0.borrow().texts;
266 let cursor = |painted: &Painted| {
267 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
268 Cursor::new(
269 painted.block,
270 painted.part,
271 painted.range.start + offset.min(painted.range.len()),
272 )
273 };
274 if let Some(painted) = entries
275 .iter()
276 .find(|painted| painted.layout.bounds().contains(&point))
277 {
278 return Some(cursor(painted));
279 }
280 entries
281 .iter()
282 .min_by_key(|painted| {
283 let bounds = painted.layout.bounds();
284 let above = (bounds.origin.y - point.y).abs();
285 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
286 f32::from(above.min(below)) as i64
287 })
288 .map(cursor)
289 }
290
291 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
297 let entries = &self.0.borrow().texts;
298 let painted = entries.iter().find(|painted| {
299 painted.block == at.block
300 && painted.part == at.part
301 && painted.range.start <= at.offset
302 && at.offset <= painted.range.end
303 })?;
304 let point = painted
305 .layout
306 .position_for_index(at.offset - painted.range.start)?;
307 Some((point, painted.layout.line_height()))
308 }
309
310 pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
318 let (start, end) = selection.ordered();
319 self.0
320 .borrow()
321 .texts
322 .iter()
323 .filter_map(|painted| {
324 let here = Cursor::new(painted.block, painted.part, 0);
325 let (from, to) = (
326 Cursor::new(start.block, start.part, 0),
327 Cursor::new(end.block, end.part, 0),
328 );
329 if here < from || here > to {
330 return None;
331 }
332 let len = painted.range.len();
335 let first = if here == from { start.offset } else { 0 };
336 let last = if here == to { end.offset } else { usize::MAX };
337 let range = first.saturating_sub(painted.range.start).min(len)
338 ..last.saturating_sub(painted.range.start).min(len);
339 (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
340 })
341 .flatten()
342 .collect()
343 }
344
345 pub fn step_row(
356 &self,
357 at: Cursor,
358 from: Point<Pixels>,
359 down: bool,
360 ) -> Option<(Cursor, Pixels)> {
361 let entries = &self.0.borrow().texts;
362 let ix = entries.iter().position(|painted| {
363 painted.block == at.block
364 && painted.part == at.part
365 && painted.range.start <= at.offset
366 && at.offset <= painted.range.end
367 })?;
368 let here = &entries[ix];
369 let line = here.layout.line_height();
370 let index_at = |painted: &Painted, y: Pixels| {
371 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
372 (
373 Cursor::new(
374 painted.block,
375 painted.part,
376 painted.range.start + offset.min(painted.range.len()),
377 ),
378 y,
379 )
380 };
381
382 let bounds = here.layout.bounds();
385 let target = if down { from.y + line } else { from.y - line };
386 if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
387 return Some(index_at(here, target));
388 }
389
390 let next = match down {
391 true => entries.get(ix + 1)?,
392 false => entries.get(ix.checked_sub(1)?)?,
393 };
394 let bounds = next.layout.bounds();
396 let row = match down {
397 true => bounds.origin.y,
398 false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
399 };
400 Some(index_at(next, row))
401 }
402
403 pub fn over_text(&self, point: Point<Pixels>) -> bool {
410 self.0
411 .borrow()
412 .texts
413 .iter()
414 .any(|painted| painted.layout.bounds().contains(&point))
415 }
416
417 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
419 let blocks = &self.0.borrow().blocks;
420 blocks
421 .iter()
422 .find(|(_, bounds)| bounds.contains(&point))
423 .or_else(|| {
424 blocks.iter().min_by_key(|(_, bounds)| {
425 let above = (bounds.origin.y - point.y).abs();
426 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
427 f32::from(above.min(below)) as i64
428 })
429 })
430 .map(|(ix, _)| *ix)
431 }
432
433 pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
442 let texts = &self.0.borrow().texts;
443 let painted = texts.iter().find(|painted| painted.block == ix)?;
444 Some((
445 painted.layout.bounds().origin.y,
446 painted.layout.line_height(),
447 ))
448 }
449
450 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
452 self.0
453 .borrow()
454 .blocks
455 .iter()
456 .find(|(block, _)| *block == ix)
457 .map(|(_, bounds)| *bounds)
458 }
459
460 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
465 self.0
466 .borrow()
467 .languages
468 .iter()
469 .find(|(block, _)| *block == ix)
470 .map(|(_, bounds)| *bounds)
471 }
472
473 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
478 self.0
479 .borrow()
480 .pictures
481 .iter()
482 .find(|(block, _)| *block == ix)
483 .map(|(_, bounds)| *bounds)
484 }
485
486 pub fn checkbox_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
490 self.0
491 .borrow()
492 .checkboxes
493 .iter()
494 .find(|(block, _)| *block == ix)
495 .map(|(_, bounds)| *bounds)
496 }
497
498 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
499 self.0.borrow_mut().texts.push(Painted {
500 block,
501 part,
502 range,
503 layout,
504 });
505 }
506
507 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
508 self.0.borrow_mut().blocks.push((ix, bounds));
509 }
510
511 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
512 self.0.borrow_mut().languages.push((ix, bounds));
513 }
514
515 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
516 self.0.borrow_mut().pictures.push((ix, bounds));
517 }
518
519 fn record_checkbox(&self, ix: usize, bounds: Bounds<Pixels>) {
520 self.0.borrow_mut().checkboxes.push((ix, bounds));
521 }
522
523 fn clear(&self) {
524 let mut frames = self.0.borrow_mut();
525 frames.texts.clear();
526 frames.blocks.clear();
527 frames.languages.clear();
528 frames.pictures.clear();
529 frames.checkboxes.clear();
530 }
531}
532
533#[derive(Clone, Copy)]
539struct Overlay<'a> {
540 block: usize,
541 part: Part,
542 selection: Option<Selection>,
543 caret_on: bool,
544 layouts: Option<&'a BlockLayouts>,
545 annotations: &'a [(Selection, Annotation)],
547 placeholder: Option<&'a SharedString>,
550 caption: Caption,
551 toggle: Option<&'a Toggle>,
554 copy: CopyButton,
555}
556
557impl<'a> Overlay<'a> {
558 fn at(self, part: Part) -> Self {
559 Self { part, ..self }
560 }
561
562 fn here(&self) -> Cursor {
563 Cursor::new(self.block, self.part, 0)
564 }
565
566 fn caret_painted(&self) -> Option<usize> {
572 self.caret_on.then(|| self.caret()).flatten()
573 }
574
575 fn caret(&self) -> Option<usize> {
577 self.selection
578 .map(|selection| selection.head)
579 .filter(|head| head.block == self.block && head.part == self.part)
580 .map(|head| head.offset)
581 }
582
583 fn selected(&self, len: usize) -> Option<Range<usize>> {
585 self.clip(self.selection?, len)
586 }
587
588 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
591 self.annotations
592 .iter()
593 .filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
594 .collect()
595 }
596
597 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
603 if selection.is_collapsed() {
604 return None;
605 }
606 let (start, end) = selection.ordered();
607 let here = self.here();
608 let (first, last) = (
609 Cursor::new(start.block, start.part, 0),
610 Cursor::new(end.block, end.part, 0),
611 );
612 if here < first || here > last {
613 return None;
614 }
615 let from = if here == first { start.offset } else { 0 };
616 let to = if here == last { end.offset } else { len };
617 (from < to).then_some(from..to.min(len))
618 }
619
620 fn covers_block(&self) -> bool {
624 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
625 return false;
626 };
627 let (start, end) = selection.ordered();
628 start.block < self.block && self.block < end.block
629 }
630}
631
632pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
634 let doc = crate::parse_with(source, &crate::Marks::of(cx));
635 render(&doc, Caption::default(), window, cx)
636}
637
638pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
640 render_with(
641 doc,
642 Editing {
643 caption,
644 ..Editing::default()
645 },
646 window,
647 cx,
648 )
649}
650
651pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
659 let Editing {
660 selection,
661 caret_on,
662 layouts,
663 annotations,
664 placeholder,
665 caption,
666 typography,
667 toggle,
668 copy,
669 } = editing;
670 let reset = layouts.map(|layouts| {
676 let layouts = layouts.clone();
677 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
678 .absolute()
679 .size(px(0.0))
680 });
681 let theme = Theme::of(cx).clone();
684 let typography = typography.unwrap_or_else(|| Typography::of(cx));
685 let mut column = div().flex().flex_col().children(reset);
686
687 for (ix, block) in doc.blocks.iter().enumerate() {
688 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
689 None => 0.0,
690 Some(previous) if tight(previous, block) => LIST_GAP,
691 Some(_) => BLOCK_GAP,
692 };
693 let overlay = Overlay {
694 block: ix,
695 part: Part::Body,
696 selection,
697 caret_on,
698 layouts,
699 annotations,
700 placeholder: placeholder.as_ref(),
701 caption,
702 toggle: toggle.as_ref(),
703 copy,
704 };
705 let frame = layouts.map(|layouts| {
708 let layouts = layouts.clone();
709 canvas(
710 move |bounds, _, _| layouts.record_block(ix, bounds),
711 |_, _, _, _| (),
712 )
713 .absolute()
714 .size_full()
715 });
716 column = column.child(
717 div()
723 .mt(px(gap))
724 .pl(px(block.indent as f32 * INDENT_WIDTH))
725 .child(
726 div()
727 .w_full()
728 .relative()
729 .children(frame)
730 .when(overlay.covers_block() && block.opaque(), |el| {
735 el.rounded(px(4.0)).bg(theme.selection)
736 })
737 .child(block_element(
738 block,
739 overlay,
740 &typography,
741 &theme,
742 window,
743 cx,
744 )),
745 ),
746 );
747 }
748
749 column.into_any_element()
750}
751
752fn tight(previous: &Block, next: &Block) -> bool {
754 let marker = |block: &Block| {
755 matches!(
756 block.kind,
757 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
758 )
759 };
760 marker(previous) && (marker(next) || next.indent > previous.indent)
761}
762
763fn block_element(
764 block: &Block,
765 overlay: Overlay,
766 typography: &Typography,
767 theme: &Theme,
768 window: &mut Window,
769 cx: &mut App,
770) -> AnyElement {
771 let body = overlay.at(Part::Body);
772 match &block.kind {
773 BlockKind::Paragraph(text) => text_element(
774 text,
775 typography.body.size(),
776 typography.body.line_height(),
777 FontWeight::NORMAL,
778 body,
779 theme,
780 cx,
781 ),
782 BlockKind::Heading { level, text } => {
783 let heading = typography.heading(*level);
784 text_element(
785 text,
786 heading.size(),
787 heading.line_height(),
788 heading.weight,
789 body,
790 theme,
791 cx,
792 )
793 }
794 BlockKind::Bullet(text) => {
795 marker_row(disc(typography, theme), text, body, typography, theme, cx)
796 }
797 BlockKind::Ordered { number, text } => marker_row(
798 div()
799 .flex_none()
800 .w(px(MARKER_WIDTH))
801 .text_size(px(typography.body.size()))
802 .line_height(px(typography.body.line_height()))
803 .text_color(theme.text_muted)
804 .child(SharedString::from(format!("{number}.")))
805 .into_any_element(),
806 text,
807 body,
808 typography,
809 theme,
810 cx,
811 ),
812 BlockKind::Task { checked, text } => marker_row(
813 checkbox(*checked, overlay, typography, theme),
814 text,
815 body,
816 typography,
817 theme,
818 cx,
819 ),
820 BlockKind::Quote { kind, text } => div()
821 .border_l_2()
822 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
823 .pl(px(12.0))
824 .pr(px(10.0))
825 .py(px(2.0))
826 .text_color(theme.text_muted)
827 .children(kind.map(|kind| {
828 div()
829 .pb(px(2.0))
830 .text_size(px(typography.body.size()))
831 .line_height(px(typography.body.line_height()))
832 .font_weight(FontWeight::SEMIBOLD)
833 .text_color(alert_color(kind, theme))
834 .child(kind.label())
835 }))
836 .child(text_element(
837 text,
838 typography.body.size(),
839 typography.body.line_height(),
840 FontWeight::NORMAL,
841 body,
842 theme,
843 cx,
844 ))
845 .into_any_element(),
846 BlockKind::Code { language, code } => {
847 let overlay = overlay.at(Part::Code);
848 let painted = overlay
852 .caret()
853 .is_none()
854 .then(|| block::render(language.as_deref(), &code.text, window, cx))
855 .flatten();
856 match painted {
857 Some(element) => div()
860 .when(overlay.covers_block(), |el| {
861 el.rounded(px(4.0)).bg(theme.selection)
862 })
863 .child(element)
864 .into_any_element(),
865 None => code_block(
866 language.as_deref(),
867 &code.text,
868 overlay,
869 typography,
870 theme,
871 window,
872 cx,
873 ),
874 }
875 }
876 BlockKind::Image { url, alt, width } => {
877 image(url, alt, *width, overlay, typography, theme, cx)
878 }
879 BlockKind::Bookmark { url, form } => {
880 bookmark(overlay.block, url, *form, typography, theme, cx)
881 }
882 BlockKind::Table {
883 align,
884 header,
885 rows,
886 } => table(align, header, rows, overlay, typography, theme, window, cx),
887 BlockKind::Rule => div()
888 .h(px(1.0))
889 .w_full()
890 .bg(theme.border)
891 .into_any_element(),
892 }
893}
894
895fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
897 div()
898 .flex_none()
899 .w(px(MARKER_WIDTH))
900 .h(px(typography.body.line_height()))
901 .flex()
902 .items_center()
903 .child(
904 div()
905 .ml(px(1.0))
906 .w(px(5.0))
907 .h(px(5.0))
908 .rounded_full()
909 .bg(theme.text_faint),
910 )
911 .into_any_element()
912}
913
914fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
915 let ix = overlay.block;
916 let mut box_ = div()
917 .relative()
918 .w(px(13.0))
919 .h(px(13.0))
920 .rounded(px(3.5))
921 .border_1()
922 .flex()
923 .items_center()
924 .justify_center();
925 box_ = if checked {
926 box_.bg(theme.solid)
927 .border_color(theme.solid)
928 .text_style(TextStyle::Caption)
929 .text_color(theme.on_solid)
930 .child("✓")
931 } else {
932 box_.border_color(theme.border_strong)
933 };
934 box_ = box_.children(overlay.layouts.map(|layouts| {
937 let layouts = layouts.clone();
938 canvas(
939 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
940 |_, _, _, _| (),
941 )
942 .absolute()
943 .size_full()
944 }));
945 if overlay.toggle.is_some() {
948 box_ = box_.cursor_pointer();
949 }
950 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
951 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
952 cx.stop_propagation();
956 toggle(ix, window, cx);
957 });
958 }
959
960 div()
961 .flex_none()
962 .w(px(MARKER_WIDTH))
963 .h(px(typography.body.line_height()))
964 .flex()
965 .items_center()
966 .child(box_)
967 .into_any_element()
968}
969
970fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
972 match kind {
973 QuoteKind::Note => theme.accent,
974 QuoteKind::Tip => theme.success,
975 QuoteKind::Important => theme.busy,
976 QuoteKind::Warning => theme.warning,
977 QuoteKind::Caution => theme.danger,
978 }
979}
980
981fn marker_row(
982 marker: AnyElement,
983 text: &Text,
984 overlay: Overlay,
985 typography: &Typography,
986 theme: &Theme,
987 cx: &App,
988) -> AnyElement {
989 div()
990 .flex()
991 .flex_row()
992 .gap(px(MARKER_GAP))
993 .child(marker)
994 .child(div().flex_1().min_w_0().child(text_element(
995 text,
996 typography.body.size(),
997 typography.body.line_height(),
998 FontWeight::NORMAL,
999 overlay,
1000 theme,
1001 cx,
1002 )))
1003 .into_any_element()
1004}
1005
1006pub struct Flat {
1009 pub text: SharedString,
1010 pub runs: Vec<TextRun>,
1011 pub links: Vec<(Range<usize>, String)>,
1012 pub code: Vec<Range<usize>>,
1013 pub chips: Vec<Range<usize>>,
1014}
1015
1016pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
1019 flatten_with(text, base_weight, theme, |_| None)
1020}
1021
1022pub fn flatten_with(
1025 text: &Text,
1026 base_weight: FontWeight,
1027 theme: &Theme,
1028 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
1029) -> Flat {
1030 let mut cuts: Vec<usize> = text
1031 .marks
1032 .iter()
1033 .flat_map(|span| [span.range.start, span.range.end])
1034 .chain([0, text.text.len()])
1035 .filter(|cut| *cut <= text.text.len())
1036 .collect();
1037 cuts.sort_unstable();
1038 cuts.dedup();
1039
1040 let mut runs = Vec::new();
1041 let mut links: Vec<(Range<usize>, String)> = Vec::new();
1042 let mut code: Vec<Range<usize>> = Vec::new();
1043 let mut chips: Vec<Range<usize>> = Vec::new();
1044
1045 for pair in cuts.windows(2) {
1046 let (start, end) = (pair[0], pair[1]);
1047 let covering = text
1048 .marks
1049 .iter()
1050 .filter(|span| span.range.start <= start && span.range.end >= end);
1051
1052 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
1053 let mut chip = false;
1054 let mut link = None;
1055 let mut custom = crate::MarkPaint::default();
1058 for span in covering {
1059 match &span.mark {
1060 Mark::Bold => bold = true,
1061 Mark::Italic => italic = true,
1062 Mark::Strike => strike = true,
1063 Mark::Code => mono = true,
1064 Mark::Mention { url, .. } => {
1065 chip = true;
1066 link = Some(url.clone());
1067 }
1068 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
1069 Mark::Custom(name) => {
1070 let Some(painted) = paint(name) else { continue };
1071 custom.color = painted.color.or(custom.color);
1072 custom.background = painted.background.or(custom.background);
1073 custom.weight = painted.weight.or(custom.weight);
1074 custom.italic |= painted.italic;
1075 custom.underline |= painted.underline;
1076 custom.strikethrough |= painted.strikethrough;
1077 }
1078 }
1079 }
1080 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
1081
1082 if mono {
1083 match code.last_mut() {
1084 Some(range) if range.end == start => range.end = end,
1085 _ => code.push(start..end),
1086 }
1087 }
1088 if chip {
1089 match chips.last_mut() {
1090 Some(range) if range.end == start => range.end = end,
1091 _ => chips.push(start..end),
1092 }
1093 }
1094 if let Some(url) = &link {
1095 match links.last_mut() {
1096 Some((range, last)) if range.end == start && last == url => range.end = end,
1097 _ => links.push((start..end, url.clone())),
1098 }
1099 }
1100
1101 let mut face = font(if mono {
1102 theme.font_mono.clone()
1103 } else {
1104 theme.font_body.clone()
1105 });
1106 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
1107 FontWeight::SEMIBOLD
1108 } else {
1109 custom.weight.unwrap_or(base_weight)
1110 };
1111 face.style = if italic {
1112 FontStyle::Italic
1113 } else {
1114 FontStyle::Normal
1115 };
1116
1117 runs.push(TextRun {
1118 len: end - start,
1119 font: face,
1120 color: match (mono, custom.color) {
1124 (_, Some(color)) => color,
1125 (true, None) => theme.code_text,
1126 (false, None) => theme.text,
1127 },
1128 background_color: custom.background,
1129 underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
1130 color: Some(theme.text_muted),
1131 thickness: px(1.0),
1132 wavy: false,
1133 }),
1134 strikethrough: strike.then_some(StrikethroughStyle {
1135 thickness: px(1.0),
1136 color: Some(theme.text_muted),
1137 }),
1138 });
1139 }
1140
1141 Flat {
1142 text: text.text.clone().into(),
1143 runs,
1144 links,
1145 code,
1146 chips,
1147 }
1148}
1149
1150fn text_element(
1151 text: &Text,
1152 size: f32,
1153 line_height: f32,
1154 weight: FontWeight,
1155 overlay: Overlay,
1156 theme: &Theme,
1157 cx: &App,
1158) -> AnyElement {
1159 let flat = flatten_with(text, weight, theme, |name| {
1160 crate::marks::paint_of(cx, name, theme)
1161 });
1162 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
1163}
1164
1165fn painted_text(
1171 flat: Flat,
1172 len: usize,
1173 size: f32,
1174 line_height: f32,
1175 overlay: Overlay,
1176 theme: &Theme,
1177) -> AnyElement {
1178 let (ix, part) = (overlay.block, overlay.part);
1179 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
1180 let span = 0..len;
1181 let hint = overlay
1184 .placeholder
1185 .filter(|_| len == 0 && overlay.caret().is_some())
1188 .map(|hint| {
1189 div()
1190 .absolute()
1191 .text_color(theme.text_faint)
1192 .child(hint.clone())
1193 });
1194 let styled = StyledText::new(flat.text).with_runs(flat.runs);
1195 let layout = styled.layout().clone();
1196
1197 let painted: AnyElement = if flat.links.is_empty() {
1198 styled.into_any_element()
1199 } else {
1200 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
1201 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
1202 .on_click(ranges, move |clicked, _window, cx| {
1203 if let Some(url) = urls.get(clicked) {
1204 cx.open_url(url);
1205 }
1206 })
1207 .into_any_element()
1208 };
1209
1210 let wash = theme.code_wash;
1214 let code_ranges = flat.code;
1215 let chip_wash = theme.element_hover;
1216 let chip_edge = theme.border;
1217 let chip_ranges = flat.chips;
1218 let caret_color = theme.caret;
1219 let selection_color = theme.selection;
1220 let annotated = overlay.annotated(len, theme);
1221 let layouts = overlay.layouts.cloned();
1222 let underlay = canvas(
1223 |_, _, _| (),
1224 move |_, _, window, _| {
1225 if let Some(layouts) = &layouts {
1226 layouts.record(ix, part, span.clone(), layout.clone());
1227 }
1228 for (range, wash) in &annotated {
1231 for rect in range_rects(&layout, range, 0.0, 0.0) {
1232 window.paint_quad(quad(
1233 rect,
1234 px(2.0),
1235 *wash,
1236 px(0.0),
1237 gpui::transparent_black(),
1238 BorderStyle::default(),
1239 ));
1240 }
1241 }
1242 if let Some(range) = &selected {
1246 for rect in range_rects(&layout, range, 0.0, 0.0) {
1247 window.paint_quad(quad(
1248 rect,
1249 px(2.0),
1250 selection_color,
1251 px(0.0),
1252 gpui::transparent_black(),
1253 BorderStyle::default(),
1254 ));
1255 }
1256 }
1257 if let Some(offset) = caret
1258 && let Some(head) = layout.position_for_index(offset)
1259 {
1260 window.paint_quad(quad(
1261 caret_quad(head, size, layout.line_height()),
1262 px(0.0),
1263 caret_color,
1264 px(0.0),
1265 gpui::transparent_black(),
1266 BorderStyle::default(),
1267 ));
1268 }
1269 for range in &code_ranges {
1270 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1271 window.paint_quad(quad(
1272 rect,
1273 px(INLINE_CODE_RADIUS),
1274 wash,
1275 px(0.0),
1276 gpui::transparent_black(),
1277 BorderStyle::default(),
1278 ));
1279 }
1280 }
1281 for range in &chip_ranges {
1284 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1285 window.paint_quad(quad(
1286 rect,
1287 px(Theme::control_radius()),
1288 chip_wash,
1289 px(1.0),
1290 chip_edge,
1291 BorderStyle::Solid,
1292 ));
1293 }
1294 }
1295 },
1296 )
1297 .absolute()
1298 .size_full();
1299
1300 div()
1301 .text_size(px(size))
1302 .line_height(px(line_height))
1303 .relative()
1304 .child(underlay)
1305 .children(hint)
1306 .child(painted)
1307 .into_any_element()
1308}
1309
1310fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1316 let inset = (line_height - px(size)) / 2.0;
1317 Bounds::new(
1318 head + point(px(0.0), inset),
1319 gpui::size(px(CARET_WIDTH), px(size)),
1320 )
1321}
1322
1323fn range_rects(
1325 layout: &gpui::TextLayout,
1326 range: &Range<usize>,
1327 pad_x: f32,
1328 inset_y: f32,
1329) -> Vec<Bounds<Pixels>> {
1330 let mut rects = Vec::new();
1331 let line_height = layout.line_height();
1332 let mut origin = layout.bounds().origin;
1333 let mut line_start = 0;
1334 for line in layout.line_layouts() {
1335 let shaped = &line.unwrapped_layout;
1336 let row_ends = line
1339 .wrap_boundaries()
1340 .iter()
1341 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
1342 .chain([line.len()]);
1343 let mut row_start = 0;
1344 for (row, row_end) in row_ends.enumerate() {
1345 let from = range
1346 .start
1347 .saturating_sub(line_start)
1348 .clamp(row_start, row_end);
1349 let to = range.end.saturating_sub(line_start).min(row_end);
1350 let row_x = shaped.x_for_index(row_start);
1351 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
1352 if from < to && right > left {
1353 rects.push(Bounds::new(
1354 origin
1355 + point(
1356 left - row_x - px(pad_x),
1357 line_height * row as f32 + px(inset_y),
1358 ),
1359 size(
1360 right - left + px(2.0 * pad_x),
1361 line_height - px(2.0 * inset_y),
1362 ),
1363 ));
1364 }
1365 row_start = row_end;
1366 }
1367 origin.y += line.size(line_height).height;
1368 line_start += line.len() + 1;
1370 }
1371 rects
1372}
1373
1374pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
1382 let Editing {
1383 selection,
1384 caret_on,
1385 layouts,
1386 annotations,
1387 typography,
1388 ..
1389 } = editing;
1390 let reset = layouts.map(|layouts| {
1394 let layouts = layouts.clone();
1395 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
1396 .absolute()
1397 .size(px(0.0))
1398 });
1399 let theme = Theme::of(cx).clone();
1400 let typography = typography.unwrap_or_else(|| Typography::of(cx));
1401 let overlay = Overlay {
1402 block: 0,
1403 part: Part::Code,
1404 selection,
1405 caret_on,
1406 layouts,
1407 annotations,
1408 placeholder: None,
1409 caption: Caption::default(),
1410 toggle: None,
1412 copy: CopyButton::Hidden,
1414 };
1415 let (underlay, lines) = code_lines(
1416 Some(crate::source::LANGUAGES[0]),
1417 code,
1418 overlay,
1419 &typography,
1420 &theme,
1421 cx,
1422 );
1423 let style = crate::SourceStyle::of(cx);
1425 let digits = lines.len().to_string().len().max(style.gutter_min_digits);
1426 let gap = style.gutter_gap.max(0.0) * typography.code.size();
1427 let gutter_width = digits as f32 * typography.code.size() + gap;
1428 let lines = lines
1429 .into_iter()
1430 .enumerate()
1431 .map(|(index, line)| {
1432 if !style.line_numbers {
1433 return line;
1434 }
1435 div()
1436 .flex()
1437 .items_start()
1438 .child(
1439 div()
1440 .w(px(gutter_width))
1441 .flex_shrink_0()
1442 .pr(px(gap))
1443 .font_family(theme.font_mono.clone())
1444 .text_color(style.gutter_color.unwrap_or(theme.text_faint))
1445 .text_right()
1446 .child((index + 1).to_string()),
1447 )
1448 .child(div().flex_1().min_w_0().child(line))
1449 .into_any_element()
1450 })
1451 .collect();
1452 div()
1453 .flex()
1454 .flex_col()
1455 .children(reset)
1456 .child(code_body(0, underlay, lines, &typography, true))
1457 .into_any_element()
1458}
1459
1460fn code_lines(
1463 language: Option<&str>,
1464 code: &str,
1465 overlay: Overlay,
1466 typography: &Typography,
1467 theme: &Theme,
1468 cx: &App,
1469) -> (AnyElement, Vec<AnyElement>) {
1470 let ix = overlay.block;
1471 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
1476 language
1477 .filter(|language| crate::source::is_markdown(language))
1478 .map(|_| crate::source::spans(code))
1479 });
1480 let mono = font(theme.font_mono.clone());
1481 let run = |len: usize, color: Hsla| TextRun {
1482 len,
1483 font: mono.clone(),
1484 color,
1485 background_color: None,
1486 underline: None,
1487 strikethrough: None,
1488 };
1489 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1494 let mut offset = 0usize;
1495 let lines: Vec<AnyElement> = code
1496 .split('\n')
1497 .map(|line| {
1498 let start = offset;
1499 offset += line.len() + 1;
1500 let mut runs = Vec::new();
1501 let mut pos = 0usize;
1504 if let Some(spans) = &spans {
1505 let end = start + line.len();
1506 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1507 let s = range.start.clamp(start, end) - start;
1508 let e = range.end.min(end) - start;
1509 if s > pos {
1510 runs.push(run(s - pos, theme.text));
1511 }
1512 runs.push(run(e - s, theme.syntax.color(*kind)));
1513 pos = e;
1514 }
1515 }
1516 if pos < line.len() {
1517 runs.push(run(line.len() - pos, theme.text));
1518 }
1519 if runs.is_empty() {
1520 runs.push(run(0, theme.text));
1521 }
1522 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1523 rows.push((start..start + line.len(), styled.layout().clone()));
1524 styled.into_any_element()
1525 })
1526 .collect();
1527
1528 let caret = overlay.caret_painted();
1529 let selected = overlay.selected(code.len());
1530 let sink = overlay.layouts.cloned();
1531 let code_size = typography.code.size();
1532 let annotated = overlay.annotated(code.len(), theme);
1533 let (caret_color, selection_color) = (theme.caret, theme.selection);
1534 let underlay = canvas(
1535 |_, _, _| (),
1536 move |_, _, window, _| {
1537 for (span, layout) in &rows {
1538 if let Some(sink) = &sink {
1539 sink.record(ix, Part::Code, span.clone(), layout.clone());
1540 }
1541 for (range, wash) in &annotated {
1542 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1543 if from < to {
1544 for rect in
1545 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1546 {
1547 window.paint_quad(quad(
1548 rect,
1549 px(2.0),
1550 *wash,
1551 px(0.0),
1552 gpui::transparent_black(),
1553 BorderStyle::default(),
1554 ));
1555 }
1556 }
1557 }
1558 if let Some(range) = &selected {
1559 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1560 if from < to {
1561 for rect in
1562 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1563 {
1564 window.paint_quad(quad(
1565 rect,
1566 px(2.0),
1567 selection_color,
1568 px(0.0),
1569 gpui::transparent_black(),
1570 BorderStyle::default(),
1571 ));
1572 }
1573 }
1574 }
1575 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1576 && let Some(head) = layout.position_for_index(offset - span.start)
1577 {
1578 window.paint_quad(quad(
1579 caret_quad(head, code_size, layout.line_height()),
1580 px(0.0),
1581 caret_color,
1582 px(0.0),
1583 gpui::transparent_black(),
1584 BorderStyle::default(),
1585 ));
1586 }
1587 }
1588 },
1589 )
1590 .absolute()
1591 .size_full();
1592
1593 (underlay.into_any_element(), lines)
1594}
1595
1596fn code_block(
1597 language: Option<&str>,
1598 code: &str,
1599 overlay: Overlay,
1600 typography: &Typography,
1601 theme: &Theme,
1602 window: &mut Window,
1603 cx: &mut App,
1604) -> AnyElement {
1605 let ix = overlay.block;
1606 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
1607 let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
1608
1609 div()
1610 .rounded(px(Theme::panel_radius()))
1611 .bg(theme.ink(0.035))
1612 .border_1()
1613 .border_color(theme.border)
1614 .overflow_hidden()
1615 .relative()
1616 .child(
1620 div()
1621 .relative()
1622 .flex()
1623 .flex_row()
1624 .items_center()
1625 .px(px(CODE_PADDING_X))
1626 .py(px(5.0))
1627 .border_b_1()
1628 .border_color(theme.border)
1629 .bg(theme.ink(0.02))
1630 .text_style(TextStyle::Subheadline)
1631 .text_color(match language {
1632 Some(_) => theme.text_muted,
1633 None => theme.text_faint,
1634 })
1635 .child(
1639 div()
1640 .relative()
1641 .children(overlay.layouts.map(|layouts| {
1642 let layouts = layouts.clone();
1643 canvas(
1644 move |bounds, _, _| layouts.record_language(ix, bounds),
1645 |_, _, _, _| (),
1646 )
1647 .absolute()
1648 .size_full()
1649 }))
1650 .child(SharedString::from(
1651 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1652 )),
1653 ),
1654 )
1655 .child(body)
1656 .children(
1657 (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
1658 )
1659 .into_any_element()
1660}
1661
1662fn code_body(
1664 ix: usize,
1665 underlay: AnyElement,
1666 lines: Vec<AnyElement>,
1667 typography: &Typography,
1668 wrap: bool,
1669) -> AnyElement {
1670 let column = div()
1671 .flex()
1672 .flex_col()
1673 .px(px(CODE_PADDING_X))
1674 .children(lines);
1675 let body = div()
1676 .id(ElementId::named_usize("md-code", ix))
1677 .relative()
1678 .py(px(CODE_PADDING_Y))
1679 .text_size(px(typography.code.size()))
1680 .line_height(px(typography.code.line_height()))
1681 .child(underlay);
1682 if wrap {
1683 body.child(column.w_full()).into_any_element()
1686 } else {
1687 ui::scroll::Viewport::new(
1688 format!("md-code-scroll-{ix}"),
1689 body.flex()
1690 .flex_row()
1691 .whitespace_nowrap()
1692 .child(column.items_start()),
1699 gpui::Axis::Horizontal,
1700 )
1701 .into_any_element()
1702 }
1703}
1704
1705fn copy_button(
1712 code: &str,
1713 ix: usize,
1714 theme: &Theme,
1715 window: &mut Window,
1716 cx: &mut App,
1717) -> AnyElement {
1718 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1719 let showing = *copied.read(cx);
1720 let text: SharedString = code.to_string().into();
1721
1722 div()
1723 .id(ElementId::named_usize("md-copy", ix))
1724 .absolute()
1725 .top(px(3.0))
1726 .right(px(5.0))
1727 .h(px(20.0))
1728 .px(px(6.0))
1729 .rounded(px(5.0))
1730 .flex()
1731 .items_center()
1732 .cursor_pointer()
1733 .text_style(TextStyle::Caption)
1734 .text_color(theme.text_muted)
1735 .hover(|el| el.bg(theme.element_hover))
1736 .child(if showing { "Copied" } else { "Copy" })
1737 .on_click({
1738 let copied = copied.clone();
1739 move |_, _, cx| {
1740 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1741 copied.update(cx, |state, cx| {
1742 *state = true;
1743 cx.notify();
1744 });
1745 }
1746 })
1747 .on_hover(move |hovering, _, cx| {
1748 if !*hovering && *copied.read(cx) {
1749 copied.update(cx, |state, cx| {
1750 *state = false;
1751 cx.notify();
1752 });
1753 }
1754 })
1755 .into_any_element()
1756}
1757
1758fn image(
1765 url: &str,
1766 alt: &Text,
1767 width: Option<u32>,
1768 overlay: Overlay,
1769 typography: &Typography,
1770 theme: &Theme,
1771 cx: &App,
1772) -> AnyElement {
1773 let hint = SharedString::new_static(CAPTION_HINT);
1774 let overlay = Overlay {
1775 placeholder: Some(&hint),
1776 ..overlay.at(Part::Caption)
1777 };
1778 let picture = if url.is_empty() {
1779 div()
1780 .h(px(IMAGE_EMPTY_HEIGHT))
1781 .flex()
1782 .items_center()
1783 .px(px(CARD_PADDING))
1784 .rounded(px(Theme::button_radius()))
1785 .border_1()
1786 .border_dashed()
1787 .border_color(theme.border)
1788 .text_size(px(typography.body.size()))
1789 .text_color(theme.text_muted)
1790 .child(IMAGE_EMPTY)
1791 } else {
1792 let picture = match url.contains("://") {
1796 true => img(SharedString::from(url.to_string())),
1797 false => img(std::path::PathBuf::from(url)),
1798 };
1799 let box_ = div()
1800 .relative()
1801 .rounded(px(Theme::button_radius()))
1802 .overflow_hidden()
1803 .border_1()
1804 .border_color(theme.border)
1805 .children(overlay.layouts.map(|layouts| {
1806 let layouts = layouts.clone();
1807 let ix = overlay.block;
1808 canvas(
1809 move |bounds, _, _| layouts.record_picture(ix, bounds),
1810 |_, _, _, _| (),
1811 )
1812 .absolute()
1813 .size_full()
1814 }));
1815 match width {
1816 Some(width) => box_
1821 .self_start()
1822 .max_w_full()
1823 .w(px(width as f32))
1824 .child(picture.w(px(width as f32)).max_w_full()),
1825 None => box_.child(picture.max_w_full()),
1828 }
1829 };
1830 div()
1831 .flex()
1832 .flex_col()
1833 .gap(px(CAPTION_GAP))
1834 .child(picture)
1835 .when(
1838 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1839 |el| {
1840 el.child(text_element(
1841 alt,
1842 typography.caption.size(),
1843 typography.caption.line_height(),
1844 FontWeight::NORMAL,
1845 overlay,
1846 theme,
1847 cx,
1848 ))
1849 },
1850 )
1851 .into_any_element()
1852}
1853
1854fn bookmark(
1866 ix: usize,
1867 url: &str,
1868 form: Form,
1869 typography: &Typography,
1870 theme: &Theme,
1871 cx: &App,
1872) -> AnyElement {
1873 let preview = preview::of(cx, url).unwrap_or_default();
1874 let host = SharedString::from(preview::host(url).to_string());
1875 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1876 let title = preview
1877 .title
1878 .clone()
1879 .unwrap_or_else(|| SharedString::from(url.to_string()));
1880
1881 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1884 let site = host.clone();
1885 let mark = move |size: f32| {
1886 let host = site.clone();
1887 match icon.clone() {
1888 Some(icon) => img(icon)
1889 .size(px(size))
1890 .rounded(px(size / 4.0))
1891 .with_fallback(move || initial(&host, size, muted, wash))
1892 .into_any_element(),
1893 None => initial(&host, size, muted, wash),
1894 }
1895 };
1896
1897 if form == Form::Chip {
1898 let open = url.to_string();
1899 let pill = div()
1900 .id(ElementId::named_usize("md-chip", ix))
1901 .flex()
1902 .flex_row()
1903 .items_center()
1904 .gap(px(6.0))
1905 .px(px(CHIP_BLOCK_PAD_X))
1906 .py(px(CHIP_BLOCK_PAD_Y))
1907 .rounded(px(Theme::control_radius()))
1908 .border_1()
1909 .border_color(theme.border)
1910 .bg(theme.element_hover)
1911 .text_size(px(typography.body.size()))
1912 .line_height(px(typography.body.line_height()))
1913 .text_color(theme.text)
1914 .cursor(CursorStyle::PointingHand)
1915 .hover(|el| el.bg(theme.element_active))
1916 .on_click(move |_, _, cx| cx.open_url(&open))
1917 .child(mark(CHIP_ICON))
1918 .child(
1921 div()
1922 .min_w_0()
1923 .truncate()
1924 .child(preview.title.unwrap_or(label)),
1925 );
1926 return div().flex().flex_row().child(pill).into_any_element();
1929 }
1930
1931 let words = div()
1932 .flex()
1933 .flex_col()
1934 .min_w_0()
1935 .px(px(CARD_PADDING))
1936 .py(px(CARD_PADDING - 2.0))
1937 .child(
1938 div()
1939 .truncate()
1940 .text_size(px(typography.body.size()))
1941 .line_height(px(typography.body.line_height()))
1942 .text_color(theme.text)
1943 .child(title),
1944 )
1945 .children(preview.description.map(|blurb| {
1946 div()
1947 .line_clamp(2)
1948 .text_size(px(typography.card.size()))
1949 .line_height(px(typography.card.line_height()))
1950 .text_color(theme.text_muted)
1951 .child(blurb)
1952 }))
1953 .child(
1954 div()
1955 .mt_auto()
1956 .pt(px(6.0))
1957 .flex()
1958 .items_center()
1959 .gap(px(6.0))
1960 .text_size(px(typography.card.size()))
1961 .text_color(theme.text_muted)
1962 .child(mark(CARD_ICON))
1963 .child(div().truncate().child(label)),
1964 );
1965
1966 let picture = corners(div(), form)
1967 .bg(theme.surface)
1968 .flex()
1969 .items_center()
1970 .justify_center()
1971 .overflow_hidden()
1972 .child(match preview.image {
1973 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
1974 .with_fallback(move || mark(CARD_COVER))
1975 .into_any_element(),
1976 None => mark(CARD_COVER),
1977 });
1978
1979 let open = url.to_string();
1980 let card = div()
1981 .id(ElementId::named_usize("md-bookmark", ix))
1982 .flex()
1983 .w_full()
1984 .overflow_hidden()
1985 .rounded(px(Theme::button_radius()))
1986 .border(px(CARD_BORDER))
1987 .border_color(theme.border)
1988 .bg(theme.surface_card)
1989 .cursor(CursorStyle::PointingHand)
1990 .hover(|el| el.bg(theme.element_hover))
1991 .on_click(move |_, _, cx| cx.open_url(&open));
1992
1993 if form == Form::Embed {
1994 card.flex_col()
1995 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1996 .child(words.w_full())
1997 } else {
1998 card.h(px(CARD_HEIGHT))
1999 .child(words.flex_1())
2000 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
2001 }
2002 .into_any_element()
2003}
2004
2005fn corners<T: Styled>(element: T, form: Form) -> T {
2009 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
2010 match form {
2011 Form::Embed => element.rounded_t(corner),
2012 _ => element.rounded_r(corner),
2013 }
2014}
2015
2016fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
2019 div()
2020 .flex_none()
2021 .size(px(size))
2022 .rounded(px(size / 4.0))
2023 .bg(wash)
2024 .flex()
2025 .items_center()
2026 .justify_center()
2027 .text_size(px(size * 0.55))
2028 .text_color(color)
2029 .child(SharedString::from(
2030 host.chars()
2031 .next()
2032 .unwrap_or('?')
2033 .to_uppercase()
2034 .to_string(),
2035 ))
2036 .into_any_element()
2037}
2038
2039#[expect(
2046 clippy::too_many_arguments,
2047 reason = "a table, its overlay, and what paints them"
2048)]
2049fn table(
2050 align: &[Align],
2051 header: &[Text],
2052 rows: &[Vec<Text>],
2053 overlay: Overlay,
2054 typography: &Typography,
2055 theme: &Theme,
2056 window: &mut Window,
2057 cx: &App,
2058) -> AnyElement {
2059 let ix = overlay.block;
2060 let all: Vec<&[Text]> = std::iter::once(header)
2061 .filter(|row| !row.is_empty())
2062 .chain(rows.iter().map(|row| row.as_slice()))
2063 .collect();
2064 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
2065 if columns == 0 {
2066 return gpui::Empty.into_any_element();
2067 }
2068 let has_header = !header.is_empty();
2069
2070 let text_system = window.text_system();
2071 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
2072 let mut content = vec![0.0f32; columns];
2073 for (r, row) in all.iter().enumerate() {
2074 let weight = if has_header && r == 0 {
2075 FontWeight::BOLD
2076 } else {
2077 FontWeight::NORMAL
2078 };
2079 let mut out = Vec::with_capacity(columns);
2080 for (c, natural) in content.iter_mut().enumerate() {
2081 let Some(cell) = row.get(c) else {
2082 out.push(None);
2083 continue;
2084 };
2085 let flat = flatten_with(cell, weight, theme, |name| {
2086 crate::marks::paint_of(cx, name, theme)
2087 });
2088 if !flat.text.is_empty() {
2089 let width = f32::from(
2090 text_system
2091 .shape_line(
2092 flat.text.clone(),
2093 px(typography.body.size()),
2094 &flat.runs,
2095 None,
2096 )
2097 .width(),
2098 );
2099 *natural = natural.max(width);
2100 }
2101 out.push(Some(flat));
2102 }
2103 flats.push(out);
2104 }
2105
2106 let naturals: Vec<f32> = content
2107 .iter()
2108 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
2109 .collect();
2110 let minimums: Vec<f32> = naturals
2111 .iter()
2112 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
2113 .collect();
2114 let hairline = theme.hairline(0.10);
2115
2116 let mut inner = div()
2117 .flex()
2118 .flex_col()
2119 .w_full()
2120 .min_w(px(minimums.iter().sum::<f32>()));
2121 for (r, row) in flats.into_iter().enumerate() {
2122 if r > 0 {
2123 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
2124 }
2125 let mut row_el = div().flex().flex_row();
2126 for (c, cell) in row.into_iter().enumerate() {
2127 let mut cell_el = div()
2128 .flex_grow(naturals[c])
2129 .flex_shrink(naturals[c])
2130 .flex_basis(px(0.0))
2131 .min_w(px(minimums[c]))
2132 .p(px(TABLE_CELL_PADDING))
2133 .text_size(px(typography.body.size()))
2134 .line_height(px(typography.body.line_height()));
2135 cell_el = match align.get(c).copied().unwrap_or_default() {
2136 Align::Left => cell_el,
2137 Align::Center => cell_el.text_center(),
2138 Align::Right => cell_el.text_right(),
2139 };
2140 if let Some(flat) = cell {
2141 let row = if has_header { r } else { r + 1 };
2145 let len = flat.text.len();
2146 cell_el = cell_el.child(painted_text(
2147 flat,
2148 len,
2149 typography.body.size(),
2150 typography.body.line_height(),
2151 overlay.at(Part::Cell { row, column: c }),
2152 theme,
2153 ));
2154 }
2155 row_el = row_el.child(cell_el);
2156 }
2157 inner = inner.child(row_el);
2158 }
2159
2160 ui::scroll::Viewport::new(
2161 format!("md-table-scroll-{ix}"),
2162 div()
2163 .id(ElementId::named_usize("md-table", ix))
2164 .w_full()
2165 .child(inner),
2166 gpui::Axis::Horizontal,
2167 )
2168 .into_any_element()
2169}