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 _, 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 app::{preview, typography::Typography},
32 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Run, Text, quote_runs},
33 fence::{
34 self, CODE_PADDING_X, CODE_PADDING_Y, copy_button, fence_band, fence_note, fence_panel,
35 },
36 select::{Affinity, Cursor, Selection},
37};
38
39mod code;
41mod column;
42mod layouts;
43mod media;
44pub use media::{Bookmark, hover_bookmark};
45pub mod selectable;
46mod source;
47mod table;
48mod text;
49
50use code::*;
51use column::*;
52pub use layouts::*;
53use media::*;
54pub use source::*;
55use table::*;
56pub use text::*;
57
58const BLOCK_GAP: f32 = 12.0;
59const LIST_GAP: f32 = 4.0;
60const INDENT_WIDTH: f32 = 22.0;
62const MARKER_WIDTH: f32 = 18.0;
64const MARKER_GAP: f32 = 8.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>>;
203
204pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
206
207pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
209
210pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
213
214#[derive(Clone)]
217pub struct FenceHost {
218 pub rewrite: OnRewrite,
221 pub leave: OnLeave,
224 pub resize: Option<OnResize>,
227}
228
229pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
232
233gpui::actions!(
234 markdown,
235 [
236 LeaveBlock
238 ]
239);
240
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
243pub enum ImageOverlayCorner {
244 TopLeft,
245 TopRight,
246 BottomLeft,
247 #[default]
248 BottomRight,
249}
250
251#[derive(Clone)]
257pub enum Toggle {
258 Handled(OnToggle),
261 HitTested,
267}
268
269#[derive(Clone)]
275pub struct Editing<'a> {
276 pub selection: Option<Selection>,
279 pub caret_on: bool,
282 pub layouts: Option<&'a BlockLayouts>,
288 pub annotations: &'a [(Selection, Annotation)],
290 pub placeholder: Option<SharedString>,
292 pub caption: Caption,
293 pub typography: Option<Typography>,
297 pub text_color: Option<Hsla>,
298 pub toggle: Option<Toggle>,
301 pub image: Option<OnImage>,
305 pub image_menu: Option<OnImageMenu>,
310 pub image_overlay: Option<ImageOverlay>,
313 pub image_overlay_corner: ImageOverlayCorner,
315 pub fence: Option<FenceHost>,
318 pub sizing: Option<(usize, u32)>,
321 pub table_controls: bool,
323 pub copy: CopyButton,
325 pub base: Option<&'a Path>,
328 pub keep: &'a [usize],
333 pub scroll: Option<&'a gpui::ScrollHandle>,
337 pub jump: Option<OnJump>,
341}
342
343impl Editing<'_> {
344 fn theme(&self, cx: &App) -> Theme {
345 let mut theme = Theme::of(cx).clone();
346 if let Some(color) = self.text_color {
347 theme.text = color;
348 }
349 theme
350 }
351}
352
353impl Default for Editing<'_> {
354 fn default() -> Self {
355 Self {
356 selection: None,
357 caret_on: true,
360 layouts: None,
361 annotations: &[],
362 placeholder: None,
363 caption: Caption::default(),
364 typography: None,
365 text_color: None,
366 toggle: None,
367 image: None,
368 image_menu: None,
369 image_overlay: None,
370 image_overlay_corner: ImageOverlayCorner::BottomRight,
371 fence: None,
372 sizing: None,
373 table_controls: false,
374 copy: CopyButton::default(),
375 base: None,
376 keep: &[],
377 scroll: None,
378 jump: None,
379 }
380 }
381}
382
383#[derive(Clone, Copy)]
389struct Overlay<'a> {
390 block: usize,
391 part: Part,
392 selection: Option<Selection>,
393 caret_on: bool,
394 caret_shape: ui::input::CaretShape,
395 caret_height: ui::input::CaretHeight,
396 caret_inactive: ui::input::InactiveCaret,
397 window_active: bool,
399 layouts: Option<&'a BlockLayouts>,
400 annotations: &'a [(Selection, Annotation)],
402 placeholder: Option<&'a SharedString>,
405 caption: Caption,
406 toggle: Option<&'a Toggle>,
409 image: Option<&'a OnImage>,
410 image_menu: Option<&'a OnImageMenu>,
411 image_overlay: Option<&'a ImageOverlay>,
412 image_overlay_corner: ImageOverlayCorner,
413 fence: Option<&'a FenceHost>,
414 sizing: Option<(usize, u32)>,
415 table_controls: bool,
416 copy: CopyButton,
417 base: Option<&'a Path>,
418 highlight: crate::HighlightPaint,
419 find: crate::FindPaint,
420 comment: crate::CommentPaint,
421 jump: Option<&'a crate::app::link::Jump>,
422}
423
424fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
428 let ix = overlay.block;
429 let mut parts = Vec::new();
430 if let Some(layouts) = overlay.layouts {
431 let layouts = layouts.clone();
432 parts.push(
433 canvas(
434 move |bounds, _, _| layouts.record_painted(ix, bounds),
435 |_, _, _, _| (),
436 )
437 .absolute()
438 .size_full()
439 .into_any_element(),
440 );
441 }
442 if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
443 let dragging = overlay.held_height().is_some();
444 parts.push(
449 div()
450 .absolute()
451 .left_0()
452 .right_0()
453 .bottom_0()
454 .flex()
455 .justify_center()
456 .child(
457 theme
458 .grip_handle()
459 .id(ElementId::named_usize("painted-grip", ix))
460 .debug_selector(|| "painted-grip".into())
461 .w(px(PAINTED_GRIP_WIDTH))
462 .when(!dragging, |el| {
463 el.invisible()
464 .group_hover(painted_group(ix), |el| el.visible())
465 })
466 .on_mouse_down(MouseButton::Left, move |event, window, cx| {
467 cx.stop_propagation();
468 resize(ix, event.position.y, window, cx);
469 }),
470 )
471 .into_any_element(),
472 );
473 }
474 parts
475}
476
477const PAINTED_GRIP_WIDTH: f32 = 120.0;
479
480fn painted_group(ix: usize) -> SharedString {
481 SharedString::from(format!("painted-{ix}"))
482}
483
484impl<'a> Overlay<'a> {
485 fn at(self, part: Part) -> Self {
486 Self { part, ..self }
487 }
488
489 fn held_height(&self) -> Option<u32> {
491 self.sizing
492 .filter(|(ix, _)| *ix == self.block)
493 .map(|(_, height)| height)
494 }
495
496 fn here(&self) -> Cursor {
497 Cursor::new(self.block, self.part, 0)
498 }
499
500 fn caret_painted(&self) -> Option<usize> {
507 let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
508 let shown = self
509 .caret_shape
510 .shown(self.window_active, self.caret_inactive);
511 (self.caret_on && collapsed && shown)
512 .then(|| self.caret())
513 .flatten()
514 }
515
516 fn affinity(&self) -> Affinity {
518 self.selection
519 .map_or_else(Affinity::default, |selection| selection.affinity)
520 }
521
522 fn covered(&self, text: &str, offset: usize) -> Option<Range<usize>> {
525 (self.affinity() == Affinity::Downstream)
526 .then(|| glyph_at(text, offset))
527 .flatten()
528 }
529
530 fn caret_hollow(&self) -> bool {
531 !self.window_active
532 }
533
534 fn caret(&self) -> Option<usize> {
536 self.selection
537 .map(|selection| selection.head)
538 .filter(|head| head.block == self.block && head.part == self.part)
539 .map(|head| head.offset)
540 }
541
542 fn selected(&self, len: usize) -> Option<Range<usize>> {
544 self.clip(self.selection?, len)
545 }
546
547 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
550 self.annotations
551 .iter()
552 .filter_map(|(range, kind)| {
553 Some((
554 self.clip(*range, len)?,
555 kind.wash(theme, self.highlight, self.find, self.comment),
556 ))
557 })
558 .collect()
559 }
560
561 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
567 if selection.is_collapsed() {
568 return None;
569 }
570 let (start, end) = selection.ordered();
571 let here = self.here();
572 let (first, last) = (
573 Cursor::new(start.block, start.part, 0),
574 Cursor::new(end.block, end.part, 0),
575 );
576 if here < first || here > last {
577 return None;
578 }
579 let from = if here == first { start.offset } else { 0 };
580 let to = if here == last { end.offset } else { len };
581 (from < to).then_some(from..to.min(len))
582 }
583
584 fn covers_block(&self) -> bool {
588 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
589 return false;
590 };
591 let (start, end) = selection.ordered();
592 start.block < self.block && self.block < end.block
593 }
594}
595
596pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
602 if url.contains("://") {
603 return SharedString::from(url.to_string()).into();
604 }
605 match base {
608 Some(base) => base.join(url).into(),
609 None => std::path::PathBuf::from(url).into(),
610 }
611}
612
613pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
615 let doc = crate::parse_with(source, &cx.marks());
616 render(&doc, Caption::default(), window, cx)
617}
618
619pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
621 render_with(
622 doc,
623 Editing {
624 caption,
625 ..Editing::default()
626 },
627 window,
628 cx,
629 )
630}
631
632pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
640 let theme = editing.theme(cx);
641 let Editing {
642 selection,
643 caret_on,
644 layouts,
645 annotations,
646 placeholder,
647 caption,
648 typography,
649 text_color: _,
650 toggle,
651 image,
652 image_menu,
653 image_overlay,
654 image_overlay_corner,
655 fence,
656 sizing,
657 table_controls,
658 copy,
659 base,
660 keep,
661 scroll,
662 jump,
663 } = editing;
664 let typography = typography.unwrap_or_else(|| cx.typography());
665 let highlight = crate::paint::marks::highlight_paint_of(cx);
666 let find = crate::paint::find::find_paint_of(cx);
667 let comment = crate::paint::comment::comment_paint_of(cx);
668 let jump = jump.map(|to| crate::app::link::Jump::new(doc, to));
669 let gaps: Vec<Pixels> = doc
670 .blocks
671 .iter()
672 .enumerate()
673 .map(|(ix, block)| {
674 px(match doc.blocks.get(ix.wrapping_sub(1)) {
675 None => 0.0,
676 Some(previous) if tight(previous, block) => LIST_GAP,
677 Some(_) => BLOCK_GAP,
678 })
679 })
680 .collect();
681 let runs = quote_runs(doc.blocks.iter().map(|block| (block.quote, block.indent)));
682
683 let Some(layouts) = layouts else {
684 let mut column = div().flex().flex_col();
685 for (ix, block) in doc.blocks.iter().enumerate() {
686 let overlay = Overlay {
687 block: ix,
688 part: Part::Body,
689 selection,
690 caret_on,
691 caret_shape: cx.caret_shape(),
692 caret_height: cx.caret_height(),
693 caret_inactive: cx.inactive_caret(),
694 window_active: window.is_window_active(),
695 layouts: None,
696 annotations,
697 placeholder: placeholder.as_ref(),
698 caption,
699 toggle: toggle.as_ref(),
700 image: image.as_ref(),
701 image_menu: image_menu.as_ref(),
702 image_overlay: image_overlay.as_ref(),
703 image_overlay_corner,
704 fence: fence.as_ref(),
705 sizing,
706 table_controls,
707 copy,
708 base,
709 highlight,
710 find,
711 comment,
712 jump: jump.as_ref(),
713 };
714 let quote = Quote {
715 run: runs[ix],
716 gap: gaps[ix],
717 };
718 column = column.child(
719 block_box(block, quote, overlay, &typography, &theme, window, cx).mt(gaps[ix]),
720 );
721 }
722 return column.into_any_element();
723 };
724
725 let keys: Vec<u64> = doc
726 .blocks
727 .iter()
728 .zip(&runs)
729 .map(|(block, run)| block_key(block, *run, &typography, table_controls))
730 .collect();
731 layouts.prune(&keys);
732 let guesses: Vec<Guess> = doc
733 .blocks
734 .iter()
735 .zip(&runs)
736 .map(|(block, run)| {
737 let mut guess = guess(block, *run, &typography);
738 if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
739 guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
740 }
741 guess
742 })
743 .collect();
744 let mut kept = keep.to_vec();
745 kept.extend(selection.map(|selection| selection.head.block));
746 kept.extend(selection.map(|selection| selection.anchor.block));
747 let owned = Owned {
748 blocks: doc.blocks.clone(),
749 runs,
750 gaps: gaps.clone(),
751 selection,
752 caret_on,
753 layouts: layouts.clone(),
754 annotations: annotations.to_vec(),
755 placeholder,
756 caption,
757 toggle,
758 image,
759 image_menu,
760 image_overlay,
761 image_overlay_corner,
762 fence,
763 sizing,
764 table_controls,
765 copy,
766 base: base.map(Path::to_path_buf),
767 highlight,
768 find,
769 comment,
770 jump,
771 typography,
772 theme,
773 };
774 Column {
775 layouts: layouts.clone(),
776 keys: keys.into(),
777 gaps: gaps.into(),
778 guesses: guesses.into(),
779 keep: kept,
780 scroll: scroll.cloned(),
781 item_of: Rc::new(|at| at.block),
782 build: Box::new(move |ix, window, cx| {
783 let overlay = Overlay {
784 block: ix,
785 part: Part::Body,
786 selection: owned.selection,
787 caret_on: owned.caret_on,
788 caret_shape: cx.caret_shape(),
789 caret_height: cx.caret_height(),
790 caret_inactive: cx.inactive_caret(),
791 window_active: window.is_window_active(),
792 layouts: Some(&owned.layouts),
793 annotations: &owned.annotations,
794 placeholder: owned.placeholder.as_ref(),
795 caption: owned.caption,
796 toggle: owned.toggle.as_ref(),
797 image: owned.image.as_ref(),
798 image_menu: owned.image_menu.as_ref(),
799 image_overlay: owned.image_overlay.as_ref(),
800 image_overlay_corner: owned.image_overlay_corner,
801 fence: owned.fence.as_ref(),
802 sizing: owned.sizing,
803 table_controls: owned.table_controls,
804 copy: owned.copy,
805 base: owned.base.as_deref(),
806 highlight: owned.highlight,
807 find: owned.find,
808 comment: owned.comment,
809 jump: owned.jump.as_ref(),
810 };
811 let quote = Quote {
812 run: owned.runs[ix],
813 gap: owned.gaps[ix],
814 };
815 block_box(
816 &owned.blocks[ix],
817 quote,
818 overlay,
819 &owned.typography,
820 &owned.theme,
821 window,
822 cx,
823 )
824 .into_any_element()
825 }),
826 }
827 .into_any_element()
828}
829
830struct Owned {
832 blocks: Vec<Block>,
833 runs: Vec<Option<Run>>,
834 gaps: Vec<Pixels>,
835 selection: Option<Selection>,
836 caret_on: bool,
837 layouts: BlockLayouts,
838 annotations: Vec<(Selection, Annotation)>,
839 placeholder: Option<SharedString>,
840 caption: Caption,
841 toggle: Option<Toggle>,
842 image: Option<OnImage>,
843 image_menu: Option<OnImageMenu>,
844 image_overlay: Option<ImageOverlay>,
845 image_overlay_corner: ImageOverlayCorner,
846 fence: Option<FenceHost>,
847 sizing: Option<(usize, u32)>,
848 table_controls: bool,
849 copy: CopyButton,
850 base: Option<std::path::PathBuf>,
851 highlight: crate::HighlightPaint,
852 find: crate::FindPaint,
853 comment: crate::CommentPaint,
854 jump: Option<crate::app::link::Jump>,
855 typography: Typography,
856 theme: Theme,
857}
858
859#[derive(Clone, Copy)]
862struct Quote {
863 run: Option<Run>,
864 gap: Pixels,
865}
866
867const QUOTE_INSET: f32 = 12.0;
869const QUOTE_BAR: f32 = 2.0;
870
871fn block_box(
874 block: &Block,
875 quote: Quote,
876 overlay: Overlay,
877 typography: &Typography,
878 theme: &Theme,
879 window: &mut Window,
880 cx: &mut App,
881) -> gpui::Div {
882 let ix = overlay.block;
883 let frame = overlay.layouts.map(|layouts| {
886 let layouts = layouts.clone();
887 canvas(
888 move |bounds, _, _| layouts.record_block(ix, bounds),
889 |_, _, _, _| (),
890 )
891 .absolute()
892 .size_full()
893 });
894 div()
900 .w_full()
901 .font_family(theme.font_body.clone())
904 .pl(px(block.indent as f32 * INDENT_WIDTH))
905 .child(
906 div()
907 .w_full()
908 .relative()
909 .children(frame)
910 .when(overlay.covers_block() && block.opaque(), |el| {
914 el.rounded(px(4.0)).bg(theme.selection)
915 })
916 .child(quoted(
917 block,
918 quote,
919 block_element(block, overlay, typography, theme, window, cx),
920 typography,
921 theme,
922 )),
923 )
924}
925
926fn quoted(
932 block: &Block,
933 quote: Quote,
934 element: AnyElement,
935 typography: &Typography,
936 theme: &Theme,
937) -> AnyElement {
938 let (Some(run), Some(quoted)) = (quote.run, block.quote) else {
939 return element;
940 };
941 let color = quoted
942 .alert
943 .map_or(theme.border_strong, |alert| alert_color(alert, theme));
944 let label = quoted.alert.filter(|_| run.first).map(|alert| {
945 div()
946 .pb(px(2.0))
947 .text_size(px(typography.body.size()))
948 .line_height(px(typography.body.line_height()))
949 .font_weight(FontWeight::SEMIBOLD)
950 .text_color(color)
951 .child(alert.label())
952 });
953 let bar = div()
954 .absolute()
955 .left(px(
956 -((block.indent - run.level.min(block.indent)) as f32) * INDENT_WIDTH
957 ))
958 .top(if run.first { px(0.0) } else { -quote.gap })
959 .bottom_0()
960 .w(px(QUOTE_BAR))
961 .bg(color);
962 div()
963 .child(bar)
964 .pl(px(QUOTE_BAR + QUOTE_INSET))
965 .pr(px(10.0))
966 .py(px(2.0))
967 .text_color(theme.text_muted)
968 .children(label)
969 .child(element)
970 .into_any_element()
971}
972
973fn block_key(
976 block: &Block,
977 run: Option<Run>,
978 typography: &Typography,
979 table_controls: bool,
980) -> u64 {
981 let mut hasher = DefaultHasher::new();
982 table_controls.hash(&mut hasher);
983 block.hash(&mut hasher);
984 run.map(|run| run.first).hash(&mut hasher);
985 typography.body.size().to_bits().hash(&mut hasher);
986 typography.body.line_height().to_bits().hash(&mut hasher);
987 hasher.finish()
988}
989
990fn guess(block: &Block, run: Option<Run>, typography: &Typography) -> Guess {
992 let body = px(typography.body.line_height());
993 let prose = |chars: usize, line: Pixels| Guess {
994 chars,
995 line,
996 rows: 0,
997 extra: px(0.0),
998 indent: px(0.0),
999 };
1000 let guess = match &block.kind {
1001 BlockKind::Paragraph(text)
1002 | BlockKind::Bullet(text)
1003 | BlockKind::Ordered { text, .. }
1004 | BlockKind::Task { text, .. } => prose(text.text.len(), body),
1005 BlockKind::Heading { level, text } => prose(
1006 text.text.len(),
1007 px(typography.heading(*level).line_height()),
1008 ),
1009 BlockKind::Code { code, .. } => Guess {
1010 rows: code.text.lines().count().max(1),
1011 line: px(typography.code.line_height()),
1012 extra: px(2.0 * CODE_PADDING_Y) + body,
1013 ..prose(0, body)
1014 },
1015 BlockKind::Table { rows, .. } => Guess {
1016 rows: rows.len() + 1,
1017 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
1018 ..prose(0, body)
1019 },
1020 BlockKind::Image { alt, .. } => Guess {
1021 extra: px(240.0),
1022 ..prose(alt.text.len(), px(typography.caption.line_height()))
1023 },
1024 BlockKind::Bookmark { .. } => Guess {
1025 extra: body * 3.0,
1026 ..prose(0, body)
1027 },
1028 BlockKind::Rule => Guess {
1029 extra: body,
1030 ..prose(0, px(0.0))
1031 },
1032 };
1033 let quoted = run.is_some();
1034 let label = run.is_some_and(|run| run.first) && block.quote.is_some_and(|q| q.alert.is_some());
1035 Guess {
1036 indent: px(block.indent as f32 * INDENT_WIDTH)
1037 + if quoted {
1038 px(QUOTE_BAR + QUOTE_INSET + 10.0)
1039 } else {
1040 px(0.0)
1041 },
1042 extra: guess.extra
1043 + if quoted { px(4.0) } else { px(0.0) }
1044 + if label { body + px(2.0) } else { px(0.0) },
1045 ..guess
1046 }
1047}
1048
1049fn tight(previous: &Block, next: &Block) -> bool {
1051 let marker = |block: &Block| {
1052 matches!(
1053 block.kind,
1054 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
1055 )
1056 };
1057 marker(previous) && (marker(next) || next.indent > previous.indent)
1058}
1059
1060fn block_element(
1061 block: &Block,
1062 overlay: Overlay,
1063 typography: &Typography,
1064 theme: &Theme,
1065 window: &mut Window,
1066 cx: &mut App,
1067) -> AnyElement {
1068 let body = overlay.at(Part::Body);
1069 match &block.kind {
1070 BlockKind::Paragraph(text) => text_element(
1071 text,
1072 typography.body.size(),
1073 typography.body.line_height(),
1074 FontWeight::NORMAL,
1075 body,
1076 theme,
1077 cx,
1078 ),
1079 BlockKind::Heading { level, text } => {
1080 let heading = typography.heading(*level);
1081 text_element(
1082 text,
1083 heading.size(),
1084 heading.line_height(),
1085 heading.weight,
1086 body,
1087 theme,
1088 cx,
1089 )
1090 }
1091 BlockKind::Bullet(text) => {
1092 marker_row(disc(typography, theme), text, body, typography, theme, cx)
1093 }
1094 BlockKind::Ordered { number, text } => marker_row(
1095 div()
1096 .flex_none()
1097 .w(px(MARKER_WIDTH))
1098 .text_size(px(typography.body.size()))
1099 .line_height(px(typography.body.line_height()))
1100 .text_color(theme.text_muted)
1101 .child(SharedString::from(format!("{number}.")))
1102 .into_any_element(),
1103 text,
1104 body,
1105 typography,
1106 theme,
1107 cx,
1108 ),
1109 BlockKind::Task { checked, text } => marker_row(
1110 checkbox(*checked, overlay, typography, theme),
1111 text,
1112 body,
1113 typography,
1114 theme,
1115 cx,
1116 ),
1117 BlockKind::Code {
1118 language,
1119 code,
1120 height,
1121 } => {
1122 let overlay = overlay.at(Part::Code);
1123 let height = overlay.held_height().or(*height);
1124 let fence = overlay
1128 .caret()
1129 .is_none()
1130 .then(|| {
1131 Some(fence::Fence {
1132 language: language.as_deref()?,
1133 code: &code.text,
1134 height,
1135 rewrite: overlay.fence.map(|host| {
1136 let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
1137 Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
1138 rewrite(ix, code, window, cx)
1139 }) as fence::Rewrite
1140 }),
1141 })
1142 })
1143 .flatten();
1144 let painted = match fence {
1145 Some(fence) => fence::render(&fence, overlay.block, window, cx),
1146 None => fence::Painted::Code(None),
1147 };
1148 match painted {
1149 fence::Painted::Block(element) => div()
1152 .key_context(PAINTED_CONTEXT)
1153 .relative()
1154 .when_some(overlay.fence, |el, host| {
1155 let (leave, ix) = (host.leave.clone(), overlay.block);
1156 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1157 })
1158 .when(overlay.covers_block(), |el| {
1159 el.rounded(px(4.0)).bg(theme.selection)
1160 })
1161 .group(painted_group(overlay.block))
1162 .child(element)
1163 .children(resizable(&overlay, theme))
1164 .into_any_element(),
1165 fence::Painted::Code(note) => code_block(
1166 language.as_deref(),
1167 &code.text,
1168 note,
1169 overlay,
1170 typography,
1171 theme,
1172 window,
1173 cx,
1174 ),
1175 }
1176 }
1177 BlockKind::Image { url, alt, width } => {
1178 image(url, alt, *width, overlay, typography, theme, window, cx)
1179 }
1180 BlockKind::Bookmark { url, form } => match crate::app::link::card(
1181 crate::Link::new(url, overlay.base),
1182 match (*form, overlay.held_height()) {
1183 (Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
1184 (form, _) => form,
1185 },
1186 window,
1187 cx,
1188 ) {
1189 Some(element) => div()
1192 .key_context(PAINTED_CONTEXT)
1193 .relative()
1194 .when_some(overlay.fence, |el, host| {
1195 let (leave, ix) = (host.leave.clone(), overlay.block);
1196 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1197 })
1198 .when(overlay.covers_block(), |el| {
1199 el.rounded(px(4.0)).bg(theme.selection)
1200 })
1201 .group(painted_group(overlay.block))
1202 .child(element)
1203 .children(match form {
1204 Form::Embed(_) => resizable(&overlay, theme),
1205 _ => Vec::new(),
1206 })
1207 .into_any_element(),
1208 None => bookmark(
1209 overlay.block,
1210 url,
1211 overlay.base,
1212 *form,
1213 typography,
1214 theme,
1215 cx,
1216 ),
1217 },
1218 BlockKind::Table {
1219 align,
1220 header,
1221 rows,
1222 } => table(align, header, rows, overlay, typography, theme, window, cx),
1223 BlockKind::Rule => div()
1224 .h(px(1.0))
1225 .w_full()
1226 .bg(theme.border)
1227 .into_any_element(),
1228 }
1229}
1230
1231fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
1233 div()
1234 .flex_none()
1235 .w(px(MARKER_WIDTH))
1236 .h(px(typography.body.line_height()))
1237 .flex()
1238 .items_center()
1239 .child(
1240 div()
1241 .ml(px(1.0))
1242 .w(px(5.0))
1243 .h(px(5.0))
1244 .rounded_full()
1245 .bg(theme.text_faint),
1246 )
1247 .into_any_element()
1248}
1249
1250fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
1251 let ix = overlay.block;
1252 let mut box_ = div()
1253 .relative()
1254 .w(px(13.0))
1255 .h(px(13.0))
1256 .rounded(px(3.5))
1257 .border_1()
1258 .flex()
1259 .items_center()
1260 .justify_center();
1261 box_ = if checked {
1262 box_.bg(theme.solid)
1263 .border_color(theme.solid)
1264 .text_style(TextStyle::Caption)
1265 .text_color(theme.on_solid)
1266 .child("✓")
1267 } else {
1268 box_.border_color(theme.border_strong)
1269 };
1270 box_ = box_.children(overlay.layouts.map(|layouts| {
1273 let layouts = layouts.clone();
1274 canvas(
1275 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1276 |_, _, _, _| (),
1277 )
1278 .absolute()
1279 .size_full()
1280 }));
1281 if overlay.toggle.is_some() {
1284 box_ = box_.cursor_pointer();
1285 }
1286 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1287 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1288 cx.stop_propagation();
1292 toggle(ix, window, cx);
1293 });
1294 }
1295
1296 div()
1297 .flex_none()
1298 .w(px(MARKER_WIDTH))
1299 .h(px(typography.body.line_height()))
1300 .flex()
1301 .items_center()
1302 .child(box_)
1303 .into_any_element()
1304}
1305
1306pub(crate) fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1308 match kind {
1309 QuoteKind::Note => theme.accent,
1310 QuoteKind::Tip => theme.success,
1311 QuoteKind::Important => theme.busy,
1312 QuoteKind::Warning => theme.warning,
1313 QuoteKind::Caution => theme.danger,
1314 }
1315}
1316
1317fn marker_row(
1318 marker: AnyElement,
1319 text: &Text,
1320 overlay: Overlay,
1321 typography: &Typography,
1322 theme: &Theme,
1323 cx: &App,
1324) -> AnyElement {
1325 div()
1326 .flex()
1327 .flex_row()
1328 .gap(px(MARKER_GAP))
1329 .child(marker)
1330 .child(div().flex_auto().min_w_0().child(text_element(
1331 text,
1332 typography.body.size(),
1333 typography.body.line_height(),
1334 FontWeight::NORMAL,
1335 overlay,
1336 theme,
1337 cx,
1338 )))
1339 .into_any_element()
1340}