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