1use std::{
11 cell::RefCell,
12 collections::HashMap,
13 hash::{DefaultHasher, Hash, Hasher},
14 ops::Range,
15 path::Path,
16 rc::Rc,
17};
18
19use gpui::{
20 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
21 ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
22 StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
23 canvas, div, font, img, point, prelude::*, px, quad, size,
24};
25use theme::{TextStyle, Theme, Typeset};
26
27use crate::{
28 block,
29 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
30 layout::Layout,
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 CHIP_PAD_X: f32 = 4.0;
75const CHIP_INSET_Y: f32 = 1.0;
76const CHIP_BLOCK_PAD_X: f32 = 8.0;
79const CHIP_BLOCK_PAD_Y: f32 = 3.0;
80const CHIP_ICON: f32 = 15.0;
81const CARD_HEIGHT: f32 = 116.0;
85const CARD_IMAGE_WIDTH: f32 = 180.0;
86const CARD_COVER_HEIGHT: f32 = 200.0;
87const CARD_PADDING: f32 = 14.0;
88const CARD_BORDER: f32 = 1.0;
89const CARD_ICON: f32 = 16.0;
90const CARD_COVER: f32 = 44.0;
91const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
93const CAPTION_GAP: f32 = 4.0;
94const IMAGE_EMPTY: &str = "Add an image";
96const CAPTION_HINT: &str = "Write a caption";
97const TABLE_CELL_PADDING: f32 = 12.0;
100const TABLE_DIVIDER: f32 = 1.0;
101const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
104const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
106
107#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
118pub enum Caption {
119 #[default]
121 Shown,
122 Hidden,
124}
125
126#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
130pub enum CopyButton {
131 #[default]
133 Shown,
134 Hidden,
137}
138
139#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
145#[non_exhaustive]
146pub enum Annotation {
147 #[default]
149 Open,
150 Resolved,
152 Active,
154 Highlight(crate::HighlightColor),
157}
158
159impl Annotation {
160 fn wash(self, theme: &Theme, highlight: crate::HighlightPaint) -> 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 }
167 }
168}
169
170pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
175
176#[derive(Clone)]
182pub enum Toggle {
183 Handled(OnToggle),
186 HitTested,
192}
193
194#[derive(Clone)]
200pub struct Editing<'a> {
201 pub selection: Option<Selection>,
204 pub caret_on: bool,
207 pub layouts: Option<&'a BlockLayouts>,
213 pub annotations: &'a [(Selection, Annotation)],
215 pub placeholder: Option<SharedString>,
217 pub caption: Caption,
218 pub typography: Option<Typography>,
222 pub toggle: Option<Toggle>,
225 pub copy: CopyButton,
227 pub base: Option<&'a Path>,
230 pub keep: &'a [usize],
235 pub scroll: Option<&'a gpui::ScrollHandle>,
239}
240
241impl Default for Editing<'_> {
242 fn default() -> Self {
243 Self {
244 selection: None,
245 caret_on: true,
248 layouts: None,
249 annotations: &[],
250 placeholder: None,
251 caption: Caption::default(),
252 typography: None,
253 toggle: None,
254 copy: CopyButton::default(),
255 base: None,
256 keep: &[],
257 scroll: None,
258 }
259 }
260}
261
262#[derive(Clone, Copy)]
268struct Overlay<'a> {
269 block: usize,
270 part: Part,
271 selection: Option<Selection>,
272 caret_on: bool,
273 layouts: Option<&'a BlockLayouts>,
274 annotations: &'a [(Selection, Annotation)],
276 placeholder: Option<&'a SharedString>,
279 caption: Caption,
280 toggle: Option<&'a Toggle>,
283 copy: CopyButton,
284 base: Option<&'a Path>,
285 highlight: crate::HighlightPaint,
286}
287
288impl<'a> Overlay<'a> {
289 fn at(self, part: Part) -> Self {
290 Self { part, ..self }
291 }
292
293 fn here(&self) -> Cursor {
294 Cursor::new(self.block, self.part, 0)
295 }
296
297 fn caret_painted(&self) -> Option<usize> {
303 self.caret_on.then(|| self.caret()).flatten()
304 }
305
306 fn caret(&self) -> Option<usize> {
308 self.selection
309 .map(|selection| selection.head)
310 .filter(|head| head.block == self.block && head.part == self.part)
311 .map(|head| head.offset)
312 }
313
314 fn selected(&self, len: usize) -> Option<Range<usize>> {
316 self.clip(self.selection?, len)
317 }
318
319 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
322 self.annotations
323 .iter()
324 .filter_map(|(range, kind)| {
325 Some((self.clip(*range, len)?, kind.wash(theme, self.highlight)))
326 })
327 .collect()
328 }
329
330 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
336 if selection.is_collapsed() {
337 return None;
338 }
339 let (start, end) = selection.ordered();
340 let here = self.here();
341 let (first, last) = (
342 Cursor::new(start.block, start.part, 0),
343 Cursor::new(end.block, end.part, 0),
344 );
345 if here < first || here > last {
346 return None;
347 }
348 let from = if here == first { start.offset } else { 0 };
349 let to = if here == last { end.offset } else { len };
350 (from < to).then_some(from..to.min(len))
351 }
352
353 fn covers_block(&self) -> bool {
357 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
358 return false;
359 };
360 let (start, end) = selection.ordered();
361 start.block < self.block && self.block < end.block
362 }
363}
364
365pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
371 if url.contains("://") {
372 return SharedString::from(url.to_string()).into();
373 }
374 match base {
377 Some(base) => base.join(url).into(),
378 None => std::path::PathBuf::from(url).into(),
379 }
380}
381
382pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
384 let doc = crate::parse_with(source, &crate::Marks::of(cx));
385 render(&doc, Caption::default(), window, cx)
386}
387
388pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
390 render_with(
391 doc,
392 Editing {
393 caption,
394 ..Editing::default()
395 },
396 window,
397 cx,
398 )
399}
400
401pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
409 let Editing {
410 selection,
411 caret_on,
412 layouts,
413 annotations,
414 placeholder,
415 caption,
416 typography,
417 toggle,
418 copy,
419 base,
420 keep,
421 scroll,
422 } = editing;
423 let theme = Theme::of(cx).clone();
426 let typography = typography.unwrap_or_else(|| Typography::of(cx));
427 let highlight = crate::marks::highlight_paint_of(cx);
428 let gaps: Vec<Pixels> = doc
429 .blocks
430 .iter()
431 .enumerate()
432 .map(|(ix, block)| {
433 px(match doc.blocks.get(ix.wrapping_sub(1)) {
434 None => 0.0,
435 Some(previous) if tight(previous, block) => LIST_GAP,
436 Some(_) => BLOCK_GAP,
437 })
438 })
439 .collect();
440
441 let Some(layouts) = layouts else {
442 let mut column = div().flex().flex_col();
443 for (ix, block) in doc.blocks.iter().enumerate() {
444 let overlay = Overlay {
445 block: ix,
446 part: Part::Body,
447 selection,
448 caret_on,
449 layouts: None,
450 annotations,
451 placeholder: placeholder.as_ref(),
452 caption,
453 toggle: toggle.as_ref(),
454 copy,
455 base,
456 highlight,
457 };
458 column = column
459 .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
460 }
461 return column.into_any_element();
462 };
463
464 let keys: Vec<u64> = doc
465 .blocks
466 .iter()
467 .map(|block| block_key(block, &typography))
468 .collect();
469 layouts.prune(&keys);
470 let guesses: Vec<Guess> = doc
471 .blocks
472 .iter()
473 .map(|block| guess(block, &typography))
474 .collect();
475 let mut kept = keep.to_vec();
476 kept.extend(selection.map(|selection| selection.head.block));
477 kept.extend(selection.map(|selection| selection.anchor.block));
478 let owned = Owned {
479 blocks: doc.blocks.clone(),
480 selection,
481 caret_on,
482 layouts: layouts.clone(),
483 annotations: annotations.to_vec(),
484 placeholder,
485 caption,
486 toggle,
487 copy,
488 base: base.map(Path::to_path_buf),
489 highlight,
490 typography,
491 theme,
492 };
493 Column {
494 layouts: layouts.clone(),
495 keys: keys.into(),
496 gaps: gaps.into(),
497 guesses: guesses.into(),
498 keep: kept,
499 scroll: scroll.cloned(),
500 build: Box::new(move |ix, window, cx| {
501 let overlay = Overlay {
502 block: ix,
503 part: Part::Body,
504 selection: owned.selection,
505 caret_on: owned.caret_on,
506 layouts: Some(&owned.layouts),
507 annotations: &owned.annotations,
508 placeholder: owned.placeholder.as_ref(),
509 caption: owned.caption,
510 toggle: owned.toggle.as_ref(),
511 copy: owned.copy,
512 base: owned.base.as_deref(),
513 highlight: owned.highlight,
514 };
515 block_box(
516 &owned.blocks[ix],
517 overlay,
518 &owned.typography,
519 &owned.theme,
520 window,
521 cx,
522 )
523 .into_any_element()
524 }),
525 }
526 .into_any_element()
527}
528
529struct Owned {
531 blocks: Vec<Block>,
532 selection: Option<Selection>,
533 caret_on: bool,
534 layouts: BlockLayouts,
535 annotations: Vec<(Selection, Annotation)>,
536 placeholder: Option<SharedString>,
537 caption: Caption,
538 toggle: Option<Toggle>,
539 copy: CopyButton,
540 base: Option<std::path::PathBuf>,
541 highlight: crate::HighlightPaint,
542 typography: Typography,
543 theme: Theme,
544}
545
546fn block_box(
549 block: &Block,
550 overlay: Overlay,
551 typography: &Typography,
552 theme: &Theme,
553 window: &mut Window,
554 cx: &mut App,
555) -> gpui::Div {
556 let ix = overlay.block;
557 let frame = overlay.layouts.map(|layouts| {
560 let layouts = layouts.clone();
561 canvas(
562 move |bounds, _, _| layouts.record_block(ix, bounds),
563 |_, _, _, _| (),
564 )
565 .absolute()
566 .size_full()
567 });
568 div()
574 .w_full()
575 .pl(px(block.indent as f32 * INDENT_WIDTH))
576 .child(
577 div()
578 .w_full()
579 .relative()
580 .children(frame)
581 .when(overlay.covers_block() && block.opaque(), |el| {
585 el.rounded(px(4.0)).bg(theme.selection)
586 })
587 .child(block_element(block, overlay, typography, theme, window, cx)),
588 )
589}
590
591fn block_key(block: &Block, typography: &Typography) -> u64 {
594 let mut hasher = DefaultHasher::new();
595 block.hash(&mut hasher);
596 typography.body.size().to_bits().hash(&mut hasher);
597 typography.body.line_height().to_bits().hash(&mut hasher);
598 hasher.finish()
599}
600
601fn guess(block: &Block, typography: &Typography) -> Guess {
603 let body = px(typography.body.line_height());
604 let prose = |chars: usize, line: Pixels| Guess {
605 chars,
606 line,
607 rows: 0,
608 extra: px(0.0),
609 indent: px(0.0),
610 };
611 let guess = match &block.kind {
612 BlockKind::Paragraph(text)
613 | BlockKind::Bullet(text)
614 | BlockKind::Ordered { text, .. }
615 | BlockKind::Task { text, .. }
616 | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
617 BlockKind::Heading { level, text } => prose(
618 text.text.len(),
619 px(typography.heading(*level).line_height()),
620 ),
621 BlockKind::Code { code, .. } => Guess {
622 rows: code.text.lines().count().max(1),
623 line: px(typography.code.line_height()),
624 extra: px(2.0 * CODE_PADDING_Y) + body,
625 ..prose(0, body)
626 },
627 BlockKind::Table { rows, .. } => Guess {
628 rows: rows.len() + 1,
629 extra: px(8.0) * (rows.len() + 1) as f32,
630 ..prose(0, body)
631 },
632 BlockKind::Image { alt, .. } => Guess {
633 extra: px(240.0),
634 ..prose(alt.text.len(), px(typography.caption.line_height()))
635 },
636 BlockKind::Bookmark { .. } => Guess {
637 extra: body * 3.0,
638 ..prose(0, body)
639 },
640 BlockKind::Rule => Guess {
641 extra: body,
642 ..prose(0, px(0.0))
643 },
644 };
645 Guess {
646 indent: px(block.indent as f32 * INDENT_WIDTH),
647 ..guess
648 }
649}
650
651fn tight(previous: &Block, next: &Block) -> bool {
653 let marker = |block: &Block| {
654 matches!(
655 block.kind,
656 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
657 )
658 };
659 marker(previous) && (marker(next) || next.indent > previous.indent)
660}
661
662fn block_element(
663 block: &Block,
664 overlay: Overlay,
665 typography: &Typography,
666 theme: &Theme,
667 window: &mut Window,
668 cx: &mut App,
669) -> AnyElement {
670 let body = overlay.at(Part::Body);
671 match &block.kind {
672 BlockKind::Paragraph(text) => text_element(
673 text,
674 typography.body.size(),
675 typography.body.line_height(),
676 FontWeight::NORMAL,
677 body,
678 theme,
679 cx,
680 ),
681 BlockKind::Heading { level, text } => {
682 let heading = typography.heading(*level);
683 text_element(
684 text,
685 heading.size(),
686 heading.line_height(),
687 heading.weight,
688 body,
689 theme,
690 cx,
691 )
692 }
693 BlockKind::Bullet(text) => {
694 marker_row(disc(typography, theme), text, body, typography, theme, cx)
695 }
696 BlockKind::Ordered { number, text } => marker_row(
697 div()
698 .flex_none()
699 .w(px(MARKER_WIDTH))
700 .text_size(px(typography.body.size()))
701 .line_height(px(typography.body.line_height()))
702 .text_color(theme.text_muted)
703 .child(SharedString::from(format!("{number}.")))
704 .into_any_element(),
705 text,
706 body,
707 typography,
708 theme,
709 cx,
710 ),
711 BlockKind::Task { checked, text } => marker_row(
712 checkbox(*checked, overlay, typography, theme),
713 text,
714 body,
715 typography,
716 theme,
717 cx,
718 ),
719 BlockKind::Quote { kind, text } => div()
720 .border_l_2()
721 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
722 .pl(px(12.0))
723 .pr(px(10.0))
724 .py(px(2.0))
725 .text_color(theme.text_muted)
726 .children(kind.map(|kind| {
727 div()
728 .pb(px(2.0))
729 .text_size(px(typography.body.size()))
730 .line_height(px(typography.body.line_height()))
731 .font_weight(FontWeight::SEMIBOLD)
732 .text_color(alert_color(kind, theme))
733 .child(kind.label())
734 }))
735 .child(text_element(
736 text,
737 typography.body.size(),
738 typography.body.line_height(),
739 FontWeight::NORMAL,
740 body,
741 theme,
742 cx,
743 ))
744 .into_any_element(),
745 BlockKind::Code { language, code } => {
746 let overlay = overlay.at(Part::Code);
747 let painted = overlay
751 .caret()
752 .is_none()
753 .then(|| block::render(language.as_deref(), &code.text, window, cx))
754 .flatten();
755 match painted {
756 Some(element) => div()
759 .when(overlay.covers_block(), |el| {
760 el.rounded(px(4.0)).bg(theme.selection)
761 })
762 .child(element)
763 .into_any_element(),
764 None => code_block(
765 language.as_deref(),
766 &code.text,
767 overlay,
768 typography,
769 theme,
770 window,
771 cx,
772 ),
773 }
774 }
775 BlockKind::Image { url, alt, width } => {
776 image(url, alt, *width, overlay, typography, theme, cx)
777 }
778 BlockKind::Bookmark { url, form } => {
779 bookmark(overlay.block, url, *form, typography, theme, cx)
780 }
781 BlockKind::Table {
782 align,
783 header,
784 rows,
785 } => table(align, header, rows, overlay, typography, theme, window, cx),
786 BlockKind::Rule => div()
787 .h(px(1.0))
788 .w_full()
789 .bg(theme.border)
790 .into_any_element(),
791 }
792}
793
794fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
796 div()
797 .flex_none()
798 .w(px(MARKER_WIDTH))
799 .h(px(typography.body.line_height()))
800 .flex()
801 .items_center()
802 .child(
803 div()
804 .ml(px(1.0))
805 .w(px(5.0))
806 .h(px(5.0))
807 .rounded_full()
808 .bg(theme.text_faint),
809 )
810 .into_any_element()
811}
812
813fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
814 let ix = overlay.block;
815 let mut box_ = div()
816 .relative()
817 .w(px(13.0))
818 .h(px(13.0))
819 .rounded(px(3.5))
820 .border_1()
821 .flex()
822 .items_center()
823 .justify_center();
824 box_ = if checked {
825 box_.bg(theme.solid)
826 .border_color(theme.solid)
827 .text_style(TextStyle::Caption)
828 .text_color(theme.on_solid)
829 .child("✓")
830 } else {
831 box_.border_color(theme.border_strong)
832 };
833 box_ = box_.children(overlay.layouts.map(|layouts| {
836 let layouts = layouts.clone();
837 canvas(
838 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
839 |_, _, _, _| (),
840 )
841 .absolute()
842 .size_full()
843 }));
844 if overlay.toggle.is_some() {
847 box_ = box_.cursor_pointer();
848 }
849 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
850 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
851 cx.stop_propagation();
855 toggle(ix, window, cx);
856 });
857 }
858
859 div()
860 .flex_none()
861 .w(px(MARKER_WIDTH))
862 .h(px(typography.body.line_height()))
863 .flex()
864 .items_center()
865 .child(box_)
866 .into_any_element()
867}
868
869fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
871 match kind {
872 QuoteKind::Note => theme.accent,
873 QuoteKind::Tip => theme.success,
874 QuoteKind::Important => theme.busy,
875 QuoteKind::Warning => theme.warning,
876 QuoteKind::Caution => theme.danger,
877 }
878}
879
880fn marker_row(
881 marker: AnyElement,
882 text: &Text,
883 overlay: Overlay,
884 typography: &Typography,
885 theme: &Theme,
886 cx: &App,
887) -> AnyElement {
888 div()
889 .flex()
890 .flex_row()
891 .gap(px(MARKER_GAP))
892 .child(marker)
893 .child(div().flex_1().min_w_0().child(text_element(
894 text,
895 typography.body.size(),
896 typography.body.line_height(),
897 FontWeight::NORMAL,
898 overlay,
899 theme,
900 cx,
901 )))
902 .into_any_element()
903}