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