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