1use std::{cell::RefCell, ops::Range, path::Path, rc::Rc};
11
12use gpui::{
13 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
14 ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
15 StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
16 canvas, div, font, 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)]
124#[non_exhaustive]
125pub enum Annotation {
126 #[default]
128 Open,
129 Resolved,
131 Active,
133 Highlight(crate::HighlightColor),
136}
137
138impl Annotation {
139 fn wash(self, theme: &Theme, highlight: crate::HighlightPaint) -> Hsla {
140 match self {
141 Self::Open => theme.warning.opacity(0.20),
142 Self::Resolved => theme.warning.opacity(0.08),
143 Self::Active => theme.warning.opacity(0.38),
144 Self::Highlight(color) => highlight(color, theme),
145 }
146 }
147}
148
149pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
154
155#[derive(Clone)]
161pub enum Toggle {
162 Handled(OnToggle),
165 HitTested,
171}
172
173#[derive(Clone)]
179pub struct Editing<'a> {
180 pub selection: Option<Selection>,
183 pub caret_on: bool,
186 pub layouts: Option<&'a BlockLayouts>,
188 pub annotations: &'a [(Selection, Annotation)],
190 pub placeholder: Option<SharedString>,
192 pub caption: Caption,
193 pub typography: Option<Typography>,
197 pub toggle: Option<Toggle>,
200 pub copy: CopyButton,
202 pub base: Option<&'a Path>,
205}
206
207impl Default for Editing<'_> {
208 fn default() -> Self {
209 Self {
210 selection: None,
211 caret_on: true,
214 layouts: None,
215 annotations: &[],
216 placeholder: None,
217 caption: Caption::default(),
218 typography: None,
219 toggle: None,
220 copy: CopyButton::default(),
221 base: None,
222 }
223 }
224}
225
226#[derive(Clone, Default)]
233pub struct BlockLayouts(Rc<RefCell<Frames>>);
234
235#[derive(Default)]
236struct Frames {
237 texts: Vec<Painted>,
238 rows: Vec<PaintedRow>,
239 blocks: Vec<(usize, Bounds<Pixels>)>,
242 languages: Vec<(usize, Bounds<Pixels>)>,
245 pictures: Vec<(usize, Bounds<Pixels>)>,
249 checkboxes: Vec<(usize, Bounds<Pixels>)>,
252}
253
254struct Painted {
261 block: usize,
262 part: Part,
263 range: Range<usize>,
264 layout: TextLayout,
265}
266
267struct PaintedRow {
268 painted: usize,
269 block: usize,
270 part: Part,
271 range: Range<usize>,
272 bounds: Bounds<Pixels>,
273 line_start: usize,
274 wrapped_row: usize,
275}
276
277impl BlockLayouts {
278 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
284 let frames = self.0.borrow();
285 if let Some(row) = frames.rows.iter().find(|row| row.bounds.contains(&point)) {
286 return cursor_in_row(&frames, row, point.x);
287 }
288 frames
289 .rows
290 .iter()
291 .min_by_key(|row| {
292 let bounds = row.bounds;
293 let above = (bounds.origin.y - point.y).abs();
294 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
295 f32::from(above.min(below)) as i64
296 })
297 .and_then(|row| cursor_in_row(&frames, row, point.x))
298 }
299
300 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
306 let frames = self.0.borrow();
307 let painted = frames.texts.iter().find(|painted| {
308 painted.block == at.block
309 && painted.part == at.part
310 && painted.range.start <= at.offset
311 && at.offset <= painted.range.end
312 })?;
313 let point = painted
314 .layout
315 .position_for_index(at.offset - painted.range.start)?;
316 Some((point, painted.layout.line_height()))
317 }
318
319 pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
327 let (start, end) = selection.ordered();
328 self.0
329 .borrow()
330 .texts
331 .iter()
332 .filter_map(|painted| {
333 let here = Cursor::new(painted.block, painted.part, 0);
334 let (from, to) = (
335 Cursor::new(start.block, start.part, 0),
336 Cursor::new(end.block, end.part, 0),
337 );
338 if here < from || here > to {
339 return None;
340 }
341 let len = painted.range.len();
344 let first = if here == from { start.offset } else { 0 };
345 let last = if here == to { end.offset } else { usize::MAX };
346 let range = first.saturating_sub(painted.range.start).min(len)
347 ..last.saturating_sub(painted.range.start).min(len);
348 (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
349 })
350 .flatten()
351 .collect()
352 }
353
354 pub fn step_row(
365 &self,
366 at: Cursor,
367 from: Point<Pixels>,
368 down: bool,
369 ) -> Option<(Cursor, Pixels)> {
370 let frames = self.0.borrow();
371 let ix = frames
372 .rows
373 .iter()
374 .position(|row| {
375 row.block == at.block
376 && row.part == at.part
377 && row_contains(row, at.offset)
378 && row.bounds.origin.y <= from.y
379 && from.y < row.bounds.origin.y + row.bounds.size.height
380 })
381 .or_else(|| {
382 frames.rows.iter().position(|row| {
383 row.block == at.block && row.part == at.part && row_contains(row, at.offset)
384 })
385 })?;
386 let next = match down {
387 true => frames.rows.get(ix + 1)?,
388 false => frames.rows.get(ix.checked_sub(1)?)?,
389 };
390 Some((cursor_in_row(&frames, next, from.x)?, next.bounds.origin.y))
391 }
392
393 pub fn over_text(&self, point: Point<Pixels>) -> bool {
400 self.0
401 .borrow()
402 .texts
403 .iter()
404 .any(|painted| painted.layout.bounds().contains(&point))
405 }
406
407 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
409 let blocks = &self.0.borrow().blocks;
410 blocks
411 .iter()
412 .find(|(_, bounds)| bounds.contains(&point))
413 .or_else(|| {
414 blocks.iter().min_by_key(|(_, bounds)| {
415 let above = (bounds.origin.y - point.y).abs();
416 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
417 f32::from(above.min(below)) as i64
418 })
419 })
420 .map(|(ix, _)| *ix)
421 }
422
423 pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
432 let texts = &self.0.borrow().texts;
433 let painted = texts.iter().find(|painted| painted.block == ix)?;
434 Some((
435 painted.layout.bounds().origin.y,
436 painted.layout.line_height(),
437 ))
438 }
439
440 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
442 self.0
443 .borrow()
444 .blocks
445 .iter()
446 .find(|(block, _)| *block == ix)
447 .map(|(_, bounds)| *bounds)
448 }
449
450 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
455 self.0
456 .borrow()
457 .languages
458 .iter()
459 .find(|(block, _)| *block == ix)
460 .map(|(_, bounds)| *bounds)
461 }
462
463 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
468 self.0
469 .borrow()
470 .pictures
471 .iter()
472 .find(|(block, _)| *block == ix)
473 .map(|(_, bounds)| *bounds)
474 }
475
476 pub fn checkbox_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
480 self.0
481 .borrow()
482 .checkboxes
483 .iter()
484 .find(|(block, _)| *block == ix)
485 .map(|(_, bounds)| *bounds)
486 }
487
488 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
489 let mut frames = self.0.borrow_mut();
490 let painted = frames.texts.len();
491 record_rows(&mut frames.rows, painted, block, part, &range, &layout);
492 frames.texts.push(Painted {
493 block,
494 part,
495 range,
496 layout,
497 });
498 }
499
500 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
501 self.0.borrow_mut().blocks.push((ix, bounds));
502 }
503
504 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
505 self.0.borrow_mut().languages.push((ix, bounds));
506 }
507
508 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
509 self.0.borrow_mut().pictures.push((ix, bounds));
510 }
511
512 fn record_checkbox(&self, ix: usize, bounds: Bounds<Pixels>) {
513 self.0.borrow_mut().checkboxes.push((ix, bounds));
514 }
515
516 fn clear(&self) {
517 let mut frames = self.0.borrow_mut();
518 frames.texts.clear();
519 frames.rows.clear();
520 frames.blocks.clear();
521 frames.languages.clear();
522 frames.pictures.clear();
523 frames.checkboxes.clear();
524 }
525}
526
527fn row_contains(row: &PaintedRow, offset: usize) -> bool {
528 row.range.start <= offset && offset <= row.range.end
529}
530
531fn cursor_in_row(frames: &Frames, row: &PaintedRow, x: Pixels) -> Option<Cursor> {
532 let painted = &frames.texts[row.painted];
533 let line = painted
534 .layout
535 .line_layout_for_index(row.line_start - painted.range.start)?;
536 let height = row.bounds.size.height;
537 let local = point(
538 x - row.bounds.origin.x,
539 height * (row.wrapped_row as f32 + 0.5),
540 );
541 let (Ok(offset) | Err(offset)) = line.closest_index_for_position(local, height);
542 Some(Cursor::new(
543 row.block,
544 row.part,
545 (row.line_start + offset).min(row.range.end),
546 ))
547}
548
549fn record_rows(
550 rows: &mut Vec<PaintedRow>,
551 painted: usize,
552 block: usize,
553 part: Part,
554 range: &Range<usize>,
555 layout: &TextLayout,
556) {
557 let line_height = layout.line_height();
558 let bounds = layout.bounds();
559 let mut origin = bounds.origin;
560 let mut line_start = range.start;
561 for line in layout.line_layouts() {
562 let shaped = &line.unwrapped_layout;
563 let row_ends = line
564 .wrap_boundaries()
565 .iter()
566 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
567 .chain([line.len()]);
568 let mut row_start = 0;
569 for (row, row_end) in row_ends.enumerate() {
570 rows.push(PaintedRow {
571 painted,
572 block,
573 part,
574 range: line_start + row_start..line_start + row_end,
575 bounds: Bounds::new(
576 origin + point(px(0.0), line_height * row as f32),
577 size(bounds.size.width, line_height),
578 ),
579 line_start,
580 wrapped_row: row,
581 });
582 row_start = row_end;
583 }
584 origin.y += line.size(line_height).height;
585 line_start += line.len() + 1;
586 }
587}
588
589#[derive(Clone, Copy)]
595struct Overlay<'a> {
596 block: usize,
597 part: Part,
598 selection: Option<Selection>,
599 caret_on: bool,
600 layouts: Option<&'a BlockLayouts>,
601 annotations: &'a [(Selection, Annotation)],
603 placeholder: Option<&'a SharedString>,
606 caption: Caption,
607 toggle: Option<&'a Toggle>,
610 copy: CopyButton,
611 base: Option<&'a Path>,
612 highlight: crate::HighlightPaint,
613}
614
615impl<'a> Overlay<'a> {
616 fn at(self, part: Part) -> Self {
617 Self { part, ..self }
618 }
619
620 fn here(&self) -> Cursor {
621 Cursor::new(self.block, self.part, 0)
622 }
623
624 fn caret_painted(&self) -> Option<usize> {
630 self.caret_on.then(|| self.caret()).flatten()
631 }
632
633 fn caret(&self) -> Option<usize> {
635 self.selection
636 .map(|selection| selection.head)
637 .filter(|head| head.block == self.block && head.part == self.part)
638 .map(|head| head.offset)
639 }
640
641 fn selected(&self, len: usize) -> Option<Range<usize>> {
643 self.clip(self.selection?, len)
644 }
645
646 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
649 self.annotations
650 .iter()
651 .filter_map(|(range, kind)| {
652 Some((self.clip(*range, len)?, kind.wash(theme, self.highlight)))
653 })
654 .collect()
655 }
656
657 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
663 if selection.is_collapsed() {
664 return None;
665 }
666 let (start, end) = selection.ordered();
667 let here = self.here();
668 let (first, last) = (
669 Cursor::new(start.block, start.part, 0),
670 Cursor::new(end.block, end.part, 0),
671 );
672 if here < first || here > last {
673 return None;
674 }
675 let from = if here == first { start.offset } else { 0 };
676 let to = if here == last { end.offset } else { len };
677 (from < to).then_some(from..to.min(len))
678 }
679
680 fn covers_block(&self) -> bool {
684 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
685 return false;
686 };
687 let (start, end) = selection.ordered();
688 start.block < self.block && self.block < end.block
689 }
690}
691
692pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
698 if url.contains("://") {
699 return SharedString::from(url.to_string()).into();
700 }
701 match base {
704 Some(base) => base.join(url).into(),
705 None => std::path::PathBuf::from(url).into(),
706 }
707}
708
709pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
711 let doc = crate::parse_with(source, &crate::Marks::of(cx));
712 render(&doc, Caption::default(), window, cx)
713}
714
715pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
717 render_with(
718 doc,
719 Editing {
720 caption,
721 ..Editing::default()
722 },
723 window,
724 cx,
725 )
726}
727
728pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
736 let Editing {
737 selection,
738 caret_on,
739 layouts,
740 annotations,
741 placeholder,
742 caption,
743 typography,
744 toggle,
745 copy,
746 base,
747 } = editing;
748 let reset = layouts.map(|layouts| {
754 let layouts = layouts.clone();
755 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
756 .absolute()
757 .size(px(0.0))
758 });
759 let theme = Theme::of(cx).clone();
762 let typography = typography.unwrap_or_else(|| Typography::of(cx));
763 let highlight = crate::marks::highlight_paint_of(cx);
764 let mut column = div().flex().flex_col().children(reset);
765
766 for (ix, block) in doc.blocks.iter().enumerate() {
767 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
768 None => 0.0,
769 Some(previous) if tight(previous, block) => LIST_GAP,
770 Some(_) => BLOCK_GAP,
771 };
772 let overlay = Overlay {
773 block: ix,
774 part: Part::Body,
775 selection,
776 caret_on,
777 layouts,
778 annotations,
779 placeholder: placeholder.as_ref(),
780 caption,
781 toggle: toggle.as_ref(),
782 copy,
783 base,
784 highlight,
785 };
786 let frame = layouts.map(|layouts| {
789 let layouts = layouts.clone();
790 canvas(
791 move |bounds, _, _| layouts.record_block(ix, bounds),
792 |_, _, _, _| (),
793 )
794 .absolute()
795 .size_full()
796 });
797 column = column.child(
798 div()
804 .mt(px(gap))
805 .pl(px(block.indent as f32 * INDENT_WIDTH))
806 .child(
807 div()
808 .w_full()
809 .relative()
810 .children(frame)
811 .when(overlay.covers_block() && block.opaque(), |el| {
816 el.rounded(px(4.0)).bg(theme.selection)
817 })
818 .child(block_element(
819 block,
820 overlay,
821 &typography,
822 &theme,
823 window,
824 cx,
825 )),
826 ),
827 );
828 }
829
830 column.into_any_element()
831}
832
833fn tight(previous: &Block, next: &Block) -> bool {
835 let marker = |block: &Block| {
836 matches!(
837 block.kind,
838 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
839 )
840 };
841 marker(previous) && (marker(next) || next.indent > previous.indent)
842}
843
844fn block_element(
845 block: &Block,
846 overlay: Overlay,
847 typography: &Typography,
848 theme: &Theme,
849 window: &mut Window,
850 cx: &mut App,
851) -> AnyElement {
852 let body = overlay.at(Part::Body);
853 match &block.kind {
854 BlockKind::Paragraph(text) => text_element(
855 text,
856 typography.body.size(),
857 typography.body.line_height(),
858 FontWeight::NORMAL,
859 body,
860 theme,
861 cx,
862 ),
863 BlockKind::Heading { level, text } => {
864 let heading = typography.heading(*level);
865 text_element(
866 text,
867 heading.size(),
868 heading.line_height(),
869 heading.weight,
870 body,
871 theme,
872 cx,
873 )
874 }
875 BlockKind::Bullet(text) => {
876 marker_row(disc(typography, theme), text, body, typography, theme, cx)
877 }
878 BlockKind::Ordered { number, text } => marker_row(
879 div()
880 .flex_none()
881 .w(px(MARKER_WIDTH))
882 .text_size(px(typography.body.size()))
883 .line_height(px(typography.body.line_height()))
884 .text_color(theme.text_muted)
885 .child(SharedString::from(format!("{number}.")))
886 .into_any_element(),
887 text,
888 body,
889 typography,
890 theme,
891 cx,
892 ),
893 BlockKind::Task { checked, text } => marker_row(
894 checkbox(*checked, overlay, typography, theme),
895 text,
896 body,
897 typography,
898 theme,
899 cx,
900 ),
901 BlockKind::Quote { kind, text } => div()
902 .border_l_2()
903 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
904 .pl(px(12.0))
905 .pr(px(10.0))
906 .py(px(2.0))
907 .text_color(theme.text_muted)
908 .children(kind.map(|kind| {
909 div()
910 .pb(px(2.0))
911 .text_size(px(typography.body.size()))
912 .line_height(px(typography.body.line_height()))
913 .font_weight(FontWeight::SEMIBOLD)
914 .text_color(alert_color(kind, theme))
915 .child(kind.label())
916 }))
917 .child(text_element(
918 text,
919 typography.body.size(),
920 typography.body.line_height(),
921 FontWeight::NORMAL,
922 body,
923 theme,
924 cx,
925 ))
926 .into_any_element(),
927 BlockKind::Code { language, code } => {
928 let overlay = overlay.at(Part::Code);
929 let painted = overlay
933 .caret()
934 .is_none()
935 .then(|| block::render(language.as_deref(), &code.text, window, cx))
936 .flatten();
937 match painted {
938 Some(element) => div()
941 .when(overlay.covers_block(), |el| {
942 el.rounded(px(4.0)).bg(theme.selection)
943 })
944 .child(element)
945 .into_any_element(),
946 None => code_block(
947 language.as_deref(),
948 &code.text,
949 overlay,
950 typography,
951 theme,
952 window,
953 cx,
954 ),
955 }
956 }
957 BlockKind::Image { url, alt, width } => {
958 image(url, alt, *width, overlay, typography, theme, cx)
959 }
960 BlockKind::Bookmark { url, form } => {
961 bookmark(overlay.block, url, *form, typography, theme, cx)
962 }
963 BlockKind::Table {
964 align,
965 header,
966 rows,
967 } => table(align, header, rows, overlay, typography, theme, window, cx),
968 BlockKind::Rule => div()
969 .h(px(1.0))
970 .w_full()
971 .bg(theme.border)
972 .into_any_element(),
973 }
974}
975
976fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
978 div()
979 .flex_none()
980 .w(px(MARKER_WIDTH))
981 .h(px(typography.body.line_height()))
982 .flex()
983 .items_center()
984 .child(
985 div()
986 .ml(px(1.0))
987 .w(px(5.0))
988 .h(px(5.0))
989 .rounded_full()
990 .bg(theme.text_faint),
991 )
992 .into_any_element()
993}
994
995fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
996 let ix = overlay.block;
997 let mut box_ = div()
998 .relative()
999 .w(px(13.0))
1000 .h(px(13.0))
1001 .rounded(px(3.5))
1002 .border_1()
1003 .flex()
1004 .items_center()
1005 .justify_center();
1006 box_ = if checked {
1007 box_.bg(theme.solid)
1008 .border_color(theme.solid)
1009 .text_style(TextStyle::Caption)
1010 .text_color(theme.on_solid)
1011 .child("✓")
1012 } else {
1013 box_.border_color(theme.border_strong)
1014 };
1015 box_ = box_.children(overlay.layouts.map(|layouts| {
1018 let layouts = layouts.clone();
1019 canvas(
1020 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1021 |_, _, _, _| (),
1022 )
1023 .absolute()
1024 .size_full()
1025 }));
1026 if overlay.toggle.is_some() {
1029 box_ = box_.cursor_pointer();
1030 }
1031 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1032 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1033 cx.stop_propagation();
1037 toggle(ix, window, cx);
1038 });
1039 }
1040
1041 div()
1042 .flex_none()
1043 .w(px(MARKER_WIDTH))
1044 .h(px(typography.body.line_height()))
1045 .flex()
1046 .items_center()
1047 .child(box_)
1048 .into_any_element()
1049}
1050
1051fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1053 match kind {
1054 QuoteKind::Note => theme.accent,
1055 QuoteKind::Tip => theme.success,
1056 QuoteKind::Important => theme.busy,
1057 QuoteKind::Warning => theme.warning,
1058 QuoteKind::Caution => theme.danger,
1059 }
1060}
1061
1062fn marker_row(
1063 marker: AnyElement,
1064 text: &Text,
1065 overlay: Overlay,
1066 typography: &Typography,
1067 theme: &Theme,
1068 cx: &App,
1069) -> AnyElement {
1070 div()
1071 .flex()
1072 .flex_row()
1073 .gap(px(MARKER_GAP))
1074 .child(marker)
1075 .child(div().flex_1().min_w_0().child(text_element(
1076 text,
1077 typography.body.size(),
1078 typography.body.line_height(),
1079 FontWeight::NORMAL,
1080 overlay,
1081 theme,
1082 cx,
1083 )))
1084 .into_any_element()
1085}
1086
1087pub struct Flat {
1090 pub text: SharedString,
1091 pub runs: Vec<TextRun>,
1092 pub links: Vec<(Range<usize>, String)>,
1093 pub code: Vec<Range<usize>>,
1094 pub chips: Vec<Range<usize>>,
1095}
1096
1097pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
1100 flatten_with(text, base_weight, theme, |_| None)
1101}
1102
1103pub fn flatten_with(
1106 text: &Text,
1107 base_weight: FontWeight,
1108 theme: &Theme,
1109 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
1110) -> Flat {
1111 let mut cuts: Vec<usize> = text
1112 .marks
1113 .iter()
1114 .flat_map(|span| [span.range.start, span.range.end])
1115 .chain([0, text.text.len()])
1116 .filter(|cut| *cut <= text.text.len())
1117 .collect();
1118 cuts.sort_unstable();
1119 cuts.dedup();
1120
1121 let mut runs = Vec::new();
1122 let mut links: Vec<(Range<usize>, String)> = Vec::new();
1123 let mut code: Vec<Range<usize>> = Vec::new();
1124 let mut chips: Vec<Range<usize>> = Vec::new();
1125
1126 for pair in cuts.windows(2) {
1127 let (start, end) = (pair[0], pair[1]);
1128 let covering = text
1129 .marks
1130 .iter()
1131 .filter(|span| span.range.start <= start && span.range.end >= end);
1132
1133 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
1134 let mut chip = false;
1135 let mut link = None;
1136 let mut custom = crate::MarkPaint::default();
1139 for span in covering {
1140 match &span.mark {
1141 Mark::Bold => bold = true,
1142 Mark::Italic => italic = true,
1143 Mark::Strike => strike = true,
1144 Mark::Code => mono = true,
1145 Mark::Mention { url, .. } => {
1146 chip = true;
1147 link = Some(url.clone());
1148 }
1149 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
1150 Mark::Custom(name) => {
1151 let Some(painted) = paint(name) else { continue };
1152 custom.color = painted.color.or(custom.color);
1153 custom.background = painted.background.or(custom.background);
1154 custom.weight = painted.weight.or(custom.weight);
1155 custom.italic |= painted.italic;
1156 custom.underline |= painted.underline;
1157 custom.strikethrough |= painted.strikethrough;
1158 }
1159 }
1160 }
1161 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
1162
1163 if mono {
1164 match code.last_mut() {
1165 Some(range) if range.end == start => range.end = end,
1166 _ => code.push(start..end),
1167 }
1168 }
1169 if chip {
1170 match chips.last_mut() {
1171 Some(range) if range.end == start => range.end = end,
1172 _ => chips.push(start..end),
1173 }
1174 }
1175 if let Some(url) = &link {
1176 match links.last_mut() {
1177 Some((range, last)) if range.end == start && last == url => range.end = end,
1178 _ => links.push((start..end, url.clone())),
1179 }
1180 }
1181
1182 let mut face = font(if mono {
1183 theme.font_mono.clone()
1184 } else {
1185 theme.font_body.clone()
1186 });
1187 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
1188 FontWeight::SEMIBOLD
1189 } else {
1190 custom.weight.unwrap_or(base_weight)
1191 };
1192 face.style = if italic {
1193 FontStyle::Italic
1194 } else {
1195 FontStyle::Normal
1196 };
1197
1198 runs.push(TextRun {
1199 len: end - start,
1200 font: face,
1201 color: match (mono, custom.color) {
1205 (_, Some(color)) => color,
1206 (true, None) => theme.code_text,
1207 (false, None) => theme.text,
1208 },
1209 background_color: custom.background,
1210 underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
1211 color: Some(theme.text_muted),
1212 thickness: px(1.0),
1213 wavy: false,
1214 }),
1215 strikethrough: strike.then_some(StrikethroughStyle {
1216 thickness: px(1.0),
1217 color: Some(theme.text_muted),
1218 }),
1219 });
1220 }
1221
1222 Flat {
1223 text: text.text.clone().into(),
1224 runs,
1225 links,
1226 code,
1227 chips,
1228 }
1229}
1230
1231fn text_element(
1232 text: &Text,
1233 size: f32,
1234 line_height: f32,
1235 weight: FontWeight,
1236 overlay: Overlay,
1237 theme: &Theme,
1238 cx: &App,
1239) -> AnyElement {
1240 let flat = flatten_with(text, weight, theme, |name| {
1241 crate::marks::paint_of(cx, name, theme)
1242 });
1243 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
1244}
1245
1246fn painted_text(
1252 flat: Flat,
1253 len: usize,
1254 size: f32,
1255 line_height: f32,
1256 overlay: Overlay,
1257 theme: &Theme,
1258) -> AnyElement {
1259 let (ix, part) = (overlay.block, overlay.part);
1260 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
1261 let span = 0..len;
1262 let hint = overlay
1265 .placeholder
1266 .filter(|_| len == 0 && overlay.caret().is_some())
1269 .map(|hint| {
1270 div()
1271 .absolute()
1272 .text_color(theme.text_faint)
1273 .child(hint.clone())
1274 });
1275 let styled = StyledText::new(flat.text).with_runs(flat.runs);
1276 let layout = styled.layout().clone();
1277
1278 let painted: AnyElement = if flat.links.is_empty() {
1279 styled.into_any_element()
1280 } else {
1281 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
1282 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
1283 .on_click(ranges, move |clicked, _window, cx| {
1284 if let Some(url) = urls.get(clicked) {
1285 cx.open_url(url);
1286 }
1287 })
1288 .into_any_element()
1289 };
1290
1291 let wash = theme.code_wash;
1295 let code_ranges = flat.code;
1296 let chip_wash = theme.element_hover;
1297 let chip_edge = theme.border;
1298 let chip_ranges = flat.chips;
1299 let caret_color = theme.caret;
1300 let selection_color = theme.selection;
1301 let annotated = overlay.annotated(len, theme);
1302 let layouts = overlay.layouts.cloned();
1303 let underlay = canvas(
1304 |_, _, _| (),
1305 move |_, _, window, _| {
1306 if let Some(layouts) = &layouts {
1307 layouts.record(ix, part, span.clone(), layout.clone());
1308 }
1309 for (range, wash) in &annotated {
1312 for rect in range_rects(&layout, range, 0.0, 0.0) {
1313 window.paint_quad(quad(
1314 rect,
1315 px(2.0),
1316 *wash,
1317 px(0.0),
1318 gpui::transparent_black(),
1319 BorderStyle::default(),
1320 ));
1321 }
1322 }
1323 if let Some(range) = &selected {
1327 for rect in range_rects(&layout, range, 0.0, 0.0) {
1328 window.paint_quad(quad(
1329 rect,
1330 px(2.0),
1331 selection_color,
1332 px(0.0),
1333 gpui::transparent_black(),
1334 BorderStyle::default(),
1335 ));
1336 }
1337 }
1338 if let Some(offset) = caret
1339 && let Some(head) = layout.position_for_index(offset)
1340 {
1341 window.paint_quad(quad(
1342 caret_quad(head, size, layout.line_height()),
1343 px(0.0),
1344 caret_color,
1345 px(0.0),
1346 gpui::transparent_black(),
1347 BorderStyle::default(),
1348 ));
1349 }
1350 for range in &code_ranges {
1351 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1352 window.paint_quad(quad(
1353 rect,
1354 px(INLINE_CODE_RADIUS),
1355 wash,
1356 px(0.0),
1357 gpui::transparent_black(),
1358 BorderStyle::default(),
1359 ));
1360 }
1361 }
1362 for range in &chip_ranges {
1365 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1366 window.paint_quad(quad(
1367 rect,
1368 px(Theme::control_radius()),
1369 chip_wash,
1370 px(1.0),
1371 chip_edge,
1372 BorderStyle::Solid,
1373 ));
1374 }
1375 }
1376 },
1377 )
1378 .absolute()
1379 .size_full();
1380
1381 div()
1382 .text_size(px(size))
1383 .line_height(px(line_height))
1384 .relative()
1385 .child(underlay)
1386 .children(hint)
1387 .child(painted)
1388 .into_any_element()
1389}
1390
1391fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1397 let inset = (line_height - px(size)) / 2.0;
1398 Bounds::new(
1399 head + point(px(0.0), inset),
1400 gpui::size(px(CARET_WIDTH), px(size)),
1401 )
1402}
1403
1404fn range_rects(
1406 layout: &gpui::TextLayout,
1407 range: &Range<usize>,
1408 pad_x: f32,
1409 inset_y: f32,
1410) -> Vec<Bounds<Pixels>> {
1411 let mut rects = Vec::new();
1412 let line_height = layout.line_height();
1413 let mut origin = layout.bounds().origin;
1414 let mut line_start = 0;
1415 for line in layout.line_layouts() {
1416 let shaped = &line.unwrapped_layout;
1417 let row_ends = line
1420 .wrap_boundaries()
1421 .iter()
1422 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
1423 .chain([line.len()]);
1424 let mut row_start = 0;
1425 for (row, row_end) in row_ends.enumerate() {
1426 let from = range
1427 .start
1428 .saturating_sub(line_start)
1429 .clamp(row_start, row_end);
1430 let to = range.end.saturating_sub(line_start).min(row_end);
1431 let row_x = shaped.x_for_index(row_start);
1432 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
1433 if from < to && right > left {
1434 rects.push(Bounds::new(
1435 origin
1436 + point(
1437 left - row_x - px(pad_x),
1438 line_height * row as f32 + px(inset_y),
1439 ),
1440 size(
1441 right - left + px(2.0 * pad_x),
1442 line_height - px(2.0 * inset_y),
1443 ),
1444 ));
1445 }
1446 row_start = row_end;
1447 }
1448 origin.y += line.size(line_height).height;
1449 line_start += line.len() + 1;
1451 }
1452 rects
1453}
1454
1455pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
1463 let Editing {
1464 selection,
1465 caret_on,
1466 layouts,
1467 annotations,
1468 typography,
1469 ..
1470 } = editing;
1471 let reset = layouts.map(|layouts| {
1475 let layouts = layouts.clone();
1476 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
1477 .absolute()
1478 .size(px(0.0))
1479 });
1480 let theme = Theme::of(cx).clone();
1481 let typography = typography.unwrap_or_else(|| Typography::of(cx));
1482 let overlay = Overlay {
1483 block: 0,
1484 part: Part::Code,
1485 selection,
1486 caret_on,
1487 layouts,
1488 annotations,
1489 placeholder: None,
1490 caption: Caption::default(),
1491 toggle: None,
1493 copy: CopyButton::Hidden,
1495 base: None,
1496 highlight: crate::marks::highlight_paint_of(cx),
1497 };
1498 let (underlay, lines) = code_lines(
1499 Some(crate::source::LANGUAGES[0]),
1500 code,
1501 overlay,
1502 &typography,
1503 &theme,
1504 cx,
1505 );
1506 let style = crate::SourceStyle::of(cx);
1508 let digits = lines.len().to_string().len().max(style.gutter_min_digits);
1509 let gap = style.gutter_gap.max(0.0) * typography.code.size();
1510 let gutter_width = digits as f32 * typography.code.size() + gap;
1511 let lines = lines
1512 .into_iter()
1513 .enumerate()
1514 .map(|(index, line)| {
1515 if !style.line_numbers {
1516 return line;
1517 }
1518 div()
1519 .flex()
1520 .items_start()
1521 .child(
1522 div()
1523 .w(px(gutter_width))
1524 .flex_shrink_0()
1525 .pr(px(gap))
1526 .font_family(theme.font_mono.clone())
1527 .text_color(style.gutter_color.unwrap_or(theme.text_faint))
1528 .text_right()
1529 .child((index + 1).to_string()),
1530 )
1531 .child(div().flex_1().min_w_0().child(line))
1532 .into_any_element()
1533 })
1534 .collect();
1535 div()
1536 .flex()
1537 .flex_col()
1538 .children(reset)
1539 .child(code_body(0, underlay, lines, &typography, true))
1540 .into_any_element()
1541}
1542
1543fn code_lines(
1546 language: Option<&str>,
1547 code: &str,
1548 overlay: Overlay,
1549 typography: &Typography,
1550 theme: &Theme,
1551 cx: &App,
1552) -> (AnyElement, Vec<AnyElement>) {
1553 let ix = overlay.block;
1554 let spans = crate::highlight::spans(cx, language, code).or_else(|| {
1559 language
1560 .filter(|language| crate::source::is_markdown(language))
1561 .map(|_| crate::source::spans(code))
1562 });
1563 let mono = font(theme.font_mono.clone());
1564 let run = |len: usize, color: Hsla| TextRun {
1565 len,
1566 font: mono.clone(),
1567 color,
1568 background_color: None,
1569 underline: None,
1570 strikethrough: None,
1571 };
1572 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1577 let mut offset = 0usize;
1578 let lines: Vec<AnyElement> = code
1579 .split('\n')
1580 .map(|line| {
1581 let start = offset;
1582 offset += line.len() + 1;
1583 let mut runs = Vec::new();
1584 let mut pos = 0usize;
1587 if let Some(spans) = &spans {
1588 let end = start + line.len();
1589 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1590 let s = range.start.clamp(start, end) - start;
1591 let e = range.end.min(end) - start;
1592 if s > pos {
1593 runs.push(run(s - pos, theme.text));
1594 }
1595 runs.push(run(e - s, theme.syntax.color(*kind)));
1596 pos = e;
1597 }
1598 }
1599 if pos < line.len() {
1600 runs.push(run(line.len() - pos, theme.text));
1601 }
1602 if runs.is_empty() {
1603 runs.push(run(0, theme.text));
1604 }
1605 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1606 rows.push((start..start + line.len(), styled.layout().clone()));
1607 styled.into_any_element()
1608 })
1609 .collect();
1610
1611 let caret = overlay.caret_painted();
1612 let selected = overlay.selected(code.len());
1613 let sink = overlay.layouts.cloned();
1614 let code_size = typography.code.size();
1615 let annotated = overlay.annotated(code.len(), theme);
1616 let (caret_color, selection_color) = (theme.caret, theme.selection);
1617 let underlay = canvas(
1618 |_, _, _| (),
1619 move |_, _, window, _| {
1620 for (span, layout) in &rows {
1621 if let Some(sink) = &sink {
1622 sink.record(ix, Part::Code, span.clone(), layout.clone());
1623 }
1624 for (range, wash) in &annotated {
1625 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1626 if from < to {
1627 for rect in
1628 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1629 {
1630 window.paint_quad(quad(
1631 rect,
1632 px(2.0),
1633 *wash,
1634 px(0.0),
1635 gpui::transparent_black(),
1636 BorderStyle::default(),
1637 ));
1638 }
1639 }
1640 }
1641 if let Some(range) = &selected {
1642 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1643 if from < to {
1644 for rect in
1645 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1646 {
1647 window.paint_quad(quad(
1648 rect,
1649 px(2.0),
1650 selection_color,
1651 px(0.0),
1652 gpui::transparent_black(),
1653 BorderStyle::default(),
1654 ));
1655 }
1656 }
1657 }
1658 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1659 && let Some(head) = layout.position_for_index(offset - span.start)
1660 {
1661 window.paint_quad(quad(
1662 caret_quad(head, code_size, layout.line_height()),
1663 px(0.0),
1664 caret_color,
1665 px(0.0),
1666 gpui::transparent_black(),
1667 BorderStyle::default(),
1668 ));
1669 }
1670 }
1671 },
1672 )
1673 .absolute()
1674 .size_full();
1675
1676 (underlay.into_any_element(), lines)
1677}
1678
1679fn code_block(
1680 language: Option<&str>,
1681 code: &str,
1682 overlay: Overlay,
1683 typography: &Typography,
1684 theme: &Theme,
1685 window: &mut Window,
1686 cx: &mut App,
1687) -> AnyElement {
1688 let ix = overlay.block;
1689 let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
1690 let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
1691
1692 div()
1693 .rounded(px(Theme::panel_radius()))
1694 .bg(theme.ink(0.035))
1695 .border_1()
1696 .border_color(theme.border)
1697 .overflow_hidden()
1698 .relative()
1699 .child(
1703 div()
1704 .relative()
1705 .flex()
1706 .flex_row()
1707 .items_center()
1708 .px(px(CODE_PADDING_X))
1709 .py(px(5.0))
1710 .border_b_1()
1711 .border_color(theme.border)
1712 .bg(theme.ink(0.02))
1713 .text_style(TextStyle::Subheadline)
1714 .text_color(match language {
1715 Some(_) => theme.text_muted,
1716 None => theme.text_faint,
1717 })
1718 .child(
1722 div()
1723 .relative()
1724 .children(overlay.layouts.map(|layouts| {
1725 let layouts = layouts.clone();
1726 canvas(
1727 move |bounds, _, _| layouts.record_language(ix, bounds),
1728 |_, _, _, _| (),
1729 )
1730 .absolute()
1731 .size_full()
1732 }))
1733 .child(SharedString::from(
1734 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1735 )),
1736 ),
1737 )
1738 .child(body)
1739 .children(
1740 (overlay.copy == CopyButton::Shown).then(|| copy_button(code, ix, theme, window, cx)),
1741 )
1742 .into_any_element()
1743}
1744
1745fn code_body(
1747 ix: usize,
1748 underlay: AnyElement,
1749 lines: Vec<AnyElement>,
1750 typography: &Typography,
1751 wrap: bool,
1752) -> AnyElement {
1753 let column = div()
1754 .flex()
1755 .flex_col()
1756 .px(px(CODE_PADDING_X))
1757 .children(lines);
1758 let body = div()
1759 .id(ElementId::named_usize("md-code", ix))
1760 .relative()
1761 .py(px(CODE_PADDING_Y))
1762 .text_size(px(typography.code.size()))
1763 .line_height(px(typography.code.line_height()))
1764 .child(underlay);
1765 if wrap {
1766 body.child(column.w_full()).into_any_element()
1769 } else {
1770 ui::scroll::Viewport::new(
1771 format!("md-code-scroll-{ix}"),
1772 body.flex()
1773 .flex_row()
1774 .whitespace_nowrap()
1775 .child(column.items_start()),
1782 gpui::Axis::Horizontal,
1783 )
1784 .into_any_element()
1785 }
1786}
1787
1788fn copy_button(
1795 code: &str,
1796 ix: usize,
1797 theme: &Theme,
1798 window: &mut Window,
1799 cx: &mut App,
1800) -> AnyElement {
1801 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1802 let showing = *copied.read(cx);
1803 let text: SharedString = code.to_string().into();
1804
1805 div()
1806 .id(ElementId::named_usize("md-copy", ix))
1807 .absolute()
1808 .top(px(3.0))
1809 .right(px(5.0))
1810 .h(px(20.0))
1811 .px(px(6.0))
1812 .rounded(px(5.0))
1813 .flex()
1814 .items_center()
1815 .cursor_pointer()
1816 .text_style(TextStyle::Caption)
1817 .text_color(theme.text_muted)
1818 .hover(|el| el.bg(theme.element_hover))
1819 .child(if showing { "Copied" } else { "Copy" })
1820 .on_click({
1821 let copied = copied.clone();
1822 move |_, _, cx| {
1823 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1824 copied.update(cx, |state, cx| {
1825 *state = true;
1826 cx.notify();
1827 });
1828 }
1829 })
1830 .on_hover(move |hovering, _, cx| {
1831 if !*hovering && *copied.read(cx) {
1832 copied.update(cx, |state, cx| {
1833 *state = false;
1834 cx.notify();
1835 });
1836 }
1837 })
1838 .into_any_element()
1839}
1840
1841fn image(
1848 url: &str,
1849 alt: &Text,
1850 width: Option<u32>,
1851 overlay: Overlay,
1852 typography: &Typography,
1853 theme: &Theme,
1854 cx: &App,
1855) -> AnyElement {
1856 let hint = SharedString::new_static(CAPTION_HINT);
1857 let overlay = Overlay {
1858 placeholder: Some(&hint),
1859 ..overlay.at(Part::Caption)
1860 };
1861 let picture = if url.is_empty() {
1862 div()
1863 .h(px(IMAGE_EMPTY_HEIGHT))
1864 .flex()
1865 .items_center()
1866 .px(px(CARD_PADDING))
1867 .rounded(px(Theme::button_radius()))
1868 .border_1()
1869 .border_dashed()
1870 .border_color(theme.border)
1871 .text_size(px(typography.body.size()))
1872 .text_color(theme.text_muted)
1873 .child(IMAGE_EMPTY)
1874 } else {
1875 let picture = img(image_source(url, overlay.base));
1876 let box_ = div()
1877 .relative()
1878 .rounded(px(Theme::button_radius()))
1879 .overflow_hidden()
1880 .border_1()
1881 .border_color(theme.border)
1882 .children(overlay.layouts.map(|layouts| {
1883 let layouts = layouts.clone();
1884 let ix = overlay.block;
1885 canvas(
1886 move |bounds, _, _| layouts.record_picture(ix, bounds),
1887 |_, _, _, _| (),
1888 )
1889 .absolute()
1890 .size_full()
1891 }));
1892 match width {
1893 Some(width) => box_
1898 .self_start()
1899 .max_w_full()
1900 .w(px(width as f32))
1901 .child(picture.w(px(width as f32)).max_w_full()),
1902 None => box_.child(picture.max_w_full()),
1905 }
1906 };
1907 div()
1908 .flex()
1909 .flex_col()
1910 .gap(px(CAPTION_GAP))
1911 .child(picture)
1912 .when(
1915 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1916 |el| {
1917 el.child(text_element(
1918 alt,
1919 typography.caption.size(),
1920 typography.caption.line_height(),
1921 FontWeight::NORMAL,
1922 overlay,
1923 theme,
1924 cx,
1925 ))
1926 },
1927 )
1928 .into_any_element()
1929}
1930
1931fn bookmark(
1943 ix: usize,
1944 url: &str,
1945 form: Form,
1946 typography: &Typography,
1947 theme: &Theme,
1948 cx: &App,
1949) -> AnyElement {
1950 let preview = preview::of(cx, url).unwrap_or_default();
1951 let host = SharedString::from(preview::host(url).to_string());
1952 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1953 let title = preview
1954 .title
1955 .clone()
1956 .unwrap_or_else(|| SharedString::from(url.to_string()));
1957
1958 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1961 let site = host.clone();
1962 let mark = move |size: f32| {
1963 let host = site.clone();
1964 match icon.clone() {
1965 Some(icon) => img(icon)
1966 .size(px(size))
1967 .rounded(px(size / 4.0))
1968 .with_fallback(move || initial(&host, size, muted, wash))
1969 .into_any_element(),
1970 None => initial(&host, size, muted, wash),
1971 }
1972 };
1973
1974 if form == Form::Chip {
1975 let open = url.to_string();
1976 let pill = div()
1977 .id(ElementId::named_usize("md-chip", ix))
1978 .flex()
1979 .flex_row()
1980 .items_center()
1981 .gap(px(6.0))
1982 .px(px(CHIP_BLOCK_PAD_X))
1983 .py(px(CHIP_BLOCK_PAD_Y))
1984 .rounded(px(Theme::control_radius()))
1985 .border_1()
1986 .border_color(theme.border)
1987 .bg(theme.element_hover)
1988 .text_size(px(typography.body.size()))
1989 .line_height(px(typography.body.line_height()))
1990 .text_color(theme.text)
1991 .cursor(CursorStyle::PointingHand)
1992 .hover(|el| el.bg(theme.element_active))
1993 .on_click(move |_, _, cx| cx.open_url(&open))
1994 .child(mark(CHIP_ICON))
1995 .child(
1998 div()
1999 .min_w_0()
2000 .truncate()
2001 .child(preview.title.unwrap_or(label)),
2002 );
2003 return div().flex().flex_row().child(pill).into_any_element();
2006 }
2007
2008 let words = div()
2009 .flex()
2010 .flex_col()
2011 .min_w_0()
2012 .px(px(CARD_PADDING))
2013 .py(px(CARD_PADDING - 2.0))
2014 .child(
2015 div()
2016 .truncate()
2017 .text_size(px(typography.body.size()))
2018 .line_height(px(typography.body.line_height()))
2019 .text_color(theme.text)
2020 .child(title),
2021 )
2022 .children(preview.description.map(|blurb| {
2023 div()
2024 .line_clamp(2)
2025 .text_size(px(typography.card.size()))
2026 .line_height(px(typography.card.line_height()))
2027 .text_color(theme.text_muted)
2028 .child(blurb)
2029 }))
2030 .child(
2031 div()
2032 .mt_auto()
2033 .pt(px(6.0))
2034 .flex()
2035 .items_center()
2036 .gap(px(6.0))
2037 .text_size(px(typography.card.size()))
2038 .text_color(theme.text_muted)
2039 .child(mark(CARD_ICON))
2040 .child(div().truncate().child(label)),
2041 );
2042
2043 let picture = corners(div(), form)
2044 .bg(theme.surface)
2045 .flex()
2046 .items_center()
2047 .justify_center()
2048 .overflow_hidden()
2049 .child(match preview.image {
2050 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
2051 .with_fallback(move || mark(CARD_COVER))
2052 .into_any_element(),
2053 None => mark(CARD_COVER),
2054 });
2055
2056 let open = url.to_string();
2057 let card = div()
2058 .id(ElementId::named_usize("md-bookmark", ix))
2059 .flex()
2060 .w_full()
2061 .overflow_hidden()
2062 .rounded(px(Theme::button_radius()))
2063 .border(px(CARD_BORDER))
2064 .border_color(theme.border)
2065 .bg(theme.surface_card)
2066 .cursor(CursorStyle::PointingHand)
2067 .hover(|el| el.bg(theme.element_hover))
2068 .on_click(move |_, _, cx| cx.open_url(&open));
2069
2070 if form == Form::Embed {
2071 card.flex_col()
2072 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
2073 .child(words.w_full())
2074 } else {
2075 card.h(px(CARD_HEIGHT))
2076 .child(words.flex_1())
2077 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
2078 }
2079 .into_any_element()
2080}
2081
2082fn corners<T: Styled>(element: T, form: Form) -> T {
2086 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
2087 match form {
2088 Form::Embed => element.rounded_t(corner),
2089 _ => element.rounded_r(corner),
2090 }
2091}
2092
2093fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
2096 div()
2097 .flex_none()
2098 .size(px(size))
2099 .rounded(px(size / 4.0))
2100 .bg(wash)
2101 .flex()
2102 .items_center()
2103 .justify_center()
2104 .text_size(px(size * 0.55))
2105 .text_color(color)
2106 .child(SharedString::from(
2107 host.chars()
2108 .next()
2109 .unwrap_or('?')
2110 .to_uppercase()
2111 .to_string(),
2112 ))
2113 .into_any_element()
2114}
2115
2116#[expect(
2123 clippy::too_many_arguments,
2124 reason = "a table, its overlay, and what paints them"
2125)]
2126fn table(
2127 align: &[Align],
2128 header: &[Text],
2129 rows: &[Vec<Text>],
2130 overlay: Overlay,
2131 typography: &Typography,
2132 theme: &Theme,
2133 window: &mut Window,
2134 cx: &App,
2135) -> AnyElement {
2136 let ix = overlay.block;
2137 let all: Vec<&[Text]> = std::iter::once(header)
2138 .filter(|row| !row.is_empty())
2139 .chain(rows.iter().map(|row| row.as_slice()))
2140 .collect();
2141 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
2142 if columns == 0 {
2143 return gpui::Empty.into_any_element();
2144 }
2145 let has_header = !header.is_empty();
2146
2147 let text_system = window.text_system();
2148 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
2149 let mut content = vec![0.0f32; columns];
2150 for (r, row) in all.iter().enumerate() {
2151 let weight = if has_header && r == 0 {
2152 FontWeight::BOLD
2153 } else {
2154 FontWeight::NORMAL
2155 };
2156 let mut out = Vec::with_capacity(columns);
2157 for (c, natural) in content.iter_mut().enumerate() {
2158 let Some(cell) = row.get(c) else {
2159 out.push(None);
2160 continue;
2161 };
2162 let flat = flatten_with(cell, weight, theme, |name| {
2163 crate::marks::paint_of(cx, name, theme)
2164 });
2165 if !flat.text.is_empty() {
2166 let width = f32::from(
2167 text_system
2168 .shape_line(
2169 flat.text.clone(),
2170 px(typography.body.size()),
2171 &flat.runs,
2172 None,
2173 )
2174 .width(),
2175 );
2176 *natural = natural.max(width);
2177 }
2178 out.push(Some(flat));
2179 }
2180 flats.push(out);
2181 }
2182
2183 let naturals: Vec<f32> = content
2184 .iter()
2185 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
2186 .collect();
2187 let minimums: Vec<f32> = naturals
2188 .iter()
2189 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
2190 .collect();
2191 let hairline = theme.hairline(0.10);
2192
2193 let mut inner = div()
2194 .flex()
2195 .flex_col()
2196 .w_full()
2197 .min_w(px(minimums.iter().sum::<f32>()));
2198 for (r, row) in flats.into_iter().enumerate() {
2199 if r > 0 {
2200 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
2201 }
2202 let mut row_el = div().flex().flex_row();
2203 for (c, cell) in row.into_iter().enumerate() {
2204 let mut cell_el = div()
2205 .flex_grow(naturals[c])
2206 .flex_shrink(naturals[c])
2207 .flex_basis(px(0.0))
2208 .min_w(px(minimums[c]))
2209 .p(px(TABLE_CELL_PADDING))
2210 .text_size(px(typography.body.size()))
2211 .line_height(px(typography.body.line_height()));
2212 cell_el = match align.get(c).copied().unwrap_or_default() {
2213 Align::Left => cell_el,
2214 Align::Center => cell_el.text_center(),
2215 Align::Right => cell_el.text_right(),
2216 };
2217 if let Some(flat) = cell {
2218 let row = if has_header { r } else { r + 1 };
2222 let len = flat.text.len();
2223 cell_el = cell_el.child(painted_text(
2224 flat,
2225 len,
2226 typography.body.size(),
2227 typography.body.line_height(),
2228 overlay.at(Part::Cell { row, column: c }),
2229 theme,
2230 ));
2231 }
2232 row_el = row_el.child(cell_el);
2233 }
2234 inner = inner.child(row_el);
2235 }
2236
2237 ui::scroll::Viewport::new(
2238 format!("md-table-scroll-{ix}"),
2239 div()
2240 .id(ElementId::named_usize("md-table", ix))
2241 .w_full()
2242 .child(inner),
2243 gpui::Axis::Horizontal,
2244 )
2245 .into_any_element()
2246}