1use crate::AppExt as _;
11use std::{
12 cell::RefCell,
13 collections::HashMap,
14 hash::{DefaultHasher, Hash, Hasher},
15 ops::Range,
16 path::Path,
17 rc::Rc,
18};
19
20use gpui::{
21 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
22 ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
23 StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
24 canvas, div, font, img, point, prelude::*, px, quad, size,
25};
26use theme::{TextStyle, Theme, Typeset};
27
28use crate::{
29 block,
30 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
31 preview,
32 select::{Cursor, Selection},
33 typography::Typography,
34};
35
36mod code;
38mod column;
39mod layouts;
40mod media;
41mod table;
42mod text;
43
44pub use code::*;
45use column::*;
46pub use layouts::*;
47use media::*;
48use table::*;
49pub use text::*;
50
51const BLOCK_GAP: f32 = 12.0;
52const LIST_GAP: f32 = 4.0;
53const INDENT_WIDTH: f32 = 22.0;
55const MARKER_WIDTH: f32 = 18.0;
57const MARKER_GAP: f32 = 8.0;
58const CODE_PADDING_X: f32 = 12.0;
60const CODE_PADDING_Y: f32 = 10.0;
61pub const PLAIN_LANGUAGE: &str = "Plain";
64const CARET_WIDTH: f32 = 1.5;
67const INLINE_CODE_RADIUS: f32 = 4.5;
70const INLINE_CODE_PAD_X: f32 = 2.0;
71const INLINE_CODE_INSET_Y: f32 = 2.0;
72const MENTION_CARD_WIDTH: f32 = 320.0;
74const CARD_HEIGHT: f32 = 116.0;
78const CARD_IMAGE_WIDTH: f32 = 180.0;
79const CARD_COVER_HEIGHT: f32 = 200.0;
80const CARD_PADDING: f32 = 14.0;
81const CARD_BORDER: f32 = 1.0;
82const CARD_ICON: f32 = 16.0;
83const CARD_COVER: f32 = 44.0;
84const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
86const CAPTION_GAP: f32 = 4.0;
87const IMAGE_EMPTY: &str = "Add an image";
89const CAPTION_HINT: &str = "Write a caption";
90const TABLE_CELL_PADDING: f32 = 12.0;
93pub const TABLE_CONTROL_SIZE: f32 = 16.0;
95const TABLE_DIVIDER: f32 = 1.0;
96const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
99const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
101
102#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
113pub enum Caption {
114 #[default]
116 Shown,
117 Hidden,
119}
120
121#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
125pub enum CopyButton {
126 #[default]
128 Shown,
129 Hidden,
132}
133
134#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
140#[non_exhaustive]
141pub enum Annotation {
142 #[default]
144 Open,
145 Resolved,
147 Active,
149 Highlight(crate::HighlightColor),
152 Match,
154 Current,
156}
157
158impl Annotation {
159 fn wash(self, theme: &Theme, highlight: crate::HighlightPaint, find: crate::FindPaint) -> Hsla {
160 match self {
161 Self::Open => theme.warning.opacity(0.20),
162 Self::Resolved => theme.warning.opacity(0.08),
163 Self::Active => theme.warning.opacity(0.38),
164 Self::Highlight(color) => highlight(color, theme),
165 Self::Match => find(theme).0,
166 Self::Current => find(theme).1,
167 }
168 }
169}
170
171pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
176
177pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
179
180pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
185pub enum ImageOverlayCorner {
186 TopLeft,
187 TopRight,
188 BottomLeft,
189 #[default]
190 BottomRight,
191}
192
193#[derive(Clone)]
199pub enum Toggle {
200 Handled(OnToggle),
203 HitTested,
209}
210
211#[derive(Clone)]
217pub struct Editing<'a> {
218 pub selection: Option<Selection>,
221 pub caret_on: bool,
224 pub layouts: Option<&'a BlockLayouts>,
230 pub annotations: &'a [(Selection, Annotation)],
232 pub placeholder: Option<SharedString>,
234 pub caption: Caption,
235 pub typography: Option<Typography>,
239 pub toggle: Option<Toggle>,
242 pub image: Option<OnImage>,
246 pub image_overlay: Option<ImageOverlay>,
249 pub image_overlay_corner: ImageOverlayCorner,
251 pub table_controls: bool,
253 pub copy: CopyButton,
255 pub base: Option<&'a Path>,
258 pub keep: &'a [usize],
263 pub scroll: Option<&'a gpui::ScrollHandle>,
267}
268
269impl Default for Editing<'_> {
270 fn default() -> Self {
271 Self {
272 selection: None,
273 caret_on: true,
276 layouts: None,
277 annotations: &[],
278 placeholder: None,
279 caption: Caption::default(),
280 typography: None,
281 toggle: None,
282 image: None,
283 image_overlay: None,
284 image_overlay_corner: ImageOverlayCorner::BottomRight,
285 table_controls: false,
286 copy: CopyButton::default(),
287 base: None,
288 keep: &[],
289 scroll: None,
290 }
291 }
292}
293
294#[derive(Clone, Copy)]
300struct Overlay<'a> {
301 block: usize,
302 part: Part,
303 selection: Option<Selection>,
304 caret_on: bool,
305 layouts: Option<&'a BlockLayouts>,
306 annotations: &'a [(Selection, Annotation)],
308 placeholder: Option<&'a SharedString>,
311 caption: Caption,
312 toggle: Option<&'a Toggle>,
315 image: Option<&'a OnImage>,
316 image_overlay: Option<&'a ImageOverlay>,
317 image_overlay_corner: ImageOverlayCorner,
318 table_controls: bool,
319 copy: CopyButton,
320 base: Option<&'a Path>,
321 highlight: crate::HighlightPaint,
322 find: crate::FindPaint,
323}
324
325impl<'a> Overlay<'a> {
326 fn at(self, part: Part) -> Self {
327 Self { part, ..self }
328 }
329
330 fn here(&self) -> Cursor {
331 Cursor::new(self.block, self.part, 0)
332 }
333
334 fn caret_painted(&self) -> Option<usize> {
340 self.caret_on.then(|| self.caret()).flatten()
341 }
342
343 fn caret(&self) -> Option<usize> {
345 self.selection
346 .map(|selection| selection.head)
347 .filter(|head| head.block == self.block && head.part == self.part)
348 .map(|head| head.offset)
349 }
350
351 fn selected(&self, len: usize) -> Option<Range<usize>> {
353 self.clip(self.selection?, len)
354 }
355
356 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
359 self.annotations
360 .iter()
361 .filter_map(|(range, kind)| {
362 Some((
363 self.clip(*range, len)?,
364 kind.wash(theme, self.highlight, self.find),
365 ))
366 })
367 .collect()
368 }
369
370 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
376 if selection.is_collapsed() {
377 return None;
378 }
379 let (start, end) = selection.ordered();
380 let here = self.here();
381 let (first, last) = (
382 Cursor::new(start.block, start.part, 0),
383 Cursor::new(end.block, end.part, 0),
384 );
385 if here < first || here > last {
386 return None;
387 }
388 let from = if here == first { start.offset } else { 0 };
389 let to = if here == last { end.offset } else { len };
390 (from < to).then_some(from..to.min(len))
391 }
392
393 fn covers_block(&self) -> bool {
397 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
398 return false;
399 };
400 let (start, end) = selection.ordered();
401 start.block < self.block && self.block < end.block
402 }
403}
404
405pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
411 if url.contains("://") {
412 return SharedString::from(url.to_string()).into();
413 }
414 match base {
417 Some(base) => base.join(url).into(),
418 None => std::path::PathBuf::from(url).into(),
419 }
420}
421
422pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
424 let doc = crate::parse_with(source, &cx.marks());
425 render(&doc, Caption::default(), window, cx)
426}
427
428pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
430 render_with(
431 doc,
432 Editing {
433 caption,
434 ..Editing::default()
435 },
436 window,
437 cx,
438 )
439}
440
441pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
449 let Editing {
450 selection,
451 caret_on,
452 layouts,
453 annotations,
454 placeholder,
455 caption,
456 typography,
457 toggle,
458 image,
459 image_overlay,
460 image_overlay_corner,
461 table_controls,
462 copy,
463 base,
464 keep,
465 scroll,
466 } = editing;
467 let theme = Theme::of(cx).clone();
470 let typography = typography.unwrap_or_else(|| cx.typography());
471 let highlight = crate::marks::highlight_paint_of(cx);
472 let find = crate::find::find_paint_of(cx);
473 let gaps: Vec<Pixels> = doc
474 .blocks
475 .iter()
476 .enumerate()
477 .map(|(ix, block)| {
478 px(match doc.blocks.get(ix.wrapping_sub(1)) {
479 None => 0.0,
480 Some(previous) if tight(previous, block) => LIST_GAP,
481 Some(_) => BLOCK_GAP,
482 })
483 })
484 .collect();
485
486 let Some(layouts) = layouts else {
487 let mut column = div().flex().flex_col();
488 for (ix, block) in doc.blocks.iter().enumerate() {
489 let overlay = Overlay {
490 block: ix,
491 part: Part::Body,
492 selection,
493 caret_on,
494 layouts: None,
495 annotations,
496 placeholder: placeholder.as_ref(),
497 caption,
498 toggle: toggle.as_ref(),
499 image: image.as_ref(),
500 image_overlay: image_overlay.as_ref(),
501 image_overlay_corner,
502 table_controls,
503 copy,
504 base,
505 highlight,
506 find,
507 };
508 column = column
509 .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
510 }
511 return column.into_any_element();
512 };
513
514 let keys: Vec<u64> = doc
515 .blocks
516 .iter()
517 .map(|block| block_key(block, &typography, table_controls))
518 .collect();
519 layouts.prune(&keys);
520 let guesses: Vec<Guess> = doc
521 .blocks
522 .iter()
523 .map(|block| {
524 let mut guess = guess(block, &typography);
525 if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
526 guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
527 }
528 guess
529 })
530 .collect();
531 let mut kept = keep.to_vec();
532 kept.extend(selection.map(|selection| selection.head.block));
533 kept.extend(selection.map(|selection| selection.anchor.block));
534 let owned = Owned {
535 blocks: doc.blocks.clone(),
536 selection,
537 caret_on,
538 layouts: layouts.clone(),
539 annotations: annotations.to_vec(),
540 placeholder,
541 caption,
542 toggle,
543 image,
544 image_overlay,
545 image_overlay_corner,
546 table_controls,
547 copy,
548 base: base.map(Path::to_path_buf),
549 highlight,
550 find,
551 typography,
552 theme,
553 };
554 Column {
555 layouts: layouts.clone(),
556 keys: keys.into(),
557 gaps: gaps.into(),
558 guesses: guesses.into(),
559 keep: kept,
560 scroll: scroll.cloned(),
561 item_of: Box::new(|at| at.block),
562 build: Box::new(move |ix, window, cx| {
563 let overlay = Overlay {
564 block: ix,
565 part: Part::Body,
566 selection: owned.selection,
567 caret_on: owned.caret_on,
568 layouts: Some(&owned.layouts),
569 annotations: &owned.annotations,
570 placeholder: owned.placeholder.as_ref(),
571 caption: owned.caption,
572 toggle: owned.toggle.as_ref(),
573 image: owned.image.as_ref(),
574 image_overlay: owned.image_overlay.as_ref(),
575 image_overlay_corner: owned.image_overlay_corner,
576 table_controls: owned.table_controls,
577 copy: owned.copy,
578 base: owned.base.as_deref(),
579 highlight: owned.highlight,
580 find: owned.find,
581 };
582 block_box(
583 &owned.blocks[ix],
584 overlay,
585 &owned.typography,
586 &owned.theme,
587 window,
588 cx,
589 )
590 .into_any_element()
591 }),
592 }
593 .into_any_element()
594}
595
596struct Owned {
598 blocks: Vec<Block>,
599 selection: Option<Selection>,
600 caret_on: bool,
601 layouts: BlockLayouts,
602 annotations: Vec<(Selection, Annotation)>,
603 placeholder: Option<SharedString>,
604 caption: Caption,
605 toggle: Option<Toggle>,
606 image: Option<OnImage>,
607 image_overlay: Option<ImageOverlay>,
608 image_overlay_corner: ImageOverlayCorner,
609 table_controls: bool,
610 copy: CopyButton,
611 base: Option<std::path::PathBuf>,
612 highlight: crate::HighlightPaint,
613 find: crate::FindPaint,
614 typography: Typography,
615 theme: Theme,
616}
617
618fn block_box(
621 block: &Block,
622 overlay: Overlay,
623 typography: &Typography,
624 theme: &Theme,
625 window: &mut Window,
626 cx: &mut App,
627) -> gpui::Div {
628 let ix = overlay.block;
629 let frame = overlay.layouts.map(|layouts| {
632 let layouts = layouts.clone();
633 canvas(
634 move |bounds, _, _| layouts.record_block(ix, bounds),
635 |_, _, _, _| (),
636 )
637 .absolute()
638 .size_full()
639 });
640 div()
646 .w_full()
647 .pl(px(block.indent as f32 * INDENT_WIDTH))
648 .child(
649 div()
650 .w_full()
651 .relative()
652 .children(frame)
653 .when(overlay.covers_block() && block.opaque(), |el| {
657 el.rounded(px(4.0)).bg(theme.selection)
658 })
659 .child(block_element(block, overlay, typography, theme, window, cx)),
660 )
661}
662
663fn block_key(block: &Block, typography: &Typography, table_controls: bool) -> u64 {
666 let mut hasher = DefaultHasher::new();
667 table_controls.hash(&mut hasher);
668 block.hash(&mut hasher);
669 typography.body.size().to_bits().hash(&mut hasher);
670 typography.body.line_height().to_bits().hash(&mut hasher);
671 hasher.finish()
672}
673
674fn guess(block: &Block, typography: &Typography) -> Guess {
676 let body = px(typography.body.line_height());
677 let prose = |chars: usize, line: Pixels| Guess {
678 chars,
679 line,
680 rows: 0,
681 extra: px(0.0),
682 indent: px(0.0),
683 };
684 let guess = match &block.kind {
685 BlockKind::Paragraph(text)
686 | BlockKind::Bullet(text)
687 | BlockKind::Ordered { text, .. }
688 | BlockKind::Task { text, .. }
689 | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
690 BlockKind::Heading { level, text } => prose(
691 text.text.len(),
692 px(typography.heading(*level).line_height()),
693 ),
694 BlockKind::Code { code, .. } => Guess {
695 rows: code.text.lines().count().max(1),
696 line: px(typography.code.line_height()),
697 extra: px(2.0 * CODE_PADDING_Y) + body,
698 ..prose(0, body)
699 },
700 BlockKind::Table { rows, .. } => Guess {
701 rows: rows.len() + 1,
702 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
703 ..prose(0, body)
704 },
705 BlockKind::Image { alt, .. } => Guess {
706 extra: px(240.0),
707 ..prose(alt.text.len(), px(typography.caption.line_height()))
708 },
709 BlockKind::Bookmark { .. } => Guess {
710 extra: body * 3.0,
711 ..prose(0, body)
712 },
713 BlockKind::Rule => Guess {
714 extra: body,
715 ..prose(0, px(0.0))
716 },
717 };
718 Guess {
719 indent: px(block.indent as f32 * INDENT_WIDTH),
720 ..guess
721 }
722}
723
724fn tight(previous: &Block, next: &Block) -> bool {
726 let marker = |block: &Block| {
727 matches!(
728 block.kind,
729 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
730 )
731 };
732 marker(previous) && (marker(next) || next.indent > previous.indent)
733}
734
735fn block_element(
736 block: &Block,
737 overlay: Overlay,
738 typography: &Typography,
739 theme: &Theme,
740 window: &mut Window,
741 cx: &mut App,
742) -> AnyElement {
743 let body = overlay.at(Part::Body);
744 match &block.kind {
745 BlockKind::Paragraph(text) => text_element(
746 text,
747 typography.body.size(),
748 typography.body.line_height(),
749 FontWeight::NORMAL,
750 body,
751 theme,
752 cx,
753 ),
754 BlockKind::Heading { level, text } => {
755 let heading = typography.heading(*level);
756 text_element(
757 text,
758 heading.size(),
759 heading.line_height(),
760 heading.weight,
761 body,
762 theme,
763 cx,
764 )
765 }
766 BlockKind::Bullet(text) => {
767 marker_row(disc(typography, theme), text, body, typography, theme, cx)
768 }
769 BlockKind::Ordered { number, text } => marker_row(
770 div()
771 .flex_none()
772 .w(px(MARKER_WIDTH))
773 .text_size(px(typography.body.size()))
774 .line_height(px(typography.body.line_height()))
775 .text_color(theme.text_muted)
776 .child(SharedString::from(format!("{number}.")))
777 .into_any_element(),
778 text,
779 body,
780 typography,
781 theme,
782 cx,
783 ),
784 BlockKind::Task { checked, text } => marker_row(
785 checkbox(*checked, overlay, typography, theme),
786 text,
787 body,
788 typography,
789 theme,
790 cx,
791 ),
792 BlockKind::Quote { kind, text } => div()
793 .border_l_2()
794 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
795 .pl(px(12.0))
796 .pr(px(10.0))
797 .py(px(2.0))
798 .text_color(theme.text_muted)
799 .children(kind.map(|kind| {
800 div()
801 .pb(px(2.0))
802 .text_size(px(typography.body.size()))
803 .line_height(px(typography.body.line_height()))
804 .font_weight(FontWeight::SEMIBOLD)
805 .text_color(alert_color(kind, theme))
806 .child(kind.label())
807 }))
808 .child(text_element(
809 text,
810 typography.body.size(),
811 typography.body.line_height(),
812 FontWeight::NORMAL,
813 body,
814 theme,
815 cx,
816 ))
817 .into_any_element(),
818 BlockKind::Code { language, code } => {
819 let overlay = overlay.at(Part::Code);
820 let painted = overlay
824 .caret()
825 .is_none()
826 .then(|| block::render(language.as_deref(), &code.text, window, cx))
827 .flatten();
828 match painted {
829 Some(element) => div()
832 .when(overlay.covers_block(), |el| {
833 el.rounded(px(4.0)).bg(theme.selection)
834 })
835 .child(element)
836 .into_any_element(),
837 None => code_block(
838 language.as_deref(),
839 &code.text,
840 overlay,
841 typography,
842 theme,
843 window,
844 cx,
845 ),
846 }
847 }
848 BlockKind::Image { url, alt, width } => {
849 image(url, alt, *width, overlay, typography, theme, window, cx)
850 }
851 BlockKind::Bookmark { url, form } => {
852 bookmark(overlay.block, url, *form, typography, theme, cx)
853 }
854 BlockKind::Table {
855 align,
856 header,
857 rows,
858 } => table(align, header, rows, overlay, typography, theme, window, cx),
859 BlockKind::Rule => div()
860 .h(px(1.0))
861 .w_full()
862 .bg(theme.border)
863 .into_any_element(),
864 }
865}
866
867fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
869 div()
870 .flex_none()
871 .w(px(MARKER_WIDTH))
872 .h(px(typography.body.line_height()))
873 .flex()
874 .items_center()
875 .child(
876 div()
877 .ml(px(1.0))
878 .w(px(5.0))
879 .h(px(5.0))
880 .rounded_full()
881 .bg(theme.text_faint),
882 )
883 .into_any_element()
884}
885
886fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
887 let ix = overlay.block;
888 let mut box_ = div()
889 .relative()
890 .w(px(13.0))
891 .h(px(13.0))
892 .rounded(px(3.5))
893 .border_1()
894 .flex()
895 .items_center()
896 .justify_center();
897 box_ = if checked {
898 box_.bg(theme.solid)
899 .border_color(theme.solid)
900 .text_style(TextStyle::Caption)
901 .text_color(theme.on_solid)
902 .child("✓")
903 } else {
904 box_.border_color(theme.border_strong)
905 };
906 box_ = box_.children(overlay.layouts.map(|layouts| {
909 let layouts = layouts.clone();
910 canvas(
911 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
912 |_, _, _, _| (),
913 )
914 .absolute()
915 .size_full()
916 }));
917 if overlay.toggle.is_some() {
920 box_ = box_.cursor_pointer();
921 }
922 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
923 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
924 cx.stop_propagation();
928 toggle(ix, window, cx);
929 });
930 }
931
932 div()
933 .flex_none()
934 .w(px(MARKER_WIDTH))
935 .h(px(typography.body.line_height()))
936 .flex()
937 .items_center()
938 .child(box_)
939 .into_any_element()
940}
941
942fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
944 match kind {
945 QuoteKind::Note => theme.accent,
946 QuoteKind::Tip => theme.success,
947 QuoteKind::Important => theme.busy,
948 QuoteKind::Warning => theme.warning,
949 QuoteKind::Caution => theme.danger,
950 }
951}
952
953fn marker_row(
954 marker: AnyElement,
955 text: &Text,
956 overlay: Overlay,
957 typography: &Typography,
958 theme: &Theme,
959 cx: &App,
960) -> AnyElement {
961 div()
962 .flex()
963 .flex_row()
964 .gap(px(MARKER_GAP))
965 .child(marker)
966 .child(div().flex_1().min_w_0().child(text_element(
967 text,
968 typography.body.size(),
969 typography.body.line_height(),
970 FontWeight::NORMAL,
971 overlay,
972 theme,
973 cx,
974 )))
975 .into_any_element()
976}