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