1use std::{cell::RefCell, ops::Range, rc::Rc};
11
12use gpui::{
13 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
14 InteractiveText, ObjectFit, Pixels, Point, SharedString, StrikethroughStyle, StyledImage as _,
15 StyledText, TextLayout, TextRun, UnderlineStyle, Window, canvas, div, font, img, point,
16 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)]
112pub enum Annotation {
113 #[default]
115 Open,
116 Resolved,
118 Active,
120}
121
122impl Annotation {
123 fn wash(self, theme: &Theme) -> Hsla {
124 match self {
125 Self::Open => theme.warning.opacity(0.20),
126 Self::Resolved => theme.warning.opacity(0.08),
127 Self::Active => theme.warning.opacity(0.38),
128 }
129 }
130}
131
132#[derive(Clone)]
138pub struct Editing<'a> {
139 pub selection: Option<Selection>,
142 pub caret_on: bool,
145 pub layouts: Option<&'a BlockLayouts>,
147 pub annotations: &'a [(Selection, Annotation)],
149 pub placeholder: Option<SharedString>,
151 pub caption: Caption,
152 pub typography: Option<Typography>,
156}
157
158impl Default for Editing<'_> {
159 fn default() -> Self {
160 Self {
161 selection: None,
162 caret_on: true,
165 layouts: None,
166 annotations: &[],
167 placeholder: None,
168 caption: Caption::default(),
169 typography: None,
170 }
171 }
172}
173
174#[derive(Clone, Default)]
181pub struct BlockLayouts(Rc<RefCell<Frames>>);
182
183#[derive(Default)]
184struct Frames {
185 texts: Vec<Painted>,
186 blocks: Vec<(usize, Bounds<Pixels>)>,
189 languages: Vec<(usize, Bounds<Pixels>)>,
192 pictures: Vec<(usize, Bounds<Pixels>)>,
196}
197
198struct Painted {
205 block: usize,
206 part: Part,
207 range: Range<usize>,
208 layout: TextLayout,
209}
210
211impl BlockLayouts {
212 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
218 let entries = &self.0.borrow().texts;
219 let cursor = |painted: &Painted| {
220 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
221 Cursor::new(
222 painted.block,
223 painted.part,
224 painted.range.start + offset.min(painted.range.len()),
225 )
226 };
227 if let Some(painted) = entries
228 .iter()
229 .find(|painted| painted.layout.bounds().contains(&point))
230 {
231 return Some(cursor(painted));
232 }
233 entries
234 .iter()
235 .min_by_key(|painted| {
236 let bounds = painted.layout.bounds();
237 let above = (bounds.origin.y - point.y).abs();
238 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
239 f32::from(above.min(below)) as i64
240 })
241 .map(cursor)
242 }
243
244 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
250 let entries = &self.0.borrow().texts;
251 let painted = entries.iter().find(|painted| {
252 painted.block == at.block
253 && painted.part == at.part
254 && painted.range.start <= at.offset
255 && at.offset <= painted.range.end
256 })?;
257 let point = painted
258 .layout
259 .position_for_index(at.offset - painted.range.start)?;
260 Some((point, painted.layout.line_height()))
261 }
262
263 pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
271 let (start, end) = selection.ordered();
272 self.0
273 .borrow()
274 .texts
275 .iter()
276 .filter_map(|painted| {
277 let here = Cursor::new(painted.block, painted.part, 0);
278 let (from, to) = (
279 Cursor::new(start.block, start.part, 0),
280 Cursor::new(end.block, end.part, 0),
281 );
282 if here < from || here > to {
283 return None;
284 }
285 let len = painted.range.len();
288 let first = if here == from { start.offset } else { 0 };
289 let last = if here == to { end.offset } else { usize::MAX };
290 let range = first.saturating_sub(painted.range.start).min(len)
291 ..last.saturating_sub(painted.range.start).min(len);
292 (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
293 })
294 .flatten()
295 .collect()
296 }
297
298 pub fn step_row(
309 &self,
310 at: Cursor,
311 from: Point<Pixels>,
312 down: bool,
313 ) -> Option<(Cursor, Pixels)> {
314 let entries = &self.0.borrow().texts;
315 let ix = entries.iter().position(|painted| {
316 painted.block == at.block
317 && painted.part == at.part
318 && painted.range.start <= at.offset
319 && at.offset <= painted.range.end
320 })?;
321 let here = &entries[ix];
322 let line = here.layout.line_height();
323 let index_at = |painted: &Painted, y: Pixels| {
324 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
325 (
326 Cursor::new(
327 painted.block,
328 painted.part,
329 painted.range.start + offset.min(painted.range.len()),
330 ),
331 y,
332 )
333 };
334
335 let bounds = here.layout.bounds();
338 let target = if down { from.y + line } else { from.y - line };
339 if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
340 return Some(index_at(here, target));
341 }
342
343 let next = match down {
344 true => entries.get(ix + 1)?,
345 false => entries.get(ix.checked_sub(1)?)?,
346 };
347 let bounds = next.layout.bounds();
349 let row = match down {
350 true => bounds.origin.y,
351 false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
352 };
353 Some(index_at(next, row))
354 }
355
356 pub fn over_text(&self, point: Point<Pixels>) -> bool {
363 self.0
364 .borrow()
365 .texts
366 .iter()
367 .any(|painted| painted.layout.bounds().contains(&point))
368 }
369
370 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
372 let blocks = &self.0.borrow().blocks;
373 blocks
374 .iter()
375 .find(|(_, bounds)| bounds.contains(&point))
376 .or_else(|| {
377 blocks.iter().min_by_key(|(_, bounds)| {
378 let above = (bounds.origin.y - point.y).abs();
379 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
380 f32::from(above.min(below)) as i64
381 })
382 })
383 .map(|(ix, _)| *ix)
384 }
385
386 pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
395 let texts = &self.0.borrow().texts;
396 let painted = texts.iter().find(|painted| painted.block == ix)?;
397 Some((
398 painted.layout.bounds().origin.y,
399 painted.layout.line_height(),
400 ))
401 }
402
403 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
405 self.0
406 .borrow()
407 .blocks
408 .iter()
409 .find(|(block, _)| *block == ix)
410 .map(|(_, bounds)| *bounds)
411 }
412
413 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
418 self.0
419 .borrow()
420 .languages
421 .iter()
422 .find(|(block, _)| *block == ix)
423 .map(|(_, bounds)| *bounds)
424 }
425
426 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
431 self.0
432 .borrow()
433 .pictures
434 .iter()
435 .find(|(block, _)| *block == ix)
436 .map(|(_, bounds)| *bounds)
437 }
438
439 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
440 self.0.borrow_mut().texts.push(Painted {
441 block,
442 part,
443 range,
444 layout,
445 });
446 }
447
448 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
449 self.0.borrow_mut().blocks.push((ix, bounds));
450 }
451
452 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
453 self.0.borrow_mut().languages.push((ix, bounds));
454 }
455
456 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
457 self.0.borrow_mut().pictures.push((ix, bounds));
458 }
459
460 fn clear(&self) {
461 let mut frames = self.0.borrow_mut();
462 frames.texts.clear();
463 frames.blocks.clear();
464 frames.languages.clear();
465 frames.pictures.clear();
466 }
467}
468
469#[derive(Clone, Copy)]
475struct Overlay<'a> {
476 block: usize,
477 part: Part,
478 selection: Option<Selection>,
479 caret_on: bool,
480 layouts: Option<&'a BlockLayouts>,
481 annotations: &'a [(Selection, Annotation)],
483 placeholder: Option<&'a SharedString>,
486 caption: Caption,
487}
488
489impl<'a> Overlay<'a> {
490 fn at(self, part: Part) -> Self {
491 Self { part, ..self }
492 }
493
494 fn here(&self) -> Cursor {
495 Cursor::new(self.block, self.part, 0)
496 }
497
498 fn caret_painted(&self) -> Option<usize> {
504 self.caret_on.then(|| self.caret()).flatten()
505 }
506
507 fn caret(&self) -> Option<usize> {
509 self.selection
510 .map(|selection| selection.head)
511 .filter(|head| head.block == self.block && head.part == self.part)
512 .map(|head| head.offset)
513 }
514
515 fn selected(&self, len: usize) -> Option<Range<usize>> {
517 self.clip(self.selection?, len)
518 }
519
520 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
523 self.annotations
524 .iter()
525 .filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
526 .collect()
527 }
528
529 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
535 if selection.is_collapsed() {
536 return None;
537 }
538 let (start, end) = selection.ordered();
539 let here = self.here();
540 let (first, last) = (
541 Cursor::new(start.block, start.part, 0),
542 Cursor::new(end.block, end.part, 0),
543 );
544 if here < first || here > last {
545 return None;
546 }
547 let from = if here == first { start.offset } else { 0 };
548 let to = if here == last { end.offset } else { len };
549 (from < to).then_some(from..to.min(len))
550 }
551
552 fn covers_block(&self) -> bool {
556 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
557 return false;
558 };
559 let (start, end) = selection.ordered();
560 start.block < self.block && self.block < end.block
561 }
562}
563
564pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
566 let doc = crate::parse_with(source, &crate::Marks::of(cx));
567 render(&doc, Caption::default(), window, cx)
568}
569
570pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
572 render_with(
573 doc,
574 Editing {
575 caption,
576 ..Editing::default()
577 },
578 window,
579 cx,
580 )
581}
582
583pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
591 let Editing {
592 selection,
593 caret_on,
594 layouts,
595 annotations,
596 placeholder,
597 caption,
598 typography,
599 } = editing;
600 let reset = layouts.map(|layouts| {
606 let layouts = layouts.clone();
607 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
608 .absolute()
609 .size(px(0.0))
610 });
611 let theme = Theme::of(cx).clone();
614 let typography = typography.unwrap_or_else(|| Typography::of(cx));
615 let mut column = div().flex().flex_col().children(reset);
616
617 for (ix, block) in doc.blocks.iter().enumerate() {
618 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
619 None => 0.0,
620 Some(previous) if tight(previous, block) => LIST_GAP,
621 Some(_) => BLOCK_GAP,
622 };
623 let overlay = Overlay {
624 block: ix,
625 part: Part::Body,
626 selection,
627 caret_on,
628 layouts,
629 annotations,
630 placeholder: placeholder.as_ref(),
631 caption,
632 };
633 let frame = layouts.map(|layouts| {
636 let layouts = layouts.clone();
637 canvas(
638 move |bounds, _, _| layouts.record_block(ix, bounds),
639 |_, _, _, _| (),
640 )
641 .absolute()
642 .size_full()
643 });
644 column = column.child(
645 div()
651 .mt(px(gap))
652 .pl(px(block.indent as f32 * INDENT_WIDTH))
653 .child(
654 div()
655 .w_full()
656 .relative()
657 .children(frame)
658 .when(overlay.covers_block() && block.opaque(), |el| {
663 el.rounded(px(4.0)).bg(theme.selection)
664 })
665 .child(block_element(
666 block,
667 overlay,
668 &typography,
669 &theme,
670 window,
671 cx,
672 )),
673 ),
674 );
675 }
676
677 column.into_any_element()
678}
679
680fn tight(previous: &Block, next: &Block) -> bool {
682 let marker = |block: &Block| {
683 matches!(
684 block.kind,
685 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
686 )
687 };
688 marker(previous) && (marker(next) || next.indent > previous.indent)
689}
690
691fn block_element(
692 block: &Block,
693 overlay: Overlay,
694 typography: &Typography,
695 theme: &Theme,
696 window: &mut Window,
697 cx: &mut App,
698) -> AnyElement {
699 let body = overlay.at(Part::Body);
700 match &block.kind {
701 BlockKind::Paragraph(text) => text_element(
702 text,
703 typography.body.size(),
704 typography.body.line_height(),
705 FontWeight::NORMAL,
706 body,
707 theme,
708 cx,
709 ),
710 BlockKind::Heading { level, text } => {
711 let heading = typography.heading(*level);
712 text_element(
713 text,
714 heading.size(),
715 heading.line_height(),
716 heading.weight,
717 body,
718 theme,
719 cx,
720 )
721 }
722 BlockKind::Bullet(text) => {
723 marker_row(disc(typography, theme), text, body, typography, theme, cx)
724 }
725 BlockKind::Ordered { number, text } => marker_row(
726 div()
727 .flex_none()
728 .w(px(MARKER_WIDTH))
729 .text_size(px(typography.body.size()))
730 .line_height(px(typography.body.line_height()))
731 .text_color(theme.text_muted)
732 .child(SharedString::from(format!("{number}.")))
733 .into_any_element(),
734 text,
735 body,
736 typography,
737 theme,
738 cx,
739 ),
740 BlockKind::Task { checked, text } => marker_row(
741 checkbox(*checked, typography, theme),
742 text,
743 body,
744 typography,
745 theme,
746 cx,
747 ),
748 BlockKind::Quote { kind, text } => div()
749 .border_l_2()
750 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
751 .pl(px(12.0))
752 .pr(px(10.0))
753 .py(px(2.0))
754 .text_color(theme.text_muted)
755 .children(kind.map(|kind| {
756 div()
757 .pb(px(2.0))
758 .text_size(px(typography.body.size()))
759 .line_height(px(typography.body.line_height()))
760 .font_weight(FontWeight::SEMIBOLD)
761 .text_color(alert_color(kind, theme))
762 .child(kind.label())
763 }))
764 .child(text_element(
765 text,
766 typography.body.size(),
767 typography.body.line_height(),
768 FontWeight::NORMAL,
769 body,
770 theme,
771 cx,
772 ))
773 .into_any_element(),
774 BlockKind::Code { language, code } => {
775 let overlay = overlay.at(Part::Code);
776 let painted = overlay
780 .caret()
781 .is_none()
782 .then(|| block::render(language.as_deref(), &code.text, window, cx))
783 .flatten();
784 match painted {
785 Some(element) => div()
788 .when(overlay.covers_block(), |el| {
789 el.rounded(px(4.0)).bg(theme.selection)
790 })
791 .child(element)
792 .into_any_element(),
793 None => code_block(
794 language.as_deref(),
795 &code.text,
796 overlay,
797 typography,
798 theme,
799 window,
800 cx,
801 ),
802 }
803 }
804 BlockKind::Image { url, alt, width } => {
805 image(url, alt, *width, overlay, typography, theme, cx)
806 }
807 BlockKind::Bookmark { url, form } => {
808 bookmark(overlay.block, url, *form, typography, theme, cx)
809 }
810 BlockKind::Table {
811 align,
812 header,
813 rows,
814 } => table(align, header, rows, overlay, typography, theme, window, cx),
815 BlockKind::Rule => div()
816 .h(px(1.0))
817 .w_full()
818 .bg(theme.border)
819 .into_any_element(),
820 }
821}
822
823fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
825 div()
826 .flex_none()
827 .w(px(MARKER_WIDTH))
828 .h(px(typography.body.line_height()))
829 .flex()
830 .items_center()
831 .child(
832 div()
833 .ml(px(1.0))
834 .w(px(5.0))
835 .h(px(5.0))
836 .rounded_full()
837 .bg(theme.text_faint),
838 )
839 .into_any_element()
840}
841
842fn checkbox(checked: bool, typography: &Typography, theme: &Theme) -> AnyElement {
843 let mut box_ = div()
844 .w(px(13.0))
845 .h(px(13.0))
846 .rounded(px(3.5))
847 .border_1()
848 .flex()
849 .items_center()
850 .justify_center();
851 box_ = if checked {
852 box_.bg(theme.solid)
853 .border_color(theme.solid)
854 .text_style(TextStyle::Caption)
855 .text_color(theme.on_solid)
856 .child("✓")
857 } else {
858 box_.border_color(theme.border_strong)
859 };
860
861 div()
862 .flex_none()
863 .w(px(MARKER_WIDTH))
864 .h(px(typography.body.line_height()))
865 .flex()
866 .items_center()
867 .child(box_)
868 .into_any_element()
869}
870
871fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
873 match kind {
874 QuoteKind::Note => theme.accent,
875 QuoteKind::Tip => theme.success,
876 QuoteKind::Important => theme.busy,
877 QuoteKind::Warning => theme.warning,
878 QuoteKind::Caution => theme.danger,
879 }
880}
881
882fn marker_row(
883 marker: AnyElement,
884 text: &Text,
885 overlay: Overlay,
886 typography: &Typography,
887 theme: &Theme,
888 cx: &App,
889) -> AnyElement {
890 div()
891 .flex()
892 .flex_row()
893 .gap(px(MARKER_GAP))
894 .child(marker)
895 .child(div().flex_1().min_w_0().child(text_element(
896 text,
897 typography.body.size(),
898 typography.body.line_height(),
899 FontWeight::NORMAL,
900 overlay,
901 theme,
902 cx,
903 )))
904 .into_any_element()
905}
906
907pub struct Flat {
910 pub text: SharedString,
911 pub runs: Vec<TextRun>,
912 pub links: Vec<(Range<usize>, String)>,
913 pub code: Vec<Range<usize>>,
914 pub chips: Vec<Range<usize>>,
915}
916
917pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
920 flatten_with(text, base_weight, theme, |_| None)
921}
922
923pub fn flatten_with(
926 text: &Text,
927 base_weight: FontWeight,
928 theme: &Theme,
929 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
930) -> Flat {
931 let mut cuts: Vec<usize> = text
932 .marks
933 .iter()
934 .flat_map(|span| [span.range.start, span.range.end])
935 .chain([0, text.text.len()])
936 .filter(|cut| *cut <= text.text.len())
937 .collect();
938 cuts.sort_unstable();
939 cuts.dedup();
940
941 let mut runs = Vec::new();
942 let mut links: Vec<(Range<usize>, String)> = Vec::new();
943 let mut code: Vec<Range<usize>> = Vec::new();
944 let mut chips: Vec<Range<usize>> = Vec::new();
945
946 for pair in cuts.windows(2) {
947 let (start, end) = (pair[0], pair[1]);
948 let covering = text
949 .marks
950 .iter()
951 .filter(|span| span.range.start <= start && span.range.end >= end);
952
953 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
954 let mut chip = false;
955 let mut link = None;
956 let mut custom = crate::MarkPaint::default();
959 for span in covering {
960 match &span.mark {
961 Mark::Bold => bold = true,
962 Mark::Italic => italic = true,
963 Mark::Strike => strike = true,
964 Mark::Code => mono = true,
965 Mark::Mention { url, .. } => {
966 chip = true;
967 link = Some(url.clone());
968 }
969 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
970 Mark::Custom(name) => {
971 let Some(painted) = paint(name) else { continue };
972 custom.color = painted.color.or(custom.color);
973 custom.background = painted.background.or(custom.background);
974 custom.weight = painted.weight.or(custom.weight);
975 custom.italic |= painted.italic;
976 custom.underline |= painted.underline;
977 custom.strikethrough |= painted.strikethrough;
978 }
979 }
980 }
981 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
982
983 if mono {
984 match code.last_mut() {
985 Some(range) if range.end == start => range.end = end,
986 _ => code.push(start..end),
987 }
988 }
989 if chip {
990 match chips.last_mut() {
991 Some(range) if range.end == start => range.end = end,
992 _ => chips.push(start..end),
993 }
994 }
995 if let Some(url) = &link {
996 match links.last_mut() {
997 Some((range, last)) if range.end == start && last == url => range.end = end,
998 _ => links.push((start..end, url.clone())),
999 }
1000 }
1001
1002 let mut face = font(if mono {
1003 theme.font_mono.clone()
1004 } else {
1005 theme.font_sans.clone()
1006 });
1007 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
1008 FontWeight::SEMIBOLD
1009 } else {
1010 custom.weight.unwrap_or(base_weight)
1011 };
1012 face.style = if italic {
1013 FontStyle::Italic
1014 } else {
1015 FontStyle::Normal
1016 };
1017
1018 runs.push(TextRun {
1019 len: end - start,
1020 font: face,
1021 color: match (mono, custom.color) {
1025 (_, Some(color)) => color,
1026 (true, None) => theme.code_text,
1027 (false, None) => theme.text,
1028 },
1029 background_color: custom.background,
1030 underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
1031 color: Some(theme.text_muted),
1032 thickness: px(1.0),
1033 wavy: false,
1034 }),
1035 strikethrough: strike.then_some(StrikethroughStyle {
1036 thickness: px(1.0),
1037 color: Some(theme.text_muted),
1038 }),
1039 });
1040 }
1041
1042 Flat {
1043 text: text.text.clone().into(),
1044 runs,
1045 links,
1046 code,
1047 chips,
1048 }
1049}
1050
1051fn text_element(
1052 text: &Text,
1053 size: f32,
1054 line_height: f32,
1055 weight: FontWeight,
1056 overlay: Overlay,
1057 theme: &Theme,
1058 cx: &App,
1059) -> AnyElement {
1060 let flat = flatten_with(text, weight, theme, |name| {
1061 crate::marks::paint_of(cx, name, theme)
1062 });
1063 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
1064}
1065
1066fn painted_text(
1072 flat: Flat,
1073 len: usize,
1074 size: f32,
1075 line_height: f32,
1076 overlay: Overlay,
1077 theme: &Theme,
1078) -> AnyElement {
1079 let (ix, part) = (overlay.block, overlay.part);
1080 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
1081 let span = 0..len;
1082 let hint = overlay
1085 .placeholder
1086 .filter(|_| len == 0 && overlay.caret().is_some())
1089 .map(|hint| {
1090 div()
1091 .absolute()
1092 .text_color(theme.text_faint)
1093 .child(hint.clone())
1094 });
1095 let styled = StyledText::new(flat.text).with_runs(flat.runs);
1096 let layout = styled.layout().clone();
1097
1098 let painted: AnyElement = if flat.links.is_empty() {
1099 styled.into_any_element()
1100 } else {
1101 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
1102 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
1103 .on_click(ranges, move |clicked, _window, cx| {
1104 if let Some(url) = urls.get(clicked) {
1105 cx.open_url(url);
1106 }
1107 })
1108 .into_any_element()
1109 };
1110
1111 let wash = theme.code_wash;
1115 let code_ranges = flat.code;
1116 let chip_wash = theme.element_hover;
1117 let chip_edge = theme.border;
1118 let chip_ranges = flat.chips;
1119 let caret_color = theme.caret;
1120 let selection_color = theme.selection;
1121 let annotated = overlay.annotated(len, theme);
1122 let layouts = overlay.layouts.cloned();
1123 let underlay = canvas(
1124 |_, _, _| (),
1125 move |_, _, window, _| {
1126 if let Some(layouts) = &layouts {
1127 layouts.record(ix, part, span.clone(), layout.clone());
1128 }
1129 for (range, wash) in &annotated {
1132 for rect in range_rects(&layout, range, 0.0, 0.0) {
1133 window.paint_quad(quad(
1134 rect,
1135 px(2.0),
1136 *wash,
1137 px(0.0),
1138 gpui::transparent_black(),
1139 BorderStyle::default(),
1140 ));
1141 }
1142 }
1143 if let Some(range) = &selected {
1147 for rect in range_rects(&layout, range, 0.0, 0.0) {
1148 window.paint_quad(quad(
1149 rect,
1150 px(2.0),
1151 selection_color,
1152 px(0.0),
1153 gpui::transparent_black(),
1154 BorderStyle::default(),
1155 ));
1156 }
1157 }
1158 if let Some(offset) = caret
1159 && let Some(head) = layout.position_for_index(offset)
1160 {
1161 window.paint_quad(quad(
1162 caret_quad(head, size, layout.line_height()),
1163 px(0.0),
1164 caret_color,
1165 px(0.0),
1166 gpui::transparent_black(),
1167 BorderStyle::default(),
1168 ));
1169 }
1170 for range in &code_ranges {
1171 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1172 window.paint_quad(quad(
1173 rect,
1174 px(INLINE_CODE_RADIUS),
1175 wash,
1176 px(0.0),
1177 gpui::transparent_black(),
1178 BorderStyle::default(),
1179 ));
1180 }
1181 }
1182 for range in &chip_ranges {
1185 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1186 window.paint_quad(quad(
1187 rect,
1188 px(Theme::control_radius()),
1189 chip_wash,
1190 px(1.0),
1191 chip_edge,
1192 BorderStyle::Solid,
1193 ));
1194 }
1195 }
1196 },
1197 )
1198 .absolute()
1199 .size_full();
1200
1201 div()
1202 .text_size(px(size))
1203 .line_height(px(line_height))
1204 .relative()
1205 .child(underlay)
1206 .children(hint)
1207 .child(painted)
1208 .into_any_element()
1209}
1210
1211fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1217 let inset = (line_height - px(size)) / 2.0;
1218 Bounds::new(
1219 head + point(px(0.0), inset),
1220 gpui::size(px(CARET_WIDTH), px(size)),
1221 )
1222}
1223
1224fn range_rects(
1226 layout: &gpui::TextLayout,
1227 range: &Range<usize>,
1228 pad_x: f32,
1229 inset_y: f32,
1230) -> Vec<Bounds<Pixels>> {
1231 let mut rects = Vec::new();
1232 let line_height = layout.line_height();
1233 let mut origin = layout.bounds().origin;
1234 let mut line_start = 0;
1235 for line in layout.line_layouts() {
1236 let shaped = &line.unwrapped_layout;
1237 let row_ends = line
1240 .wrap_boundaries()
1241 .iter()
1242 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
1243 .chain([line.len()]);
1244 let mut row_start = 0;
1245 for (row, row_end) in row_ends.enumerate() {
1246 let from = range
1247 .start
1248 .saturating_sub(line_start)
1249 .clamp(row_start, row_end);
1250 let to = range.end.saturating_sub(line_start).min(row_end);
1251 let row_x = shaped.x_for_index(row_start);
1252 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
1253 if from < to && right > left {
1254 rects.push(Bounds::new(
1255 origin
1256 + point(
1257 left - row_x - px(pad_x),
1258 line_height * row as f32 + px(inset_y),
1259 ),
1260 size(
1261 right - left + px(2.0 * pad_x),
1262 line_height - px(2.0 * inset_y),
1263 ),
1264 ));
1265 }
1266 row_start = row_end;
1267 }
1268 origin.y += line.size(line_height).height;
1269 line_start += line.len() + 1;
1271 }
1272 rects
1273}
1274
1275pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
1283 let Editing {
1284 selection,
1285 caret_on,
1286 layouts,
1287 annotations,
1288 typography,
1289 ..
1290 } = editing;
1291 let reset = layouts.map(|layouts| {
1295 let layouts = layouts.clone();
1296 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
1297 .absolute()
1298 .size(px(0.0))
1299 });
1300 let theme = Theme::of(cx).clone();
1301 let typography = typography.unwrap_or_else(|| Typography::of(cx));
1302 let overlay = Overlay {
1303 block: 0,
1304 part: Part::Code,
1305 selection,
1306 caret_on,
1307 layouts,
1308 annotations,
1309 placeholder: None,
1310 caption: Caption::default(),
1311 };
1312 let (underlay, lines) = code_lines(
1313 Some(crate::source::LANGUAGES[0]),
1314 code,
1315 overlay,
1316 &typography,
1317 &theme,
1318 cx,
1319 );
1320 let style = crate::SourceStyle::of(cx);
1322 let digits = lines.len().to_string().len().max(style.gutter_min_digits);
1323 let gap = style.gutter_gap.max(0.0) * typography.code.size();
1324 let gutter_width = digits as f32 * typography.code.size() + gap;
1325 let lines = lines
1326 .into_iter()
1327 .enumerate()
1328 .map(|(index, line)| {
1329 if !style.line_numbers {
1330 return line;
1331 }
1332 div()
1333 .flex()
1334 .items_start()
1335 .child(
1336 div()
1337 .w(px(gutter_width))
1338 .flex_shrink_0()
1339 .pr(px(gap))
1340 .font_family(theme.font_mono.clone())
1341 .text_color(style.gutter_color.unwrap_or(theme.text_faint))
1342 .text_right()
1343 .child((index + 1).to_string()),
1344 )
1345 .child(div().flex_1().min_w_0().child(line))
1346 .into_any_element()
1347 })
1348 .collect();
1349 div()
1350 .flex()
1351 .flex_col()
1352 .children(reset)
1353 .child(code_body(0, underlay, lines, &typography, true))
1354 .into_any_element()
1355}
1356
1357fn code_lines(
1360 language: Option<&str>,
1361 code: &str,
1362 overlay: Overlay,
1363 typography: &Typography,
1364 theme: &Theme,
1365 cx: &App,
1366) -> (AnyElement, Vec<AnyElement>) {
1367 let ix = overlay.block;
1368 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
1373 language
1374 .filter(|language| crate::source::is_markdown(language))
1375 .map(|_| crate::source::spans(code))
1376 });
1377 let mono = font(theme.font_mono.clone());
1378 let run = |len: usize, color: Hsla| TextRun {
1379 len,
1380 font: mono.clone(),
1381 color,
1382 background_color: None,
1383 underline: None,
1384 strikethrough: None,
1385 };
1386 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1391 let mut offset = 0usize;
1392 let lines: Vec<AnyElement> = code
1393 .split('\n')
1394 .map(|line| {
1395 let start = offset;
1396 offset += line.len() + 1;
1397 let mut runs = Vec::new();
1398 let mut pos = 0usize;
1401 if let Some(spans) = &spans {
1402 let end = start + line.len();
1403 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1404 let s = range.start.clamp(start, end) - start;
1405 let e = range.end.min(end) - start;
1406 if s > pos {
1407 runs.push(run(s - pos, theme.text));
1408 }
1409 runs.push(run(e - s, theme.syntax.color(*kind)));
1410 pos = e;
1411 }
1412 }
1413 if pos < line.len() {
1414 runs.push(run(line.len() - pos, theme.text));
1415 }
1416 if runs.is_empty() {
1417 runs.push(run(0, theme.text));
1418 }
1419 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1420 rows.push((start..start + line.len(), styled.layout().clone()));
1421 styled.into_any_element()
1422 })
1423 .collect();
1424
1425 let caret = overlay.caret_painted();
1426 let selected = overlay.selected(code.len());
1427 let sink = overlay.layouts.cloned();
1428 let code_size = typography.code.size();
1429 let annotated = overlay.annotated(code.len(), theme);
1430 let (caret_color, selection_color) = (theme.caret, theme.selection);
1431 let underlay = canvas(
1432 |_, _, _| (),
1433 move |_, _, window, _| {
1434 for (span, layout) in &rows {
1435 if let Some(sink) = &sink {
1436 sink.record(ix, Part::Code, span.clone(), layout.clone());
1437 }
1438 for (range, wash) in &annotated {
1439 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1440 if from < to {
1441 for rect in
1442 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1443 {
1444 window.paint_quad(quad(
1445 rect,
1446 px(2.0),
1447 *wash,
1448 px(0.0),
1449 gpui::transparent_black(),
1450 BorderStyle::default(),
1451 ));
1452 }
1453 }
1454 }
1455 if let Some(range) = &selected {
1456 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1457 if from < to {
1458 for rect in
1459 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1460 {
1461 window.paint_quad(quad(
1462 rect,
1463 px(2.0),
1464 selection_color,
1465 px(0.0),
1466 gpui::transparent_black(),
1467 BorderStyle::default(),
1468 ));
1469 }
1470 }
1471 }
1472 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1473 && let Some(head) = layout.position_for_index(offset - span.start)
1474 {
1475 window.paint_quad(quad(
1476 caret_quad(head, code_size, layout.line_height()),
1477 px(0.0),
1478 caret_color,
1479 px(0.0),
1480 gpui::transparent_black(),
1481 BorderStyle::default(),
1482 ));
1483 }
1484 }
1485 },
1486 )
1487 .absolute()
1488 .size_full();
1489
1490 (underlay.into_any_element(), lines)
1491}
1492
1493fn code_block(
1494 language: Option<&str>,
1495 code: &str,
1496 overlay: Overlay,
1497 typography: &Typography,
1498 theme: &Theme,
1499 window: &mut Window,
1500 cx: &mut App,
1501) -> AnyElement {
1502 let ix = overlay.block;
1503 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
1504 let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
1505
1506 div()
1507 .rounded(px(Theme::panel_radius()))
1508 .bg(theme.ink(0.035))
1509 .border_1()
1510 .border_color(theme.border)
1511 .overflow_hidden()
1512 .relative()
1513 .child(
1517 div()
1518 .relative()
1519 .flex()
1520 .flex_row()
1521 .items_center()
1522 .px(px(CODE_PADDING_X))
1523 .py(px(5.0))
1524 .border_b_1()
1525 .border_color(theme.border)
1526 .bg(theme.ink(0.02))
1527 .text_style(TextStyle::Subheadline)
1528 .text_color(match language {
1529 Some(_) => theme.text_muted,
1530 None => theme.text_faint,
1531 })
1532 .child(
1536 div()
1537 .relative()
1538 .children(overlay.layouts.map(|layouts| {
1539 let layouts = layouts.clone();
1540 canvas(
1541 move |bounds, _, _| layouts.record_language(ix, bounds),
1542 |_, _, _, _| (),
1543 )
1544 .absolute()
1545 .size_full()
1546 }))
1547 .child(SharedString::from(
1548 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1549 )),
1550 ),
1551 )
1552 .child(body)
1553 .child(copy_button(code, ix, theme, window, cx))
1554 .into_any_element()
1555}
1556
1557fn code_body(
1559 ix: usize,
1560 underlay: AnyElement,
1561 lines: Vec<AnyElement>,
1562 typography: &Typography,
1563 wrap: bool,
1564) -> AnyElement {
1565 let column = div()
1566 .flex()
1567 .flex_col()
1568 .px(px(CODE_PADDING_X))
1569 .children(lines);
1570 let body = div()
1571 .id(ElementId::named_usize("md-code", ix))
1572 .relative()
1573 .py(px(CODE_PADDING_Y))
1574 .text_size(px(typography.code.size()))
1575 .line_height(px(typography.code.line_height()))
1576 .child(underlay);
1577 if wrap {
1578 body.child(column.w_full()).into_any_element()
1581 } else {
1582 ui::scroll::Viewport::new(
1583 format!("md-code-scroll-{ix}"),
1584 body.flex()
1585 .flex_row()
1586 .whitespace_nowrap()
1587 .child(column.items_start()),
1594 gpui::Axis::Horizontal,
1595 )
1596 .into_any_element()
1597 }
1598}
1599
1600fn copy_button(
1607 code: &str,
1608 ix: usize,
1609 theme: &Theme,
1610 window: &mut Window,
1611 cx: &mut App,
1612) -> AnyElement {
1613 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1614 let showing = *copied.read(cx);
1615 let text: SharedString = code.to_string().into();
1616
1617 div()
1618 .id(ElementId::named_usize("md-copy", ix))
1619 .absolute()
1620 .top(px(3.0))
1621 .right(px(5.0))
1622 .h(px(20.0))
1623 .px(px(6.0))
1624 .rounded(px(5.0))
1625 .flex()
1626 .items_center()
1627 .cursor_pointer()
1628 .text_style(TextStyle::Caption)
1629 .text_color(theme.text_muted)
1630 .hover(|el| el.bg(theme.element_hover))
1631 .child(if showing { "Copied" } else { "Copy" })
1632 .on_click({
1633 let copied = copied.clone();
1634 move |_, _, cx| {
1635 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1636 copied.update(cx, |state, cx| {
1637 *state = true;
1638 cx.notify();
1639 });
1640 }
1641 })
1642 .on_hover(move |hovering, _, cx| {
1643 if !*hovering && *copied.read(cx) {
1644 copied.update(cx, |state, cx| {
1645 *state = false;
1646 cx.notify();
1647 });
1648 }
1649 })
1650 .into_any_element()
1651}
1652
1653fn image(
1660 url: &str,
1661 alt: &Text,
1662 width: Option<u32>,
1663 overlay: Overlay,
1664 typography: &Typography,
1665 theme: &Theme,
1666 cx: &App,
1667) -> AnyElement {
1668 let hint = SharedString::new_static(CAPTION_HINT);
1669 let overlay = Overlay {
1670 placeholder: Some(&hint),
1671 ..overlay.at(Part::Caption)
1672 };
1673 let picture = if url.is_empty() {
1674 div()
1675 .h(px(IMAGE_EMPTY_HEIGHT))
1676 .flex()
1677 .items_center()
1678 .px(px(CARD_PADDING))
1679 .rounded(px(Theme::button_radius()))
1680 .border_1()
1681 .border_dashed()
1682 .border_color(theme.border)
1683 .text_size(px(typography.body.size()))
1684 .text_color(theme.text_muted)
1685 .child(IMAGE_EMPTY)
1686 } else {
1687 let picture = match url.contains("://") {
1691 true => img(SharedString::from(url.to_string())),
1692 false => img(std::path::PathBuf::from(url)),
1693 };
1694 let box_ = div()
1695 .relative()
1696 .rounded(px(Theme::button_radius()))
1697 .overflow_hidden()
1698 .border_1()
1699 .border_color(theme.border)
1700 .children(overlay.layouts.map(|layouts| {
1701 let layouts = layouts.clone();
1702 let ix = overlay.block;
1703 canvas(
1704 move |bounds, _, _| layouts.record_picture(ix, bounds),
1705 |_, _, _, _| (),
1706 )
1707 .absolute()
1708 .size_full()
1709 }));
1710 match width {
1711 Some(width) => box_
1716 .self_start()
1717 .max_w_full()
1718 .w(px(width as f32))
1719 .child(picture.w(px(width as f32)).max_w_full()),
1720 None => box_.child(picture.max_w_full()),
1723 }
1724 };
1725 div()
1726 .flex()
1727 .flex_col()
1728 .gap(px(CAPTION_GAP))
1729 .child(picture)
1730 .when(
1733 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1734 |el| {
1735 el.child(text_element(
1736 alt,
1737 typography.caption.size(),
1738 typography.caption.line_height(),
1739 FontWeight::NORMAL,
1740 overlay,
1741 theme,
1742 cx,
1743 ))
1744 },
1745 )
1746 .into_any_element()
1747}
1748
1749fn bookmark(
1761 ix: usize,
1762 url: &str,
1763 form: Form,
1764 typography: &Typography,
1765 theme: &Theme,
1766 cx: &App,
1767) -> AnyElement {
1768 let preview = preview::of(cx, url).unwrap_or_default();
1769 let host = SharedString::from(preview::host(url).to_string());
1770 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1771 let title = preview
1772 .title
1773 .clone()
1774 .unwrap_or_else(|| SharedString::from(url.to_string()));
1775
1776 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1779 let site = host.clone();
1780 let mark = move |size: f32| {
1781 let host = site.clone();
1782 match icon.clone() {
1783 Some(icon) => img(icon)
1784 .size(px(size))
1785 .rounded(px(size / 4.0))
1786 .with_fallback(move || initial(&host, size, muted, wash))
1787 .into_any_element(),
1788 None => initial(&host, size, muted, wash),
1789 }
1790 };
1791
1792 if form == Form::Chip {
1793 let open = url.to_string();
1794 let pill = div()
1795 .id(ElementId::named_usize("md-chip", ix))
1796 .flex()
1797 .flex_row()
1798 .items_center()
1799 .gap(px(6.0))
1800 .px(px(CHIP_BLOCK_PAD_X))
1801 .py(px(CHIP_BLOCK_PAD_Y))
1802 .rounded(px(Theme::control_radius()))
1803 .border_1()
1804 .border_color(theme.border)
1805 .bg(theme.element_hover)
1806 .text_size(px(typography.body.size()))
1807 .line_height(px(typography.body.line_height()))
1808 .text_color(theme.text)
1809 .cursor(CursorStyle::PointingHand)
1810 .hover(|el| el.bg(theme.element_active))
1811 .on_click(move |_, _, cx| cx.open_url(&open))
1812 .child(mark(CHIP_ICON))
1813 .child(
1816 div()
1817 .min_w_0()
1818 .truncate()
1819 .child(preview.title.unwrap_or(label)),
1820 );
1821 return div().flex().flex_row().child(pill).into_any_element();
1824 }
1825
1826 let words = div()
1827 .flex()
1828 .flex_col()
1829 .min_w_0()
1830 .px(px(CARD_PADDING))
1831 .py(px(CARD_PADDING - 2.0))
1832 .child(
1833 div()
1834 .truncate()
1835 .text_size(px(typography.body.size()))
1836 .line_height(px(typography.body.line_height()))
1837 .text_color(theme.text)
1838 .child(title),
1839 )
1840 .children(preview.description.map(|blurb| {
1841 div()
1842 .line_clamp(2)
1843 .text_size(px(typography.card.size()))
1844 .line_height(px(typography.card.line_height()))
1845 .text_color(theme.text_muted)
1846 .child(blurb)
1847 }))
1848 .child(
1849 div()
1850 .mt_auto()
1851 .pt(px(6.0))
1852 .flex()
1853 .items_center()
1854 .gap(px(6.0))
1855 .text_size(px(typography.card.size()))
1856 .text_color(theme.text_muted)
1857 .child(mark(CARD_ICON))
1858 .child(div().truncate().child(label)),
1859 );
1860
1861 let picture = corners(div(), form)
1862 .bg(theme.surface)
1863 .flex()
1864 .items_center()
1865 .justify_center()
1866 .overflow_hidden()
1867 .child(match preview.image {
1868 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
1869 .with_fallback(move || mark(CARD_COVER))
1870 .into_any_element(),
1871 None => mark(CARD_COVER),
1872 });
1873
1874 let open = url.to_string();
1875 let card = div()
1876 .id(ElementId::named_usize("md-bookmark", ix))
1877 .flex()
1878 .w_full()
1879 .overflow_hidden()
1880 .rounded(px(Theme::button_radius()))
1881 .border(px(CARD_BORDER))
1882 .border_color(theme.border)
1883 .bg(theme.surface_card)
1884 .cursor(CursorStyle::PointingHand)
1885 .hover(|el| el.bg(theme.element_hover))
1886 .on_click(move |_, _, cx| cx.open_url(&open));
1887
1888 if form == Form::Embed {
1889 card.flex_col()
1890 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1891 .child(words.w_full())
1892 } else {
1893 card.h(px(CARD_HEIGHT))
1894 .child(words.flex_1())
1895 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1896 }
1897 .into_any_element()
1898}
1899
1900fn corners<T: Styled>(element: T, form: Form) -> T {
1904 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
1905 match form {
1906 Form::Embed => element.rounded_t(corner),
1907 _ => element.rounded_r(corner),
1908 }
1909}
1910
1911fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1914 div()
1915 .flex_none()
1916 .size(px(size))
1917 .rounded(px(size / 4.0))
1918 .bg(wash)
1919 .flex()
1920 .items_center()
1921 .justify_center()
1922 .text_size(px(size * 0.55))
1923 .text_color(color)
1924 .child(SharedString::from(
1925 host.chars()
1926 .next()
1927 .unwrap_or('?')
1928 .to_uppercase()
1929 .to_string(),
1930 ))
1931 .into_any_element()
1932}
1933
1934#[expect(
1941 clippy::too_many_arguments,
1942 reason = "a table, its overlay, and what paints them"
1943)]
1944fn table(
1945 align: &[Align],
1946 header: &[Text],
1947 rows: &[Vec<Text>],
1948 overlay: Overlay,
1949 typography: &Typography,
1950 theme: &Theme,
1951 window: &mut Window,
1952 cx: &App,
1953) -> AnyElement {
1954 let ix = overlay.block;
1955 let all: Vec<&[Text]> = std::iter::once(header)
1956 .filter(|row| !row.is_empty())
1957 .chain(rows.iter().map(|row| row.as_slice()))
1958 .collect();
1959 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1960 if columns == 0 {
1961 return gpui::Empty.into_any_element();
1962 }
1963 let has_header = !header.is_empty();
1964
1965 let text_system = window.text_system();
1966 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1967 let mut content = vec![0.0f32; columns];
1968 for (r, row) in all.iter().enumerate() {
1969 let weight = if has_header && r == 0 {
1970 FontWeight::BOLD
1971 } else {
1972 FontWeight::NORMAL
1973 };
1974 let mut out = Vec::with_capacity(columns);
1975 for (c, natural) in content.iter_mut().enumerate() {
1976 let Some(cell) = row.get(c) else {
1977 out.push(None);
1978 continue;
1979 };
1980 let flat = flatten_with(cell, weight, theme, |name| {
1981 crate::marks::paint_of(cx, name, theme)
1982 });
1983 if !flat.text.is_empty() {
1984 let width = f32::from(
1985 text_system
1986 .shape_line(
1987 flat.text.clone(),
1988 px(typography.body.size()),
1989 &flat.runs,
1990 None,
1991 )
1992 .width(),
1993 );
1994 *natural = natural.max(width);
1995 }
1996 out.push(Some(flat));
1997 }
1998 flats.push(out);
1999 }
2000
2001 let naturals: Vec<f32> = content
2002 .iter()
2003 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
2004 .collect();
2005 let minimums: Vec<f32> = naturals
2006 .iter()
2007 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
2008 .collect();
2009 let hairline = theme.hairline(0.10);
2010
2011 let mut inner = div()
2012 .flex()
2013 .flex_col()
2014 .w_full()
2015 .min_w(px(minimums.iter().sum::<f32>()));
2016 for (r, row) in flats.into_iter().enumerate() {
2017 if r > 0 {
2018 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
2019 }
2020 let mut row_el = div().flex().flex_row();
2021 for (c, cell) in row.into_iter().enumerate() {
2022 let mut cell_el = div()
2023 .flex_grow(naturals[c])
2024 .flex_shrink(naturals[c])
2025 .flex_basis(px(0.0))
2026 .min_w(px(minimums[c]))
2027 .p(px(TABLE_CELL_PADDING))
2028 .text_size(px(typography.body.size()))
2029 .line_height(px(typography.body.line_height()));
2030 cell_el = match align.get(c).copied().unwrap_or_default() {
2031 Align::Left => cell_el,
2032 Align::Center => cell_el.text_center(),
2033 Align::Right => cell_el.text_right(),
2034 };
2035 if let Some(flat) = cell {
2036 let row = if has_header { r } else { r + 1 };
2040 let len = flat.text.len();
2041 cell_el = cell_el.child(painted_text(
2042 flat,
2043 len,
2044 typography.body.size(),
2045 typography.body.line_height(),
2046 overlay.at(Part::Cell { row, column: c }),
2047 theme,
2048 ));
2049 }
2050 row_el = row_el.child(cell_el);
2051 }
2052 inner = inner.child(row_el);
2053 }
2054
2055 ui::scroll::Viewport::new(
2056 format!("md-table-scroll-{ix}"),
2057 div()
2058 .id(ElementId::named_usize("md-table", ix))
2059 .w_full()
2060 .child(inner),
2061 gpui::Axis::Horizontal,
2062 )
2063 .into_any_element()
2064}