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, Text},
33 preview,
34 select::{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 ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
184
185pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
187
188pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
190
191pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
194
195#[derive(Clone)]
198pub struct FenceHost {
199 pub rewrite: OnRewrite,
202 pub leave: OnLeave,
205 pub resize: Option<OnResize>,
208}
209
210pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
213
214gpui::actions!(
215 markdown,
216 [
217 LeaveBlock
219 ]
220);
221
222#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
224pub enum ImageOverlayCorner {
225 TopLeft,
226 TopRight,
227 BottomLeft,
228 #[default]
229 BottomRight,
230}
231
232#[derive(Clone)]
238pub enum Toggle {
239 Handled(OnToggle),
242 HitTested,
248}
249
250#[derive(Clone)]
256pub struct Editing<'a> {
257 pub selection: Option<Selection>,
260 pub caret_on: bool,
263 pub layouts: Option<&'a BlockLayouts>,
269 pub annotations: &'a [(Selection, Annotation)],
271 pub placeholder: Option<SharedString>,
273 pub caption: Caption,
274 pub typography: Option<Typography>,
278 pub toggle: Option<Toggle>,
281 pub image: Option<OnImage>,
285 pub image_overlay: Option<ImageOverlay>,
288 pub image_overlay_corner: ImageOverlayCorner,
290 pub fence: Option<FenceHost>,
293 pub sizing: Option<(usize, u32)>,
296 pub table_controls: bool,
298 pub copy: CopyButton,
300 pub base: Option<&'a Path>,
303 pub keep: &'a [usize],
308 pub scroll: Option<&'a gpui::ScrollHandle>,
312}
313
314impl Default for Editing<'_> {
315 fn default() -> Self {
316 Self {
317 selection: None,
318 caret_on: true,
321 layouts: None,
322 annotations: &[],
323 placeholder: None,
324 caption: Caption::default(),
325 typography: None,
326 toggle: None,
327 image: None,
328 image_overlay: None,
329 image_overlay_corner: ImageOverlayCorner::BottomRight,
330 fence: None,
331 sizing: None,
332 table_controls: false,
333 copy: CopyButton::default(),
334 base: None,
335 keep: &[],
336 scroll: None,
337 }
338 }
339}
340
341#[derive(Clone, Copy)]
347struct Overlay<'a> {
348 block: usize,
349 part: Part,
350 selection: Option<Selection>,
351 caret_on: bool,
352 caret_shape: ui::input::CaretShape,
353 caret_height: ui::input::CaretHeight,
354 caret_inactive: ui::input::InactiveCaret,
355 window_active: bool,
357 layouts: Option<&'a BlockLayouts>,
358 annotations: &'a [(Selection, Annotation)],
360 placeholder: Option<&'a SharedString>,
363 caption: Caption,
364 toggle: Option<&'a Toggle>,
367 image: Option<&'a OnImage>,
368 image_overlay: Option<&'a ImageOverlay>,
369 image_overlay_corner: ImageOverlayCorner,
370 fence: Option<&'a FenceHost>,
371 sizing: Option<(usize, u32)>,
372 table_controls: bool,
373 copy: CopyButton,
374 base: Option<&'a Path>,
375 highlight: crate::HighlightPaint,
376 find: crate::FindPaint,
377}
378
379fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
383 let ix = overlay.block;
384 let mut parts = Vec::new();
385 if let Some(layouts) = overlay.layouts {
386 let layouts = layouts.clone();
387 parts.push(
388 canvas(
389 move |bounds, _, _| layouts.record_painted(ix, bounds),
390 |_, _, _, _| (),
391 )
392 .absolute()
393 .size_full()
394 .into_any_element(),
395 );
396 }
397 if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
398 let dragging = overlay.held_height().is_some();
399 parts.push(
404 div()
405 .absolute()
406 .left_0()
407 .right_0()
408 .bottom_0()
409 .flex()
410 .justify_center()
411 .child(
412 theme
413 .grip_handle()
414 .id(ElementId::named_usize("painted-grip", ix))
415 .debug_selector(|| "painted-grip".into())
416 .w(px(PAINTED_GRIP_WIDTH))
417 .when(!dragging, |el| {
418 el.invisible()
419 .group_hover(painted_group(ix), |el| el.visible())
420 })
421 .on_mouse_down(MouseButton::Left, move |event, window, cx| {
422 cx.stop_propagation();
423 resize(ix, event.position.y, window, cx);
424 }),
425 )
426 .into_any_element(),
427 );
428 }
429 parts
430}
431
432const PAINTED_GRIP_WIDTH: f32 = 120.0;
434
435fn painted_group(ix: usize) -> SharedString {
436 SharedString::from(format!("painted-{ix}"))
437}
438
439impl<'a> Overlay<'a> {
440 fn at(self, part: Part) -> Self {
441 Self { part, ..self }
442 }
443
444 fn held_height(&self) -> Option<u32> {
446 self.sizing
447 .filter(|(ix, _)| *ix == self.block)
448 .map(|(_, height)| height)
449 }
450
451 fn here(&self) -> Cursor {
452 Cursor::new(self.block, self.part, 0)
453 }
454
455 fn caret_painted(&self) -> Option<usize> {
462 let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
463 let shown = self
464 .caret_shape
465 .shown(self.window_active, self.caret_inactive);
466 (self.caret_on && collapsed && shown)
467 .then(|| self.caret())
468 .flatten()
469 }
470
471 fn caret_hollow(&self) -> bool {
472 !self.window_active
473 }
474
475 fn caret(&self) -> Option<usize> {
477 self.selection
478 .map(|selection| selection.head)
479 .filter(|head| head.block == self.block && head.part == self.part)
480 .map(|head| head.offset)
481 }
482
483 fn selected(&self, len: usize) -> Option<Range<usize>> {
485 self.clip(self.selection?, len)
486 }
487
488 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
491 self.annotations
492 .iter()
493 .filter_map(|(range, kind)| {
494 Some((
495 self.clip(*range, len)?,
496 kind.wash(theme, self.highlight, self.find),
497 ))
498 })
499 .collect()
500 }
501
502 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
508 if selection.is_collapsed() {
509 return None;
510 }
511 let (start, end) = selection.ordered();
512 let here = self.here();
513 let (first, last) = (
514 Cursor::new(start.block, start.part, 0),
515 Cursor::new(end.block, end.part, 0),
516 );
517 if here < first || here > last {
518 return None;
519 }
520 let from = if here == first { start.offset } else { 0 };
521 let to = if here == last { end.offset } else { len };
522 (from < to).then_some(from..to.min(len))
523 }
524
525 fn covers_block(&self) -> bool {
529 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
530 return false;
531 };
532 let (start, end) = selection.ordered();
533 start.block < self.block && self.block < end.block
534 }
535}
536
537pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
543 if url.contains("://") {
544 return SharedString::from(url.to_string()).into();
545 }
546 match base {
549 Some(base) => base.join(url).into(),
550 None => std::path::PathBuf::from(url).into(),
551 }
552}
553
554pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
556 let doc = crate::parse_with(source, &cx.marks());
557 render(&doc, Caption::default(), window, cx)
558}
559
560pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
562 render_with(
563 doc,
564 Editing {
565 caption,
566 ..Editing::default()
567 },
568 window,
569 cx,
570 )
571}
572
573pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
581 let Editing {
582 selection,
583 caret_on,
584 layouts,
585 annotations,
586 placeholder,
587 caption,
588 typography,
589 toggle,
590 image,
591 image_overlay,
592 image_overlay_corner,
593 fence,
594 sizing,
595 table_controls,
596 copy,
597 base,
598 keep,
599 scroll,
600 } = editing;
601 let theme = Theme::of(cx).clone();
604 let typography = typography.unwrap_or_else(|| cx.typography());
605 let highlight = crate::marks::highlight_paint_of(cx);
606 let find = crate::find::find_paint_of(cx);
607 let gaps: Vec<Pixels> = doc
608 .blocks
609 .iter()
610 .enumerate()
611 .map(|(ix, block)| {
612 px(match doc.blocks.get(ix.wrapping_sub(1)) {
613 None => 0.0,
614 Some(previous) if tight(previous, block) => LIST_GAP,
615 Some(_) => BLOCK_GAP,
616 })
617 })
618 .collect();
619
620 let Some(layouts) = layouts else {
621 let mut column = div().flex().flex_col();
622 for (ix, block) in doc.blocks.iter().enumerate() {
623 let overlay = Overlay {
624 block: ix,
625 part: Part::Body,
626 selection,
627 caret_on,
628 caret_shape: cx.caret_shape(),
629 caret_height: cx.caret_height(),
630 caret_inactive: cx.inactive_caret(),
631 window_active: window.is_window_active(),
632 layouts: None,
633 annotations,
634 placeholder: placeholder.as_ref(),
635 caption,
636 toggle: toggle.as_ref(),
637 image: image.as_ref(),
638 image_overlay: image_overlay.as_ref(),
639 image_overlay_corner,
640 fence: fence.as_ref(),
641 sizing,
642 table_controls,
643 copy,
644 base,
645 highlight,
646 find,
647 };
648 column = column
649 .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
650 }
651 return column.into_any_element();
652 };
653
654 let keys: Vec<u64> = doc
655 .blocks
656 .iter()
657 .map(|block| block_key(block, &typography, table_controls))
658 .collect();
659 layouts.prune(&keys);
660 let guesses: Vec<Guess> = doc
661 .blocks
662 .iter()
663 .map(|block| {
664 let mut guess = guess(block, &typography);
665 if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
666 guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
667 }
668 guess
669 })
670 .collect();
671 let mut kept = keep.to_vec();
672 kept.extend(selection.map(|selection| selection.head.block));
673 kept.extend(selection.map(|selection| selection.anchor.block));
674 let owned = Owned {
675 blocks: doc.blocks.clone(),
676 selection,
677 caret_on,
678 layouts: layouts.clone(),
679 annotations: annotations.to_vec(),
680 placeholder,
681 caption,
682 toggle,
683 image,
684 image_overlay,
685 image_overlay_corner,
686 fence,
687 sizing,
688 table_controls,
689 copy,
690 base: base.map(Path::to_path_buf),
691 highlight,
692 find,
693 typography,
694 theme,
695 };
696 Column {
697 layouts: layouts.clone(),
698 keys: keys.into(),
699 gaps: gaps.into(),
700 guesses: guesses.into(),
701 keep: kept,
702 scroll: scroll.cloned(),
703 item_of: Box::new(|at| at.block),
704 build: Box::new(move |ix, window, cx| {
705 let overlay = Overlay {
706 block: ix,
707 part: Part::Body,
708 selection: owned.selection,
709 caret_on: owned.caret_on,
710 caret_shape: cx.caret_shape(),
711 caret_height: cx.caret_height(),
712 caret_inactive: cx.inactive_caret(),
713 window_active: window.is_window_active(),
714 layouts: Some(&owned.layouts),
715 annotations: &owned.annotations,
716 placeholder: owned.placeholder.as_ref(),
717 caption: owned.caption,
718 toggle: owned.toggle.as_ref(),
719 image: owned.image.as_ref(),
720 image_overlay: owned.image_overlay.as_ref(),
721 image_overlay_corner: owned.image_overlay_corner,
722 fence: owned.fence.as_ref(),
723 sizing: owned.sizing,
724 table_controls: owned.table_controls,
725 copy: owned.copy,
726 base: owned.base.as_deref(),
727 highlight: owned.highlight,
728 find: owned.find,
729 };
730 block_box(
731 &owned.blocks[ix],
732 overlay,
733 &owned.typography,
734 &owned.theme,
735 window,
736 cx,
737 )
738 .into_any_element()
739 }),
740 }
741 .into_any_element()
742}
743
744struct Owned {
746 blocks: Vec<Block>,
747 selection: Option<Selection>,
748 caret_on: bool,
749 layouts: BlockLayouts,
750 annotations: Vec<(Selection, Annotation)>,
751 placeholder: Option<SharedString>,
752 caption: Caption,
753 toggle: Option<Toggle>,
754 image: Option<OnImage>,
755 image_overlay: Option<ImageOverlay>,
756 image_overlay_corner: ImageOverlayCorner,
757 fence: Option<FenceHost>,
758 sizing: Option<(usize, u32)>,
759 table_controls: bool,
760 copy: CopyButton,
761 base: Option<std::path::PathBuf>,
762 highlight: crate::HighlightPaint,
763 find: crate::FindPaint,
764 typography: Typography,
765 theme: Theme,
766}
767
768fn block_box(
771 block: &Block,
772 overlay: Overlay,
773 typography: &Typography,
774 theme: &Theme,
775 window: &mut Window,
776 cx: &mut App,
777) -> gpui::Div {
778 let ix = overlay.block;
779 let frame = overlay.layouts.map(|layouts| {
782 let layouts = layouts.clone();
783 canvas(
784 move |bounds, _, _| layouts.record_block(ix, bounds),
785 |_, _, _, _| (),
786 )
787 .absolute()
788 .size_full()
789 });
790 div()
796 .w_full()
797 .pl(px(block.indent as f32 * INDENT_WIDTH))
798 .child(
799 div()
800 .w_full()
801 .relative()
802 .children(frame)
803 .when(overlay.covers_block() && block.opaque(), |el| {
807 el.rounded(px(4.0)).bg(theme.selection)
808 })
809 .child(block_element(block, overlay, typography, theme, window, cx)),
810 )
811}
812
813fn block_key(block: &Block, typography: &Typography, table_controls: bool) -> u64 {
816 let mut hasher = DefaultHasher::new();
817 table_controls.hash(&mut hasher);
818 block.hash(&mut hasher);
819 typography.body.size().to_bits().hash(&mut hasher);
820 typography.body.line_height().to_bits().hash(&mut hasher);
821 hasher.finish()
822}
823
824fn guess(block: &Block, typography: &Typography) -> Guess {
826 let body = px(typography.body.line_height());
827 let prose = |chars: usize, line: Pixels| Guess {
828 chars,
829 line,
830 rows: 0,
831 extra: px(0.0),
832 indent: px(0.0),
833 };
834 let guess = match &block.kind {
835 BlockKind::Paragraph(text)
836 | BlockKind::Bullet(text)
837 | BlockKind::Ordered { text, .. }
838 | BlockKind::Task { text, .. }
839 | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
840 BlockKind::Heading { level, text } => prose(
841 text.text.len(),
842 px(typography.heading(*level).line_height()),
843 ),
844 BlockKind::Code { code, .. } => Guess {
845 rows: code.text.lines().count().max(1),
846 line: px(typography.code.line_height()),
847 extra: px(2.0 * CODE_PADDING_Y) + body,
848 ..prose(0, body)
849 },
850 BlockKind::Table { rows, .. } => Guess {
851 rows: rows.len() + 1,
852 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
853 ..prose(0, body)
854 },
855 BlockKind::Image { alt, .. } => Guess {
856 extra: px(240.0),
857 ..prose(alt.text.len(), px(typography.caption.line_height()))
858 },
859 BlockKind::Bookmark { .. } => Guess {
860 extra: body * 3.0,
861 ..prose(0, body)
862 },
863 BlockKind::Rule => Guess {
864 extra: body,
865 ..prose(0, px(0.0))
866 },
867 };
868 Guess {
869 indent: px(block.indent as f32 * INDENT_WIDTH),
870 ..guess
871 }
872}
873
874fn tight(previous: &Block, next: &Block) -> bool {
876 let marker = |block: &Block| {
877 matches!(
878 block.kind,
879 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
880 )
881 };
882 marker(previous) && (marker(next) || next.indent > previous.indent)
883}
884
885fn block_element(
886 block: &Block,
887 overlay: Overlay,
888 typography: &Typography,
889 theme: &Theme,
890 window: &mut Window,
891 cx: &mut App,
892) -> AnyElement {
893 let body = overlay.at(Part::Body);
894 match &block.kind {
895 BlockKind::Paragraph(text) => text_element(
896 text,
897 typography.body.size(),
898 typography.body.line_height(),
899 FontWeight::NORMAL,
900 body,
901 theme,
902 cx,
903 ),
904 BlockKind::Heading { level, text } => {
905 let heading = typography.heading(*level);
906 text_element(
907 text,
908 heading.size(),
909 heading.line_height(),
910 heading.weight,
911 body,
912 theme,
913 cx,
914 )
915 }
916 BlockKind::Bullet(text) => {
917 marker_row(disc(typography, theme), text, body, typography, theme, cx)
918 }
919 BlockKind::Ordered { number, text } => marker_row(
920 div()
921 .flex_none()
922 .w(px(MARKER_WIDTH))
923 .text_size(px(typography.body.size()))
924 .line_height(px(typography.body.line_height()))
925 .text_color(theme.text_muted)
926 .child(SharedString::from(format!("{number}.")))
927 .into_any_element(),
928 text,
929 body,
930 typography,
931 theme,
932 cx,
933 ),
934 BlockKind::Task { checked, text } => marker_row(
935 checkbox(*checked, overlay, typography, theme),
936 text,
937 body,
938 typography,
939 theme,
940 cx,
941 ),
942 BlockKind::Quote { kind, text } => div()
943 .border_l_2()
944 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
945 .pl(px(12.0))
946 .pr(px(10.0))
947 .py(px(2.0))
948 .text_color(theme.text_muted)
949 .children(kind.map(|kind| {
950 div()
951 .pb(px(2.0))
952 .text_size(px(typography.body.size()))
953 .line_height(px(typography.body.line_height()))
954 .font_weight(FontWeight::SEMIBOLD)
955 .text_color(alert_color(kind, theme))
956 .child(kind.label())
957 }))
958 .child(text_element(
959 text,
960 typography.body.size(),
961 typography.body.line_height(),
962 FontWeight::NORMAL,
963 body,
964 theme,
965 cx,
966 ))
967 .into_any_element(),
968 BlockKind::Code {
969 language,
970 code,
971 height,
972 } => {
973 let overlay = overlay.at(Part::Code);
974 let height = overlay.held_height().or(*height);
975 let painted = overlay
979 .caret()
980 .is_none()
981 .then(|| {
982 let fence = block::Fence {
983 language: language.as_deref()?,
984 code: &code.text,
985 height,
986 rewrite: overlay.fence.map(|host| {
987 let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
988 Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
989 rewrite(ix, code, window, cx)
990 }) as block::Rewrite
991 }),
992 };
993 block::render(&fence, window, cx)
994 })
995 .flatten();
996 match painted {
997 Some(element) => div()
1000 .key_context(PAINTED_CONTEXT)
1001 .relative()
1002 .when_some(overlay.fence, |el, host| {
1003 let (leave, ix) = (host.leave.clone(), overlay.block);
1004 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1005 })
1006 .when(overlay.covers_block(), |el| {
1007 el.rounded(px(4.0)).bg(theme.selection)
1008 })
1009 .group(painted_group(overlay.block))
1010 .child(element)
1011 .children(resizable(&overlay, theme))
1012 .into_any_element(),
1013 None => code_block(
1014 language.as_deref(),
1015 &code.text,
1016 overlay,
1017 typography,
1018 theme,
1019 window,
1020 cx,
1021 ),
1022 }
1023 }
1024 BlockKind::Image { url, alt, width } => {
1025 image(url, alt, *width, overlay, typography, theme, window, cx)
1026 }
1027 BlockKind::Bookmark { url, form } => match crate::preview::card(
1028 url,
1029 match (*form, overlay.held_height()) {
1030 (Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
1031 (form, _) => form,
1032 },
1033 window,
1034 cx,
1035 ) {
1036 Some(element) => div()
1039 .key_context(PAINTED_CONTEXT)
1040 .relative()
1041 .when_some(overlay.fence, |el, host| {
1042 let (leave, ix) = (host.leave.clone(), overlay.block);
1043 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1044 })
1045 .when(overlay.covers_block(), |el| {
1046 el.rounded(px(4.0)).bg(theme.selection)
1047 })
1048 .group(painted_group(overlay.block))
1049 .child(element)
1050 .children(match form {
1051 Form::Embed(_) => resizable(&overlay, theme),
1052 _ => Vec::new(),
1053 })
1054 .into_any_element(),
1055 None => bookmark(overlay.block, url, *form, typography, theme, cx),
1056 },
1057 BlockKind::Table {
1058 align,
1059 header,
1060 rows,
1061 } => table(align, header, rows, overlay, typography, theme, window, cx),
1062 BlockKind::Rule => div()
1063 .h(px(1.0))
1064 .w_full()
1065 .bg(theme.border)
1066 .into_any_element(),
1067 }
1068}
1069
1070fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
1072 div()
1073 .flex_none()
1074 .w(px(MARKER_WIDTH))
1075 .h(px(typography.body.line_height()))
1076 .flex()
1077 .items_center()
1078 .child(
1079 div()
1080 .ml(px(1.0))
1081 .w(px(5.0))
1082 .h(px(5.0))
1083 .rounded_full()
1084 .bg(theme.text_faint),
1085 )
1086 .into_any_element()
1087}
1088
1089fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
1090 let ix = overlay.block;
1091 let mut box_ = div()
1092 .relative()
1093 .w(px(13.0))
1094 .h(px(13.0))
1095 .rounded(px(3.5))
1096 .border_1()
1097 .flex()
1098 .items_center()
1099 .justify_center();
1100 box_ = if checked {
1101 box_.bg(theme.solid)
1102 .border_color(theme.solid)
1103 .text_style(TextStyle::Caption)
1104 .text_color(theme.on_solid)
1105 .child("✓")
1106 } else {
1107 box_.border_color(theme.border_strong)
1108 };
1109 box_ = box_.children(overlay.layouts.map(|layouts| {
1112 let layouts = layouts.clone();
1113 canvas(
1114 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1115 |_, _, _, _| (),
1116 )
1117 .absolute()
1118 .size_full()
1119 }));
1120 if overlay.toggle.is_some() {
1123 box_ = box_.cursor_pointer();
1124 }
1125 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1126 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1127 cx.stop_propagation();
1131 toggle(ix, window, cx);
1132 });
1133 }
1134
1135 div()
1136 .flex_none()
1137 .w(px(MARKER_WIDTH))
1138 .h(px(typography.body.line_height()))
1139 .flex()
1140 .items_center()
1141 .child(box_)
1142 .into_any_element()
1143}
1144
1145fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1147 match kind {
1148 QuoteKind::Note => theme.accent,
1149 QuoteKind::Tip => theme.success,
1150 QuoteKind::Important => theme.busy,
1151 QuoteKind::Warning => theme.warning,
1152 QuoteKind::Caution => theme.danger,
1153 }
1154}
1155
1156fn marker_row(
1157 marker: AnyElement,
1158 text: &Text,
1159 overlay: Overlay,
1160 typography: &Typography,
1161 theme: &Theme,
1162 cx: &App,
1163) -> AnyElement {
1164 div()
1165 .flex()
1166 .flex_row()
1167 .gap(px(MARKER_GAP))
1168 .child(marker)
1169 .child(div().flex_1().min_w_0().child(text_element(
1170 text,
1171 typography.body.size(),
1172 typography.body.line_height(),
1173 FontWeight::NORMAL,
1174 overlay,
1175 theme,
1176 cx,
1177 )))
1178 .into_any_element()
1179}