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;
44mod table;
45mod text;
46
47pub use code::*;
48use column::*;
49pub use layouts::*;
50use media::*;
51use table::*;
52pub use text::*;
53
54const BLOCK_GAP: f32 = 12.0;
55const LIST_GAP: f32 = 4.0;
56const INDENT_WIDTH: f32 = 22.0;
58const MARKER_WIDTH: f32 = 18.0;
60const MARKER_GAP: f32 = 8.0;
61const CODE_PADDING_X: f32 = 12.0;
63const CODE_PADDING_Y: f32 = 10.0;
64pub const PLAIN_LANGUAGE: &str = "Plain";
67const CARET_WIDTH: f32 = 1.5;
70const INLINE_CODE_RADIUS: f32 = 4.5;
73const INLINE_CODE_PAD_X: f32 = 2.0;
74const INLINE_CODE_INSET_Y: f32 = 2.0;
75const MENTION_CARD_WIDTH: f32 = 320.0;
77const CARD_HEIGHT: f32 = 116.0;
81const CARD_IMAGE_WIDTH: f32 = 180.0;
82const CARD_COVER_HEIGHT: f32 = 200.0;
83const CARD_PADDING: f32 = 14.0;
84const CARD_BORDER: f32 = 1.0;
85const CARD_ICON: f32 = 16.0;
86const CARD_COVER: f32 = 44.0;
87const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
89const CAPTION_GAP: f32 = 4.0;
90const IMAGE_EMPTY: &str = "Add an image";
92const CAPTION_HINT: &str = "Write a caption";
93const TABLE_CELL_PADDING: f32 = 12.0;
96pub const TABLE_CONTROL_SIZE: f32 = 16.0;
98const TABLE_DIVIDER: f32 = 1.0;
99const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
102const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
104
105#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
116pub enum Caption {
117 #[default]
119 Shown,
120 Hidden,
122}
123
124#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
128pub enum CopyButton {
129 #[default]
131 Shown,
132 Hidden,
135}
136
137#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
143#[non_exhaustive]
144pub enum Annotation {
145 #[default]
147 Open,
148 Resolved,
150 Active,
152 Highlight(crate::HighlightColor),
155 Match,
157 Current,
159}
160
161impl Annotation {
162 fn wash(self, theme: &Theme, highlight: crate::HighlightPaint, find: crate::FindPaint) -> Hsla {
163 match self {
164 Self::Open => theme.warning.opacity(0.20),
165 Self::Resolved => theme.warning.opacity(0.08),
166 Self::Active => theme.warning.opacity(0.38),
167 Self::Highlight(color) => highlight(color, theme),
168 Self::Match => find(theme).0,
169 Self::Current => find(theme).1,
170 }
171 }
172}
173
174pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
179
180pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
182
183pub type OnJump = Rc<dyn Fn(usize, &mut Window, &mut App)>;
186
187pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
189
190pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
192
193pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
195
196pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
199
200#[derive(Clone)]
203pub struct FenceHost {
204 pub rewrite: OnRewrite,
207 pub leave: OnLeave,
210 pub resize: Option<OnResize>,
213}
214
215pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
218
219gpui::actions!(
220 markdown,
221 [
222 LeaveBlock
224 ]
225);
226
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
229pub enum ImageOverlayCorner {
230 TopLeft,
231 TopRight,
232 BottomLeft,
233 #[default]
234 BottomRight,
235}
236
237#[derive(Clone)]
243pub enum Toggle {
244 Handled(OnToggle),
247 HitTested,
253}
254
255#[derive(Clone)]
261pub struct Editing<'a> {
262 pub selection: Option<Selection>,
265 pub caret_on: bool,
268 pub layouts: Option<&'a BlockLayouts>,
274 pub annotations: &'a [(Selection, Annotation)],
276 pub placeholder: Option<SharedString>,
278 pub caption: Caption,
279 pub typography: Option<Typography>,
283 pub toggle: Option<Toggle>,
286 pub image: Option<OnImage>,
290 pub image_overlay: Option<ImageOverlay>,
293 pub image_overlay_corner: ImageOverlayCorner,
295 pub fence: Option<FenceHost>,
298 pub sizing: Option<(usize, u32)>,
301 pub table_controls: bool,
303 pub copy: CopyButton,
305 pub base: Option<&'a Path>,
308 pub keep: &'a [usize],
313 pub scroll: Option<&'a gpui::ScrollHandle>,
317 pub jump: Option<OnJump>,
321}
322
323impl Default for Editing<'_> {
324 fn default() -> Self {
325 Self {
326 selection: None,
327 caret_on: true,
330 layouts: None,
331 annotations: &[],
332 placeholder: None,
333 caption: Caption::default(),
334 typography: None,
335 toggle: None,
336 image: None,
337 image_overlay: None,
338 image_overlay_corner: ImageOverlayCorner::BottomRight,
339 fence: None,
340 sizing: None,
341 table_controls: false,
342 copy: CopyButton::default(),
343 base: None,
344 keep: &[],
345 scroll: None,
346 jump: None,
347 }
348 }
349}
350
351#[derive(Clone, Copy)]
357struct Overlay<'a> {
358 block: usize,
359 part: Part,
360 selection: Option<Selection>,
361 caret_on: bool,
362 caret_shape: ui::input::CaretShape,
363 caret_height: ui::input::CaretHeight,
364 caret_inactive: ui::input::InactiveCaret,
365 window_active: bool,
367 layouts: Option<&'a BlockLayouts>,
368 annotations: &'a [(Selection, Annotation)],
370 placeholder: Option<&'a SharedString>,
373 caption: Caption,
374 toggle: Option<&'a Toggle>,
377 image: Option<&'a OnImage>,
378 image_overlay: Option<&'a ImageOverlay>,
379 image_overlay_corner: ImageOverlayCorner,
380 fence: Option<&'a FenceHost>,
381 sizing: Option<(usize, u32)>,
382 table_controls: bool,
383 copy: CopyButton,
384 base: Option<&'a Path>,
385 highlight: crate::HighlightPaint,
386 find: crate::FindPaint,
387 jump: Option<&'a crate::link::Jump>,
388}
389
390fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
394 let ix = overlay.block;
395 let mut parts = Vec::new();
396 if let Some(layouts) = overlay.layouts {
397 let layouts = layouts.clone();
398 parts.push(
399 canvas(
400 move |bounds, _, _| layouts.record_painted(ix, bounds),
401 |_, _, _, _| (),
402 )
403 .absolute()
404 .size_full()
405 .into_any_element(),
406 );
407 }
408 if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
409 let dragging = overlay.held_height().is_some();
410 parts.push(
415 div()
416 .absolute()
417 .left_0()
418 .right_0()
419 .bottom_0()
420 .flex()
421 .justify_center()
422 .child(
423 theme
424 .grip_handle()
425 .id(ElementId::named_usize("painted-grip", ix))
426 .debug_selector(|| "painted-grip".into())
427 .w(px(PAINTED_GRIP_WIDTH))
428 .when(!dragging, |el| {
429 el.invisible()
430 .group_hover(painted_group(ix), |el| el.visible())
431 })
432 .on_mouse_down(MouseButton::Left, move |event, window, cx| {
433 cx.stop_propagation();
434 resize(ix, event.position.y, window, cx);
435 }),
436 )
437 .into_any_element(),
438 );
439 }
440 parts
441}
442
443const PAINTED_GRIP_WIDTH: f32 = 120.0;
445
446fn painted_group(ix: usize) -> SharedString {
447 SharedString::from(format!("painted-{ix}"))
448}
449
450impl<'a> Overlay<'a> {
451 fn at(self, part: Part) -> Self {
452 Self { part, ..self }
453 }
454
455 fn held_height(&self) -> Option<u32> {
457 self.sizing
458 .filter(|(ix, _)| *ix == self.block)
459 .map(|(_, height)| height)
460 }
461
462 fn here(&self) -> Cursor {
463 Cursor::new(self.block, self.part, 0)
464 }
465
466 fn caret_painted(&self) -> Option<usize> {
473 let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
474 let shown = self
475 .caret_shape
476 .shown(self.window_active, self.caret_inactive);
477 (self.caret_on && collapsed && shown)
478 .then(|| self.caret())
479 .flatten()
480 }
481
482 fn affinity(&self) -> Affinity {
484 self.selection
485 .map_or_else(Affinity::default, |selection| selection.affinity)
486 }
487
488 fn covered(&self, text: &str, offset: usize) -> Option<Range<usize>> {
491 (self.affinity() == Affinity::Downstream)
492 .then(|| glyph_at(text, offset))
493 .flatten()
494 }
495
496 fn caret_hollow(&self) -> bool {
497 !self.window_active
498 }
499
500 fn caret(&self) -> Option<usize> {
502 self.selection
503 .map(|selection| selection.head)
504 .filter(|head| head.block == self.block && head.part == self.part)
505 .map(|head| head.offset)
506 }
507
508 fn selected(&self, len: usize) -> Option<Range<usize>> {
510 self.clip(self.selection?, len)
511 }
512
513 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
516 self.annotations
517 .iter()
518 .filter_map(|(range, kind)| {
519 Some((
520 self.clip(*range, len)?,
521 kind.wash(theme, self.highlight, self.find),
522 ))
523 })
524 .collect()
525 }
526
527 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
533 if selection.is_collapsed() {
534 return None;
535 }
536 let (start, end) = selection.ordered();
537 let here = self.here();
538 let (first, last) = (
539 Cursor::new(start.block, start.part, 0),
540 Cursor::new(end.block, end.part, 0),
541 );
542 if here < first || here > last {
543 return None;
544 }
545 let from = if here == first { start.offset } else { 0 };
546 let to = if here == last { end.offset } else { len };
547 (from < to).then_some(from..to.min(len))
548 }
549
550 fn covers_block(&self) -> bool {
554 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
555 return false;
556 };
557 let (start, end) = selection.ordered();
558 start.block < self.block && self.block < end.block
559 }
560}
561
562pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
568 if url.contains("://") {
569 return SharedString::from(url.to_string()).into();
570 }
571 match base {
574 Some(base) => base.join(url).into(),
575 None => std::path::PathBuf::from(url).into(),
576 }
577}
578
579pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
581 let doc = crate::parse_with(source, &cx.marks());
582 render(&doc, Caption::default(), window, cx)
583}
584
585pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
587 render_with(
588 doc,
589 Editing {
590 caption,
591 ..Editing::default()
592 },
593 window,
594 cx,
595 )
596}
597
598pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
606 let Editing {
607 selection,
608 caret_on,
609 layouts,
610 annotations,
611 placeholder,
612 caption,
613 typography,
614 toggle,
615 image,
616 image_overlay,
617 image_overlay_corner,
618 fence,
619 sizing,
620 table_controls,
621 copy,
622 base,
623 keep,
624 scroll,
625 jump,
626 } = editing;
627 let theme = Theme::of(cx).clone();
630 let typography = typography.unwrap_or_else(|| cx.typography());
631 let highlight = crate::marks::highlight_paint_of(cx);
632 let find = crate::find::find_paint_of(cx);
633 let jump = jump.map(|to| crate::link::Jump::new(doc, to));
634 let gaps: Vec<Pixels> = doc
635 .blocks
636 .iter()
637 .enumerate()
638 .map(|(ix, block)| {
639 px(match doc.blocks.get(ix.wrapping_sub(1)) {
640 None => 0.0,
641 Some(previous) if tight(previous, block) => LIST_GAP,
642 Some(_) => BLOCK_GAP,
643 })
644 })
645 .collect();
646 let runs = quote_runs(doc.blocks.iter().map(|block| (block.quote, block.indent)));
647
648 let Some(layouts) = layouts else {
649 let mut column = div().flex().flex_col();
650 for (ix, block) in doc.blocks.iter().enumerate() {
651 let overlay = Overlay {
652 block: ix,
653 part: Part::Body,
654 selection,
655 caret_on,
656 caret_shape: cx.caret_shape(),
657 caret_height: cx.caret_height(),
658 caret_inactive: cx.inactive_caret(),
659 window_active: window.is_window_active(),
660 layouts: None,
661 annotations,
662 placeholder: placeholder.as_ref(),
663 caption,
664 toggle: toggle.as_ref(),
665 image: image.as_ref(),
666 image_overlay: image_overlay.as_ref(),
667 image_overlay_corner,
668 fence: fence.as_ref(),
669 sizing,
670 table_controls,
671 copy,
672 base,
673 highlight,
674 find,
675 jump: jump.as_ref(),
676 };
677 let quote = Quote {
678 run: runs[ix],
679 gap: gaps[ix],
680 };
681 column = column.child(
682 block_box(block, quote, overlay, &typography, &theme, window, cx).mt(gaps[ix]),
683 );
684 }
685 return column.into_any_element();
686 };
687
688 let keys: Vec<u64> = doc
689 .blocks
690 .iter()
691 .zip(&runs)
692 .map(|(block, run)| block_key(block, *run, &typography, table_controls))
693 .collect();
694 layouts.prune(&keys);
695 let guesses: Vec<Guess> = doc
696 .blocks
697 .iter()
698 .zip(&runs)
699 .map(|(block, run)| {
700 let mut guess = guess(block, *run, &typography);
701 if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
702 guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
703 }
704 guess
705 })
706 .collect();
707 let mut kept = keep.to_vec();
708 kept.extend(selection.map(|selection| selection.head.block));
709 kept.extend(selection.map(|selection| selection.anchor.block));
710 let owned = Owned {
711 blocks: doc.blocks.clone(),
712 runs,
713 gaps: gaps.clone(),
714 selection,
715 caret_on,
716 layouts: layouts.clone(),
717 annotations: annotations.to_vec(),
718 placeholder,
719 caption,
720 toggle,
721 image,
722 image_overlay,
723 image_overlay_corner,
724 fence,
725 sizing,
726 table_controls,
727 copy,
728 base: base.map(Path::to_path_buf),
729 highlight,
730 find,
731 jump,
732 typography,
733 theme,
734 };
735 Column {
736 layouts: layouts.clone(),
737 keys: keys.into(),
738 gaps: gaps.into(),
739 guesses: guesses.into(),
740 keep: kept,
741 scroll: scroll.cloned(),
742 item_of: Rc::new(|at| at.block),
743 build: Box::new(move |ix, window, cx| {
744 let overlay = Overlay {
745 block: ix,
746 part: Part::Body,
747 selection: owned.selection,
748 caret_on: owned.caret_on,
749 caret_shape: cx.caret_shape(),
750 caret_height: cx.caret_height(),
751 caret_inactive: cx.inactive_caret(),
752 window_active: window.is_window_active(),
753 layouts: Some(&owned.layouts),
754 annotations: &owned.annotations,
755 placeholder: owned.placeholder.as_ref(),
756 caption: owned.caption,
757 toggle: owned.toggle.as_ref(),
758 image: owned.image.as_ref(),
759 image_overlay: owned.image_overlay.as_ref(),
760 image_overlay_corner: owned.image_overlay_corner,
761 fence: owned.fence.as_ref(),
762 sizing: owned.sizing,
763 table_controls: owned.table_controls,
764 copy: owned.copy,
765 base: owned.base.as_deref(),
766 highlight: owned.highlight,
767 find: owned.find,
768 jump: owned.jump.as_ref(),
769 };
770 let quote = Quote {
771 run: owned.runs[ix],
772 gap: owned.gaps[ix],
773 };
774 block_box(
775 &owned.blocks[ix],
776 quote,
777 overlay,
778 &owned.typography,
779 &owned.theme,
780 window,
781 cx,
782 )
783 .into_any_element()
784 }),
785 }
786 .into_any_element()
787}
788
789struct Owned {
791 blocks: Vec<Block>,
792 runs: Vec<Option<Run>>,
793 gaps: Vec<Pixels>,
794 selection: Option<Selection>,
795 caret_on: bool,
796 layouts: BlockLayouts,
797 annotations: Vec<(Selection, Annotation)>,
798 placeholder: Option<SharedString>,
799 caption: Caption,
800 toggle: Option<Toggle>,
801 image: Option<OnImage>,
802 image_overlay: Option<ImageOverlay>,
803 image_overlay_corner: ImageOverlayCorner,
804 fence: Option<FenceHost>,
805 sizing: Option<(usize, u32)>,
806 table_controls: bool,
807 copy: CopyButton,
808 base: Option<std::path::PathBuf>,
809 highlight: crate::HighlightPaint,
810 find: crate::FindPaint,
811 jump: Option<crate::link::Jump>,
812 typography: Typography,
813 theme: Theme,
814}
815
816#[derive(Clone, Copy)]
819struct Quote {
820 run: Option<Run>,
821 gap: Pixels,
822}
823
824const QUOTE_INSET: f32 = 12.0;
826const QUOTE_BAR: f32 = 2.0;
827
828fn block_box(
831 block: &Block,
832 quote: Quote,
833 overlay: Overlay,
834 typography: &Typography,
835 theme: &Theme,
836 window: &mut Window,
837 cx: &mut App,
838) -> gpui::Div {
839 let ix = overlay.block;
840 let frame = overlay.layouts.map(|layouts| {
843 let layouts = layouts.clone();
844 canvas(
845 move |bounds, _, _| layouts.record_block(ix, bounds),
846 |_, _, _, _| (),
847 )
848 .absolute()
849 .size_full()
850 });
851 div()
857 .w_full()
858 .font_family(theme.font_body.clone())
861 .pl(px(block.indent as f32 * INDENT_WIDTH))
862 .child(
863 div()
864 .w_full()
865 .relative()
866 .children(frame)
867 .when(overlay.covers_block() && block.opaque(), |el| {
871 el.rounded(px(4.0)).bg(theme.selection)
872 })
873 .child(quoted(
874 block,
875 quote,
876 block_element(block, overlay, typography, theme, window, cx),
877 typography,
878 theme,
879 )),
880 )
881}
882
883fn quoted(
889 block: &Block,
890 quote: Quote,
891 element: AnyElement,
892 typography: &Typography,
893 theme: &Theme,
894) -> AnyElement {
895 let (Some(run), Some(quoted)) = (quote.run, block.quote) else {
896 return element;
897 };
898 let color = quoted
899 .alert
900 .map_or(theme.border_strong, |alert| alert_color(alert, theme));
901 let label = quoted.alert.filter(|_| run.first).map(|alert| {
902 div()
903 .pb(px(2.0))
904 .text_size(px(typography.body.size()))
905 .line_height(px(typography.body.line_height()))
906 .font_weight(FontWeight::SEMIBOLD)
907 .text_color(color)
908 .child(alert.label())
909 });
910 let bar = div()
911 .absolute()
912 .left(px(
913 -((block.indent - run.level.min(block.indent)) as f32) * INDENT_WIDTH
914 ))
915 .top(if run.first { px(0.0) } else { -quote.gap })
916 .bottom_0()
917 .w(px(QUOTE_BAR))
918 .bg(color);
919 div()
920 .child(bar)
921 .pl(px(QUOTE_BAR + QUOTE_INSET))
922 .pr(px(10.0))
923 .py(px(2.0))
924 .text_color(theme.text_muted)
925 .children(label)
926 .child(element)
927 .into_any_element()
928}
929
930fn block_key(
933 block: &Block,
934 run: Option<Run>,
935 typography: &Typography,
936 table_controls: bool,
937) -> u64 {
938 let mut hasher = DefaultHasher::new();
939 table_controls.hash(&mut hasher);
940 block.hash(&mut hasher);
941 run.map(|run| run.first).hash(&mut hasher);
942 typography.body.size().to_bits().hash(&mut hasher);
943 typography.body.line_height().to_bits().hash(&mut hasher);
944 hasher.finish()
945}
946
947fn guess(block: &Block, run: Option<Run>, typography: &Typography) -> Guess {
949 let body = px(typography.body.line_height());
950 let prose = |chars: usize, line: Pixels| Guess {
951 chars,
952 line,
953 rows: 0,
954 extra: px(0.0),
955 indent: px(0.0),
956 };
957 let guess = match &block.kind {
958 BlockKind::Paragraph(text)
959 | BlockKind::Bullet(text)
960 | BlockKind::Ordered { text, .. }
961 | BlockKind::Task { text, .. } => prose(text.text.len(), body),
962 BlockKind::Heading { level, text } => prose(
963 text.text.len(),
964 px(typography.heading(*level).line_height()),
965 ),
966 BlockKind::Code { code, .. } => Guess {
967 rows: code.text.lines().count().max(1),
968 line: px(typography.code.line_height()),
969 extra: px(2.0 * CODE_PADDING_Y) + body,
970 ..prose(0, body)
971 },
972 BlockKind::Table { rows, .. } => Guess {
973 rows: rows.len() + 1,
974 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
975 ..prose(0, body)
976 },
977 BlockKind::Image { alt, .. } => Guess {
978 extra: px(240.0),
979 ..prose(alt.text.len(), px(typography.caption.line_height()))
980 },
981 BlockKind::Bookmark { .. } => Guess {
982 extra: body * 3.0,
983 ..prose(0, body)
984 },
985 BlockKind::Rule => Guess {
986 extra: body,
987 ..prose(0, px(0.0))
988 },
989 };
990 let quoted = run.is_some();
991 let label = run.is_some_and(|run| run.first) && block.quote.is_some_and(|q| q.alert.is_some());
992 Guess {
993 indent: px(block.indent as f32 * INDENT_WIDTH)
994 + if quoted {
995 px(QUOTE_BAR + QUOTE_INSET + 10.0)
996 } else {
997 px(0.0)
998 },
999 extra: guess.extra
1000 + if quoted { px(4.0) } else { px(0.0) }
1001 + if label { body + px(2.0) } else { px(0.0) },
1002 ..guess
1003 }
1004}
1005
1006fn tight(previous: &Block, next: &Block) -> bool {
1008 let marker = |block: &Block| {
1009 matches!(
1010 block.kind,
1011 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
1012 )
1013 };
1014 marker(previous) && (marker(next) || next.indent > previous.indent)
1015}
1016
1017fn block_element(
1018 block: &Block,
1019 overlay: Overlay,
1020 typography: &Typography,
1021 theme: &Theme,
1022 window: &mut Window,
1023 cx: &mut App,
1024) -> AnyElement {
1025 let body = overlay.at(Part::Body);
1026 match &block.kind {
1027 BlockKind::Paragraph(text) => text_element(
1028 text,
1029 typography.body.size(),
1030 typography.body.line_height(),
1031 FontWeight::NORMAL,
1032 body,
1033 theme,
1034 cx,
1035 ),
1036 BlockKind::Heading { level, text } => {
1037 let heading = typography.heading(*level);
1038 text_element(
1039 text,
1040 heading.size(),
1041 heading.line_height(),
1042 heading.weight,
1043 body,
1044 theme,
1045 cx,
1046 )
1047 }
1048 BlockKind::Bullet(text) => {
1049 marker_row(disc(typography, theme), text, body, typography, theme, cx)
1050 }
1051 BlockKind::Ordered { number, text } => marker_row(
1052 div()
1053 .flex_none()
1054 .w(px(MARKER_WIDTH))
1055 .text_size(px(typography.body.size()))
1056 .line_height(px(typography.body.line_height()))
1057 .text_color(theme.text_muted)
1058 .child(SharedString::from(format!("{number}.")))
1059 .into_any_element(),
1060 text,
1061 body,
1062 typography,
1063 theme,
1064 cx,
1065 ),
1066 BlockKind::Task { checked, text } => marker_row(
1067 checkbox(*checked, overlay, typography, theme),
1068 text,
1069 body,
1070 typography,
1071 theme,
1072 cx,
1073 ),
1074 BlockKind::Code {
1075 language,
1076 code,
1077 height,
1078 } => {
1079 let overlay = overlay.at(Part::Code);
1080 let height = overlay.held_height().or(*height);
1081 let painted = overlay
1085 .caret()
1086 .is_none()
1087 .then(|| {
1088 let fence = block::Fence {
1089 language: language.as_deref()?,
1090 code: &code.text,
1091 height,
1092 rewrite: overlay.fence.map(|host| {
1093 let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
1094 Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
1095 rewrite(ix, code, window, cx)
1096 }) as block::Rewrite
1097 }),
1098 };
1099 block::render(&fence, window, cx)
1100 })
1101 .flatten();
1102 match painted {
1103 Some(element) => div()
1106 .key_context(PAINTED_CONTEXT)
1107 .relative()
1108 .when_some(overlay.fence, |el, host| {
1109 let (leave, ix) = (host.leave.clone(), overlay.block);
1110 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1111 })
1112 .when(overlay.covers_block(), |el| {
1113 el.rounded(px(4.0)).bg(theme.selection)
1114 })
1115 .group(painted_group(overlay.block))
1116 .child(element)
1117 .children(resizable(&overlay, theme))
1118 .into_any_element(),
1119 None => code_block(
1120 language.as_deref(),
1121 &code.text,
1122 overlay,
1123 typography,
1124 theme,
1125 window,
1126 cx,
1127 ),
1128 }
1129 }
1130 BlockKind::Image { url, alt, width } => {
1131 image(url, alt, *width, overlay, typography, theme, window, cx)
1132 }
1133 BlockKind::Bookmark { url, form } => match crate::preview::card(
1134 url,
1135 match (*form, overlay.held_height()) {
1136 (Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
1137 (form, _) => form,
1138 },
1139 window,
1140 cx,
1141 ) {
1142 Some(element) => div()
1145 .key_context(PAINTED_CONTEXT)
1146 .relative()
1147 .when_some(overlay.fence, |el, host| {
1148 let (leave, ix) = (host.leave.clone(), overlay.block);
1149 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1150 })
1151 .when(overlay.covers_block(), |el| {
1152 el.rounded(px(4.0)).bg(theme.selection)
1153 })
1154 .group(painted_group(overlay.block))
1155 .child(element)
1156 .children(match form {
1157 Form::Embed(_) => resizable(&overlay, theme),
1158 _ => Vec::new(),
1159 })
1160 .into_any_element(),
1161 None => bookmark(
1162 overlay.block,
1163 url,
1164 overlay.base,
1165 *form,
1166 typography,
1167 theme,
1168 cx,
1169 ),
1170 },
1171 BlockKind::Table {
1172 align,
1173 header,
1174 rows,
1175 } => table(align, header, rows, overlay, typography, theme, window, cx),
1176 BlockKind::Rule => div()
1177 .h(px(1.0))
1178 .w_full()
1179 .bg(theme.border)
1180 .into_any_element(),
1181 }
1182}
1183
1184fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
1186 div()
1187 .flex_none()
1188 .w(px(MARKER_WIDTH))
1189 .h(px(typography.body.line_height()))
1190 .flex()
1191 .items_center()
1192 .child(
1193 div()
1194 .ml(px(1.0))
1195 .w(px(5.0))
1196 .h(px(5.0))
1197 .rounded_full()
1198 .bg(theme.text_faint),
1199 )
1200 .into_any_element()
1201}
1202
1203fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
1204 let ix = overlay.block;
1205 let mut box_ = div()
1206 .relative()
1207 .w(px(13.0))
1208 .h(px(13.0))
1209 .rounded(px(3.5))
1210 .border_1()
1211 .flex()
1212 .items_center()
1213 .justify_center();
1214 box_ = if checked {
1215 box_.bg(theme.solid)
1216 .border_color(theme.solid)
1217 .text_style(TextStyle::Caption)
1218 .text_color(theme.on_solid)
1219 .child("✓")
1220 } else {
1221 box_.border_color(theme.border_strong)
1222 };
1223 box_ = box_.children(overlay.layouts.map(|layouts| {
1226 let layouts = layouts.clone();
1227 canvas(
1228 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1229 |_, _, _, _| (),
1230 )
1231 .absolute()
1232 .size_full()
1233 }));
1234 if overlay.toggle.is_some() {
1237 box_ = box_.cursor_pointer();
1238 }
1239 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1240 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1241 cx.stop_propagation();
1245 toggle(ix, window, cx);
1246 });
1247 }
1248
1249 div()
1250 .flex_none()
1251 .w(px(MARKER_WIDTH))
1252 .h(px(typography.body.line_height()))
1253 .flex()
1254 .items_center()
1255 .child(box_)
1256 .into_any_element()
1257}
1258
1259fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1261 match kind {
1262 QuoteKind::Note => theme.accent,
1263 QuoteKind::Tip => theme.success,
1264 QuoteKind::Important => theme.busy,
1265 QuoteKind::Warning => theme.warning,
1266 QuoteKind::Caution => theme.danger,
1267 }
1268}
1269
1270fn marker_row(
1271 marker: AnyElement,
1272 text: &Text,
1273 overlay: Overlay,
1274 typography: &Typography,
1275 theme: &Theme,
1276 cx: &App,
1277) -> AnyElement {
1278 div()
1279 .flex()
1280 .flex_row()
1281 .gap(px(MARKER_GAP))
1282 .child(marker)
1283 .child(div().flex_auto().min_w_0().child(text_element(
1284 text,
1285 typography.body.size(),
1286 typography.body.line_height(),
1287 FontWeight::NORMAL,
1288 overlay,
1289 theme,
1290 cx,
1291 )))
1292 .into_any_element()
1293}