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 sync::Arc,
19};
20use ui::AppExt as _;
21use ui::widgets::Layout as _;
22
23use gpui::{
24 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
25 ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
26 StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
27 canvas, div, font, img, point, prelude::*, px, quad, size,
28};
29use theme::{TextStyle, Theme, Typeset};
30
31use crate::{
32 app::{preview, typography::Typography},
33 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Run, Text, quote_runs},
34 fence::{
35 self, CODE_PADDING_X, CODE_PADDING_Y, copy_button, fence_band, fence_note, fence_panel,
36 },
37 select::{Affinity, Cursor, Selection},
38};
39
40mod code;
42mod column;
43mod layouts;
44mod media;
45pub use media::hover_bookmark;
46pub mod selectable;
47mod source;
48mod table;
49mod text;
50
51use code::*;
52use column::*;
53pub use layouts::*;
54use media::*;
55pub use source::*;
56use table::*;
57pub use text::*;
58
59const BLOCK_GAP: f32 = 12.0;
60const LIST_GAP: f32 = 4.0;
61const INDENT_WIDTH: f32 = 22.0;
63const MARKER_WIDTH: f32 = 18.0;
65const MARKER_GAP: f32 = 8.0;
66pub const PLAIN_LANGUAGE: &str = "Plain";
69const CARET_WIDTH: f32 = 1.5;
72const INLINE_CODE_RADIUS: f32 = 4.5;
75const INLINE_CODE_PAD_X: f32 = 2.0;
76const INLINE_CODE_INSET_Y: f32 = 2.0;
77const MENTION_CARD_WIDTH: f32 = 320.0;
79const CARD_HEIGHT: f32 = 116.0;
83const CARD_IMAGE_WIDTH: f32 = 180.0;
84const CARD_COVER_HEIGHT: f32 = 200.0;
85const CARD_PADDING: f32 = 14.0;
86const CARD_BORDER: f32 = 1.0;
87const CARD_ICON: f32 = 16.0;
88const CARD_COVER: f32 = 44.0;
89const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
91const CAPTION_GAP: f32 = 4.0;
92const IMAGE_EMPTY: &str = "Add an image";
94const CAPTION_HINT: &str = "Write a caption";
95const TABLE_CELL_PADDING: f32 = 12.0;
98pub const TABLE_CONTROL_SIZE: f32 = 16.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]
150 Open,
151 Resolved,
153 Active,
155 Highlight(crate::HighlightColor),
158 Match,
160 Current,
162}
163
164impl Annotation {
165 fn wash(
166 self,
167 theme: &Theme,
168 highlight: crate::HighlightPaint,
169 find: crate::FindPaint,
170 comment: crate::CommentPaint,
171 ) -> Hsla {
172 match self {
173 Self::Open => comment(theme).open,
174 Self::Resolved => comment(theme).resolved,
175 Self::Active => comment(theme).active,
176 Self::Highlight(color) => highlight(color, theme),
177 Self::Match => find(theme).0,
178 Self::Current => find(theme).1,
179 }
180 }
181}
182
183pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
188
189pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
191
192pub type OnImageMenu = Rc<dyn Fn(usize, Point<Pixels>, &mut Window, &mut App)>;
195
196pub type OnJump = Rc<dyn Fn(usize, &mut Window, &mut App)>;
199
200pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
204
205pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
207
208pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
210
211pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
214
215#[derive(Clone)]
218pub struct FenceHost {
219 pub rewrite: OnRewrite,
222 pub leave: OnLeave,
225 pub resize: Option<OnResize>,
228}
229
230pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
233
234gpui::actions!(
235 markdown,
236 [
237 LeaveBlock
239 ]
240);
241
242#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
244pub enum ImageOverlayCorner {
245 TopLeft,
246 TopRight,
247 BottomLeft,
248 #[default]
249 BottomRight,
250}
251
252#[derive(Clone)]
258pub enum Toggle {
259 Handled(OnToggle),
262 HitTested,
268}
269
270#[derive(Clone)]
276pub struct Editing<'a> {
277 pub selection: Option<Selection>,
280 pub caret_on: bool,
283 pub layouts: Option<&'a BlockLayouts>,
289 pub annotations: &'a [(Selection, Annotation)],
291 pub placeholder: Option<SharedString>,
293 pub caption: Caption,
294 pub typography: Option<Typography>,
298 pub toggle: Option<Toggle>,
301 pub image: Option<OnImage>,
305 pub image_menu: Option<OnImageMenu>,
310 pub image_overlay: Option<ImageOverlay>,
313 pub image_overlay_corner: ImageOverlayCorner,
315 pub fence: Option<FenceHost>,
318 pub sizing: Option<(usize, u32)>,
321 pub table_controls: bool,
323 pub copy: CopyButton,
325 pub base: Option<&'a Path>,
328 pub keep: &'a [usize],
333 pub scroll: Option<&'a gpui::ScrollHandle>,
337 pub jump: Option<OnJump>,
341}
342
343impl Default for Editing<'_> {
344 fn default() -> Self {
345 Self {
346 selection: None,
347 caret_on: true,
350 layouts: None,
351 annotations: &[],
352 placeholder: None,
353 caption: Caption::default(),
354 typography: None,
355 toggle: None,
356 image: None,
357 image_menu: None,
358 image_overlay: None,
359 image_overlay_corner: ImageOverlayCorner::BottomRight,
360 fence: None,
361 sizing: None,
362 table_controls: false,
363 copy: CopyButton::default(),
364 base: None,
365 keep: &[],
366 scroll: None,
367 jump: None,
368 }
369 }
370}
371
372#[derive(Clone, Copy)]
378struct Overlay<'a> {
379 block: usize,
380 part: Part,
381 selection: Option<Selection>,
382 caret_on: bool,
383 caret_shape: ui::input::CaretShape,
384 caret_height: ui::input::CaretHeight,
385 caret_inactive: ui::input::InactiveCaret,
386 window_active: bool,
388 layouts: Option<&'a BlockLayouts>,
389 annotations: &'a [(Selection, Annotation)],
391 placeholder: Option<&'a SharedString>,
394 caption: Caption,
395 toggle: Option<&'a Toggle>,
398 image: Option<&'a OnImage>,
399 image_menu: Option<&'a OnImageMenu>,
400 image_overlay: Option<&'a ImageOverlay>,
401 image_overlay_corner: ImageOverlayCorner,
402 fence: Option<&'a FenceHost>,
403 sizing: Option<(usize, u32)>,
404 table_controls: bool,
405 copy: CopyButton,
406 base: Option<&'a Path>,
407 highlight: crate::HighlightPaint,
408 find: crate::FindPaint,
409 comment: crate::CommentPaint,
410 jump: Option<&'a crate::app::link::Jump>,
411}
412
413fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
417 let ix = overlay.block;
418 let mut parts = Vec::new();
419 if let Some(layouts) = overlay.layouts {
420 let layouts = layouts.clone();
421 parts.push(
422 canvas(
423 move |bounds, _, _| layouts.record_painted(ix, bounds),
424 |_, _, _, _| (),
425 )
426 .absolute()
427 .size_full()
428 .into_any_element(),
429 );
430 }
431 if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
432 let dragging = overlay.held_height().is_some();
433 parts.push(
438 div()
439 .absolute()
440 .left_0()
441 .right_0()
442 .bottom_0()
443 .flex()
444 .justify_center()
445 .child(
446 theme
447 .grip_handle()
448 .id(ElementId::named_usize("painted-grip", ix))
449 .debug_selector(|| "painted-grip".into())
450 .w(px(PAINTED_GRIP_WIDTH))
451 .when(!dragging, |el| {
452 el.invisible()
453 .group_hover(painted_group(ix), |el| el.visible())
454 })
455 .on_mouse_down(MouseButton::Left, move |event, window, cx| {
456 cx.stop_propagation();
457 resize(ix, event.position.y, window, cx);
458 }),
459 )
460 .into_any_element(),
461 );
462 }
463 parts
464}
465
466const PAINTED_GRIP_WIDTH: f32 = 120.0;
468
469fn painted_group(ix: usize) -> SharedString {
470 SharedString::from(format!("painted-{ix}"))
471}
472
473impl<'a> Overlay<'a> {
474 fn at(self, part: Part) -> Self {
475 Self { part, ..self }
476 }
477
478 fn held_height(&self) -> Option<u32> {
480 self.sizing
481 .filter(|(ix, _)| *ix == self.block)
482 .map(|(_, height)| height)
483 }
484
485 fn here(&self) -> Cursor {
486 Cursor::new(self.block, self.part, 0)
487 }
488
489 fn caret_painted(&self) -> Option<usize> {
496 let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
497 let shown = self
498 .caret_shape
499 .shown(self.window_active, self.caret_inactive);
500 (self.caret_on && collapsed && shown)
501 .then(|| self.caret())
502 .flatten()
503 }
504
505 fn affinity(&self) -> Affinity {
507 self.selection
508 .map_or_else(Affinity::default, |selection| selection.affinity)
509 }
510
511 fn covered(&self, text: &str, offset: usize) -> Option<Range<usize>> {
514 (self.affinity() == Affinity::Downstream)
515 .then(|| glyph_at(text, offset))
516 .flatten()
517 }
518
519 fn caret_hollow(&self) -> bool {
520 !self.window_active
521 }
522
523 fn caret(&self) -> Option<usize> {
525 self.selection
526 .map(|selection| selection.head)
527 .filter(|head| head.block == self.block && head.part == self.part)
528 .map(|head| head.offset)
529 }
530
531 fn selected(&self, len: usize) -> Option<Range<usize>> {
533 self.clip(self.selection?, len)
534 }
535
536 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
539 self.annotations
540 .iter()
541 .filter_map(|(range, kind)| {
542 Some((
543 self.clip(*range, len)?,
544 kind.wash(theme, self.highlight, self.find, self.comment),
545 ))
546 })
547 .collect()
548 }
549
550 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
556 if selection.is_collapsed() {
557 return None;
558 }
559 let (start, end) = selection.ordered();
560 let here = self.here();
561 let (first, last) = (
562 Cursor::new(start.block, start.part, 0),
563 Cursor::new(end.block, end.part, 0),
564 );
565 if here < first || here > last {
566 return None;
567 }
568 let from = if here == first { start.offset } else { 0 };
569 let to = if here == last { end.offset } else { len };
570 (from < to).then_some(from..to.min(len))
571 }
572
573 fn covers_block(&self) -> bool {
577 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
578 return false;
579 };
580 let (start, end) = selection.ordered();
581 start.block < self.block && self.block < end.block
582 }
583}
584
585pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
591 if url.contains("://") {
592 return SharedString::from(url.to_string()).into();
593 }
594 match base {
597 Some(base) => base.join(url).into(),
598 None => std::path::PathBuf::from(url).into(),
599 }
600}
601
602pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
604 let doc = crate::parse_with(source, &cx.marks());
605 render(&doc, Caption::default(), window, cx)
606}
607
608pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
610 render_with(
611 doc,
612 Editing {
613 caption,
614 ..Editing::default()
615 },
616 window,
617 cx,
618 )
619}
620
621pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
629 let Editing {
630 selection,
631 caret_on,
632 layouts,
633 annotations,
634 placeholder,
635 caption,
636 typography,
637 toggle,
638 image,
639 image_menu,
640 image_overlay,
641 image_overlay_corner,
642 fence,
643 sizing,
644 table_controls,
645 copy,
646 base,
647 keep,
648 scroll,
649 jump,
650 } = editing;
651 let theme = Theme::of(cx).clone();
654 let typography = typography.unwrap_or_else(|| cx.typography());
655 let highlight = crate::paint::marks::highlight_paint_of(cx);
656 let find = crate::paint::find::find_paint_of(cx);
657 let comment = crate::paint::comment::comment_paint_of(cx);
658 let jump = jump.map(|to| crate::app::link::Jump::new(doc, to));
659 let gaps: Vec<Pixels> = doc
660 .blocks
661 .iter()
662 .enumerate()
663 .map(|(ix, block)| {
664 px(match doc.blocks.get(ix.wrapping_sub(1)) {
665 None => 0.0,
666 Some(previous) if tight(previous, block) => LIST_GAP,
667 Some(_) => BLOCK_GAP,
668 })
669 })
670 .collect();
671 let runs = quote_runs(doc.blocks.iter().map(|block| (block.quote, block.indent)));
672
673 let Some(layouts) = layouts else {
674 let mut column = div().flex().flex_col();
675 for (ix, block) in doc.blocks.iter().enumerate() {
676 let overlay = Overlay {
677 block: ix,
678 part: Part::Body,
679 selection,
680 caret_on,
681 caret_shape: cx.caret_shape(),
682 caret_height: cx.caret_height(),
683 caret_inactive: cx.inactive_caret(),
684 window_active: window.is_window_active(),
685 layouts: None,
686 annotations,
687 placeholder: placeholder.as_ref(),
688 caption,
689 toggle: toggle.as_ref(),
690 image: image.as_ref(),
691 image_menu: image_menu.as_ref(),
692 image_overlay: image_overlay.as_ref(),
693 image_overlay_corner,
694 fence: fence.as_ref(),
695 sizing,
696 table_controls,
697 copy,
698 base,
699 highlight,
700 find,
701 comment,
702 jump: jump.as_ref(),
703 };
704 let quote = Quote {
705 run: runs[ix],
706 gap: gaps[ix],
707 };
708 column = column.child(
709 block_box(block, quote, overlay, &typography, &theme, window, cx).mt(gaps[ix]),
710 );
711 }
712 return column.into_any_element();
713 };
714
715 let keys: Vec<u64> = doc
716 .blocks
717 .iter()
718 .zip(&runs)
719 .map(|(block, run)| block_key(block, *run, &typography, table_controls))
720 .collect();
721 layouts.prune(&keys);
722 let guesses: Vec<Guess> = doc
723 .blocks
724 .iter()
725 .zip(&runs)
726 .map(|(block, run)| {
727 let mut guess = guess(block, *run, &typography);
728 if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
729 guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
730 }
731 guess
732 })
733 .collect();
734 let mut kept = keep.to_vec();
735 kept.extend(selection.map(|selection| selection.head.block));
736 kept.extend(selection.map(|selection| selection.anchor.block));
737 let owned = Owned {
738 blocks: doc.blocks.clone(),
739 runs,
740 gaps: gaps.clone(),
741 selection,
742 caret_on,
743 layouts: layouts.clone(),
744 annotations: annotations.to_vec(),
745 placeholder,
746 caption,
747 toggle,
748 image,
749 image_menu,
750 image_overlay,
751 image_overlay_corner,
752 fence,
753 sizing,
754 table_controls,
755 copy,
756 base: base.map(Path::to_path_buf),
757 highlight,
758 find,
759 comment,
760 jump,
761 typography,
762 theme,
763 };
764 Column {
765 layouts: layouts.clone(),
766 keys: keys.into(),
767 gaps: gaps.into(),
768 guesses: guesses.into(),
769 keep: kept,
770 scroll: scroll.cloned(),
771 item_of: Rc::new(|at| at.block),
772 build: Box::new(move |ix, window, cx| {
773 let overlay = Overlay {
774 block: ix,
775 part: Part::Body,
776 selection: owned.selection,
777 caret_on: owned.caret_on,
778 caret_shape: cx.caret_shape(),
779 caret_height: cx.caret_height(),
780 caret_inactive: cx.inactive_caret(),
781 window_active: window.is_window_active(),
782 layouts: Some(&owned.layouts),
783 annotations: &owned.annotations,
784 placeholder: owned.placeholder.as_ref(),
785 caption: owned.caption,
786 toggle: owned.toggle.as_ref(),
787 image: owned.image.as_ref(),
788 image_menu: owned.image_menu.as_ref(),
789 image_overlay: owned.image_overlay.as_ref(),
790 image_overlay_corner: owned.image_overlay_corner,
791 fence: owned.fence.as_ref(),
792 sizing: owned.sizing,
793 table_controls: owned.table_controls,
794 copy: owned.copy,
795 base: owned.base.as_deref(),
796 highlight: owned.highlight,
797 find: owned.find,
798 comment: owned.comment,
799 jump: owned.jump.as_ref(),
800 };
801 let quote = Quote {
802 run: owned.runs[ix],
803 gap: owned.gaps[ix],
804 };
805 block_box(
806 &owned.blocks[ix],
807 quote,
808 overlay,
809 &owned.typography,
810 &owned.theme,
811 window,
812 cx,
813 )
814 .into_any_element()
815 }),
816 }
817 .into_any_element()
818}
819
820struct Owned {
822 blocks: Vec<Block>,
823 runs: Vec<Option<Run>>,
824 gaps: Vec<Pixels>,
825 selection: Option<Selection>,
826 caret_on: bool,
827 layouts: BlockLayouts,
828 annotations: Vec<(Selection, Annotation)>,
829 placeholder: Option<SharedString>,
830 caption: Caption,
831 toggle: Option<Toggle>,
832 image: Option<OnImage>,
833 image_menu: Option<OnImageMenu>,
834 image_overlay: Option<ImageOverlay>,
835 image_overlay_corner: ImageOverlayCorner,
836 fence: Option<FenceHost>,
837 sizing: Option<(usize, u32)>,
838 table_controls: bool,
839 copy: CopyButton,
840 base: Option<std::path::PathBuf>,
841 highlight: crate::HighlightPaint,
842 find: crate::FindPaint,
843 comment: crate::CommentPaint,
844 jump: Option<crate::app::link::Jump>,
845 typography: Typography,
846 theme: Theme,
847}
848
849#[derive(Clone, Copy)]
852struct Quote {
853 run: Option<Run>,
854 gap: Pixels,
855}
856
857const QUOTE_INSET: f32 = 12.0;
859const QUOTE_BAR: f32 = 2.0;
860
861fn block_box(
864 block: &Block,
865 quote: Quote,
866 overlay: Overlay,
867 typography: &Typography,
868 theme: &Theme,
869 window: &mut Window,
870 cx: &mut App,
871) -> gpui::Div {
872 let ix = overlay.block;
873 let frame = overlay.layouts.map(|layouts| {
876 let layouts = layouts.clone();
877 canvas(
878 move |bounds, _, _| layouts.record_block(ix, bounds),
879 |_, _, _, _| (),
880 )
881 .absolute()
882 .size_full()
883 });
884 div()
890 .w_full()
891 .font_family(theme.font_body.clone())
894 .pl(px(block.indent as f32 * INDENT_WIDTH))
895 .child(
896 div()
897 .w_full()
898 .relative()
899 .children(frame)
900 .when(overlay.covers_block() && block.opaque(), |el| {
904 el.rounded(px(4.0)).bg(theme.selection)
905 })
906 .child(quoted(
907 block,
908 quote,
909 block_element(block, overlay, typography, theme, window, cx),
910 typography,
911 theme,
912 )),
913 )
914}
915
916fn quoted(
922 block: &Block,
923 quote: Quote,
924 element: AnyElement,
925 typography: &Typography,
926 theme: &Theme,
927) -> AnyElement {
928 let (Some(run), Some(quoted)) = (quote.run, block.quote) else {
929 return element;
930 };
931 let color = quoted
932 .alert
933 .map_or(theme.border_strong, |alert| alert_color(alert, theme));
934 let label = quoted.alert.filter(|_| run.first).map(|alert| {
935 div()
936 .pb(px(2.0))
937 .text_size(px(typography.body.size()))
938 .line_height(px(typography.body.line_height()))
939 .font_weight(FontWeight::SEMIBOLD)
940 .text_color(color)
941 .child(alert.label())
942 });
943 let bar = div()
944 .absolute()
945 .left(px(
946 -((block.indent - run.level.min(block.indent)) as f32) * INDENT_WIDTH
947 ))
948 .top(if run.first { px(0.0) } else { -quote.gap })
949 .bottom_0()
950 .w(px(QUOTE_BAR))
951 .bg(color);
952 div()
953 .child(bar)
954 .pl(px(QUOTE_BAR + QUOTE_INSET))
955 .pr(px(10.0))
956 .py(px(2.0))
957 .text_color(theme.text_muted)
958 .children(label)
959 .child(element)
960 .into_any_element()
961}
962
963fn block_key(
966 block: &Block,
967 run: Option<Run>,
968 typography: &Typography,
969 table_controls: bool,
970) -> u64 {
971 let mut hasher = DefaultHasher::new();
972 table_controls.hash(&mut hasher);
973 block.hash(&mut hasher);
974 run.map(|run| run.first).hash(&mut hasher);
975 typography.body.size().to_bits().hash(&mut hasher);
976 typography.body.line_height().to_bits().hash(&mut hasher);
977 hasher.finish()
978}
979
980fn guess(block: &Block, run: Option<Run>, typography: &Typography) -> Guess {
982 let body = px(typography.body.line_height());
983 let prose = |chars: usize, line: Pixels| Guess {
984 chars,
985 line,
986 rows: 0,
987 extra: px(0.0),
988 indent: px(0.0),
989 };
990 let guess = match &block.kind {
991 BlockKind::Paragraph(text)
992 | BlockKind::Bullet(text)
993 | BlockKind::Ordered { text, .. }
994 | BlockKind::Task { text, .. } => prose(text.text.len(), body),
995 BlockKind::Heading { level, text } => prose(
996 text.text.len(),
997 px(typography.heading(*level).line_height()),
998 ),
999 BlockKind::Code { code, .. } => Guess {
1000 rows: code.text.lines().count().max(1),
1001 line: px(typography.code.line_height()),
1002 extra: px(2.0 * CODE_PADDING_Y) + body,
1003 ..prose(0, body)
1004 },
1005 BlockKind::Table { rows, .. } => Guess {
1006 rows: rows.len() + 1,
1007 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
1008 ..prose(0, body)
1009 },
1010 BlockKind::Image { alt, .. } => Guess {
1011 extra: px(240.0),
1012 ..prose(alt.text.len(), px(typography.caption.line_height()))
1013 },
1014 BlockKind::Bookmark { .. } => Guess {
1015 extra: body * 3.0,
1016 ..prose(0, body)
1017 },
1018 BlockKind::Rule => Guess {
1019 extra: body,
1020 ..prose(0, px(0.0))
1021 },
1022 };
1023 let quoted = run.is_some();
1024 let label = run.is_some_and(|run| run.first) && block.quote.is_some_and(|q| q.alert.is_some());
1025 Guess {
1026 indent: px(block.indent as f32 * INDENT_WIDTH)
1027 + if quoted {
1028 px(QUOTE_BAR + QUOTE_INSET + 10.0)
1029 } else {
1030 px(0.0)
1031 },
1032 extra: guess.extra
1033 + if quoted { px(4.0) } else { px(0.0) }
1034 + if label { body + px(2.0) } else { px(0.0) },
1035 ..guess
1036 }
1037}
1038
1039fn tight(previous: &Block, next: &Block) -> bool {
1041 let marker = |block: &Block| {
1042 matches!(
1043 block.kind,
1044 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
1045 )
1046 };
1047 marker(previous) && (marker(next) || next.indent > previous.indent)
1048}
1049
1050fn block_element(
1051 block: &Block,
1052 overlay: Overlay,
1053 typography: &Typography,
1054 theme: &Theme,
1055 window: &mut Window,
1056 cx: &mut App,
1057) -> AnyElement {
1058 let body = overlay.at(Part::Body);
1059 match &block.kind {
1060 BlockKind::Paragraph(text) => text_element(
1061 text,
1062 typography.body.size(),
1063 typography.body.line_height(),
1064 FontWeight::NORMAL,
1065 body,
1066 theme,
1067 cx,
1068 ),
1069 BlockKind::Heading { level, text } => {
1070 let heading = typography.heading(*level);
1071 text_element(
1072 text,
1073 heading.size(),
1074 heading.line_height(),
1075 heading.weight,
1076 body,
1077 theme,
1078 cx,
1079 )
1080 }
1081 BlockKind::Bullet(text) => {
1082 marker_row(disc(typography, theme), text, body, typography, theme, cx)
1083 }
1084 BlockKind::Ordered { number, text } => marker_row(
1085 div()
1086 .flex_none()
1087 .w(px(MARKER_WIDTH))
1088 .text_size(px(typography.body.size()))
1089 .line_height(px(typography.body.line_height()))
1090 .text_color(theme.text_muted)
1091 .child(SharedString::from(format!("{number}.")))
1092 .into_any_element(),
1093 text,
1094 body,
1095 typography,
1096 theme,
1097 cx,
1098 ),
1099 BlockKind::Task { checked, text } => marker_row(
1100 checkbox(*checked, overlay, typography, theme),
1101 text,
1102 body,
1103 typography,
1104 theme,
1105 cx,
1106 ),
1107 BlockKind::Code {
1108 language,
1109 code,
1110 height,
1111 } => {
1112 let overlay = overlay.at(Part::Code);
1113 let height = overlay.held_height().or(*height);
1114 let fence = overlay
1118 .caret()
1119 .is_none()
1120 .then(|| {
1121 Some(fence::Fence {
1122 language: language.as_deref()?,
1123 code: &code.text,
1124 height,
1125 rewrite: overlay.fence.map(|host| {
1126 let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
1127 Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
1128 rewrite(ix, code, window, cx)
1129 }) as fence::Rewrite
1130 }),
1131 })
1132 })
1133 .flatten();
1134 let painted = match fence {
1135 Some(fence) => fence::render(&fence, overlay.block, window, cx),
1136 None => fence::Painted::Code(None),
1137 };
1138 match painted {
1139 fence::Painted::Block(element) => div()
1142 .key_context(PAINTED_CONTEXT)
1143 .relative()
1144 .when_some(overlay.fence, |el, host| {
1145 let (leave, ix) = (host.leave.clone(), overlay.block);
1146 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1147 })
1148 .when(overlay.covers_block(), |el| {
1149 el.rounded(px(4.0)).bg(theme.selection)
1150 })
1151 .group(painted_group(overlay.block))
1152 .child(element)
1153 .children(resizable(&overlay, theme))
1154 .into_any_element(),
1155 fence::Painted::Code(note) => code_block(
1156 language.as_deref(),
1157 &code.text,
1158 note,
1159 overlay,
1160 typography,
1161 theme,
1162 window,
1163 cx,
1164 ),
1165 }
1166 }
1167 BlockKind::Image { url, alt, width } => {
1168 image(url, alt, *width, overlay, typography, theme, window, cx)
1169 }
1170 BlockKind::Bookmark { url, form } => match crate::app::link::card(
1171 crate::Link::new(url, overlay.base),
1172 match (*form, overlay.held_height()) {
1173 (Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
1174 (form, _) => form,
1175 },
1176 window,
1177 cx,
1178 ) {
1179 Some(element) => div()
1182 .key_context(PAINTED_CONTEXT)
1183 .relative()
1184 .when_some(overlay.fence, |el, host| {
1185 let (leave, ix) = (host.leave.clone(), overlay.block);
1186 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1187 })
1188 .when(overlay.covers_block(), |el| {
1189 el.rounded(px(4.0)).bg(theme.selection)
1190 })
1191 .group(painted_group(overlay.block))
1192 .child(element)
1193 .children(match form {
1194 Form::Embed(_) => resizable(&overlay, theme),
1195 _ => Vec::new(),
1196 })
1197 .into_any_element(),
1198 None => bookmark(
1199 overlay.block,
1200 url,
1201 overlay.base,
1202 *form,
1203 typography,
1204 theme,
1205 cx,
1206 ),
1207 },
1208 BlockKind::Table {
1209 align,
1210 header,
1211 rows,
1212 } => table(align, header, rows, overlay, typography, theme, window, cx),
1213 BlockKind::Rule => div()
1214 .h(px(1.0))
1215 .w_full()
1216 .bg(theme.border)
1217 .into_any_element(),
1218 }
1219}
1220
1221fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
1223 div()
1224 .flex_none()
1225 .w(px(MARKER_WIDTH))
1226 .h(px(typography.body.line_height()))
1227 .flex()
1228 .items_center()
1229 .child(
1230 div()
1231 .ml(px(1.0))
1232 .w(px(5.0))
1233 .h(px(5.0))
1234 .rounded_full()
1235 .bg(theme.text_faint),
1236 )
1237 .into_any_element()
1238}
1239
1240fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
1241 let ix = overlay.block;
1242 let mut box_ = div()
1243 .relative()
1244 .w(px(13.0))
1245 .h(px(13.0))
1246 .rounded(px(3.5))
1247 .border_1()
1248 .flex()
1249 .items_center()
1250 .justify_center();
1251 box_ = if checked {
1252 box_.bg(theme.solid)
1253 .border_color(theme.solid)
1254 .text_style(TextStyle::Caption)
1255 .text_color(theme.on_solid)
1256 .child("✓")
1257 } else {
1258 box_.border_color(theme.border_strong)
1259 };
1260 box_ = box_.children(overlay.layouts.map(|layouts| {
1263 let layouts = layouts.clone();
1264 canvas(
1265 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1266 |_, _, _, _| (),
1267 )
1268 .absolute()
1269 .size_full()
1270 }));
1271 if overlay.toggle.is_some() {
1274 box_ = box_.cursor_pointer();
1275 }
1276 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1277 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1278 cx.stop_propagation();
1282 toggle(ix, window, cx);
1283 });
1284 }
1285
1286 div()
1287 .flex_none()
1288 .w(px(MARKER_WIDTH))
1289 .h(px(typography.body.line_height()))
1290 .flex()
1291 .items_center()
1292 .child(box_)
1293 .into_any_element()
1294}
1295
1296pub(crate) fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1298 match kind {
1299 QuoteKind::Note => theme.accent,
1300 QuoteKind::Tip => theme.success,
1301 QuoteKind::Important => theme.busy,
1302 QuoteKind::Warning => theme.warning,
1303 QuoteKind::Caution => theme.danger,
1304 }
1305}
1306
1307fn marker_row(
1308 marker: AnyElement,
1309 text: &Text,
1310 overlay: Overlay,
1311 typography: &Typography,
1312 theme: &Theme,
1313 cx: &App,
1314) -> AnyElement {
1315 div()
1316 .flex()
1317 .flex_row()
1318 .gap(px(MARKER_GAP))
1319 .child(marker)
1320 .child(div().flex_auto().min_w_0().child(text_element(
1321 text,
1322 typography.body.size(),
1323 typography.body.line_height(),
1324 FontWeight::NORMAL,
1325 overlay,
1326 theme,
1327 cx,
1328 )))
1329 .into_any_element()
1330}