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 _;
20
21use gpui::{
22 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
23 ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
24 StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
25 canvas, div, font, img, point, prelude::*, px, quad, size,
26};
27use theme::{TextStyle, Theme, Typeset};
28
29use crate::{
30 block,
31 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
32 preview,
33 select::{Cursor, Selection},
34 typography::Typography,
35};
36
37mod code;
39mod column;
40mod layouts;
41mod media;
42mod table;
43mod text;
44
45pub use code::*;
46use column::*;
47pub use layouts::*;
48use media::*;
49use table::*;
50pub use text::*;
51
52const BLOCK_GAP: f32 = 12.0;
53const LIST_GAP: f32 = 4.0;
54const INDENT_WIDTH: f32 = 22.0;
56const MARKER_WIDTH: f32 = 18.0;
58const MARKER_GAP: f32 = 8.0;
59const CODE_PADDING_X: f32 = 12.0;
61const CODE_PADDING_Y: f32 = 10.0;
62pub const PLAIN_LANGUAGE: &str = "Plain";
65const CARET_WIDTH: f32 = 1.5;
68const INLINE_CODE_RADIUS: f32 = 4.5;
71const INLINE_CODE_PAD_X: f32 = 2.0;
72const INLINE_CODE_INSET_Y: f32 = 2.0;
73const MENTION_CARD_WIDTH: f32 = 320.0;
75const CARD_HEIGHT: f32 = 116.0;
79const CARD_IMAGE_WIDTH: f32 = 180.0;
80const CARD_COVER_HEIGHT: f32 = 200.0;
81const CARD_PADDING: f32 = 14.0;
82const CARD_BORDER: f32 = 1.0;
83const CARD_ICON: f32 = 16.0;
84const CARD_COVER: f32 = 44.0;
85const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
87const CAPTION_GAP: f32 = 4.0;
88const IMAGE_EMPTY: &str = "Add an image";
90const CAPTION_HINT: &str = "Write a caption";
91const TABLE_CELL_PADDING: f32 = 12.0;
94pub const TABLE_CONTROL_SIZE: f32 = 16.0;
96const TABLE_DIVIDER: f32 = 1.0;
97const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
100const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
102
103#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
114pub enum Caption {
115 #[default]
117 Shown,
118 Hidden,
120}
121
122#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
126pub enum CopyButton {
127 #[default]
129 Shown,
130 Hidden,
133}
134
135#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
141#[non_exhaustive]
142pub enum Annotation {
143 #[default]
145 Open,
146 Resolved,
148 Active,
150 Highlight(crate::HighlightColor),
153 Match,
155 Current,
157}
158
159impl Annotation {
160 fn wash(self, theme: &Theme, highlight: crate::HighlightPaint, find: crate::FindPaint) -> Hsla {
161 match self {
162 Self::Open => theme.warning.opacity(0.20),
163 Self::Resolved => theme.warning.opacity(0.08),
164 Self::Active => theme.warning.opacity(0.38),
165 Self::Highlight(color) => highlight(color, theme),
166 Self::Match => find(theme).0,
167 Self::Current => find(theme).1,
168 }
169 }
170}
171
172pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
177
178pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
180
181pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
183
184pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
186
187pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
189
190#[derive(Clone)]
193pub struct FenceHost {
194 pub rewrite: OnRewrite,
197 pub leave: OnLeave,
200}
201
202pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
205
206gpui::actions!(
207 markdown,
208 [
209 LeaveBlock
211 ]
212);
213
214#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
216pub enum ImageOverlayCorner {
217 TopLeft,
218 TopRight,
219 BottomLeft,
220 #[default]
221 BottomRight,
222}
223
224#[derive(Clone)]
230pub enum Toggle {
231 Handled(OnToggle),
234 HitTested,
240}
241
242#[derive(Clone)]
248pub struct Editing<'a> {
249 pub selection: Option<Selection>,
252 pub caret_on: bool,
255 pub layouts: Option<&'a BlockLayouts>,
261 pub annotations: &'a [(Selection, Annotation)],
263 pub placeholder: Option<SharedString>,
265 pub caption: Caption,
266 pub typography: Option<Typography>,
270 pub toggle: Option<Toggle>,
273 pub image: Option<OnImage>,
277 pub image_overlay: Option<ImageOverlay>,
280 pub image_overlay_corner: ImageOverlayCorner,
282 pub fence: Option<FenceHost>,
285 pub table_controls: bool,
287 pub copy: CopyButton,
289 pub base: Option<&'a Path>,
292 pub keep: &'a [usize],
297 pub scroll: Option<&'a gpui::ScrollHandle>,
301}
302
303impl Default for Editing<'_> {
304 fn default() -> Self {
305 Self {
306 selection: None,
307 caret_on: true,
310 layouts: None,
311 annotations: &[],
312 placeholder: None,
313 caption: Caption::default(),
314 typography: None,
315 toggle: None,
316 image: None,
317 image_overlay: None,
318 image_overlay_corner: ImageOverlayCorner::BottomRight,
319 fence: None,
320 table_controls: false,
321 copy: CopyButton::default(),
322 base: None,
323 keep: &[],
324 scroll: None,
325 }
326 }
327}
328
329#[derive(Clone, Copy)]
335struct Overlay<'a> {
336 block: usize,
337 part: Part,
338 selection: Option<Selection>,
339 caret_on: bool,
340 caret_shape: ui::input::CaretShape,
341 caret_height: ui::input::CaretHeight,
342 window_active: bool,
344 layouts: Option<&'a BlockLayouts>,
345 annotations: &'a [(Selection, Annotation)],
347 placeholder: Option<&'a SharedString>,
350 caption: Caption,
351 toggle: Option<&'a Toggle>,
354 image: Option<&'a OnImage>,
355 image_overlay: Option<&'a ImageOverlay>,
356 image_overlay_corner: ImageOverlayCorner,
357 fence: Option<&'a FenceHost>,
358 table_controls: bool,
359 copy: CopyButton,
360 base: Option<&'a Path>,
361 highlight: crate::HighlightPaint,
362 find: crate::FindPaint,
363}
364
365impl<'a> Overlay<'a> {
366 fn at(self, part: Part) -> Self {
367 Self { part, ..self }
368 }
369
370 fn here(&self) -> Cursor {
371 Cursor::new(self.block, self.part, 0)
372 }
373
374 fn caret_painted(&self) -> Option<usize> {
381 let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
382 (self.caret_on && collapsed).then(|| self.caret()).flatten()
383 }
384
385 fn caret_hollow(&self) -> bool {
386 !self.window_active
387 }
388
389 fn caret(&self) -> Option<usize> {
391 self.selection
392 .map(|selection| selection.head)
393 .filter(|head| head.block == self.block && head.part == self.part)
394 .map(|head| head.offset)
395 }
396
397 fn selected(&self, len: usize) -> Option<Range<usize>> {
399 self.clip(self.selection?, len)
400 }
401
402 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
405 self.annotations
406 .iter()
407 .filter_map(|(range, kind)| {
408 Some((
409 self.clip(*range, len)?,
410 kind.wash(theme, self.highlight, self.find),
411 ))
412 })
413 .collect()
414 }
415
416 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
422 if selection.is_collapsed() {
423 return None;
424 }
425 let (start, end) = selection.ordered();
426 let here = self.here();
427 let (first, last) = (
428 Cursor::new(start.block, start.part, 0),
429 Cursor::new(end.block, end.part, 0),
430 );
431 if here < first || here > last {
432 return None;
433 }
434 let from = if here == first { start.offset } else { 0 };
435 let to = if here == last { end.offset } else { len };
436 (from < to).then_some(from..to.min(len))
437 }
438
439 fn covers_block(&self) -> bool {
443 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
444 return false;
445 };
446 let (start, end) = selection.ordered();
447 start.block < self.block && self.block < end.block
448 }
449}
450
451pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
457 if url.contains("://") {
458 return SharedString::from(url.to_string()).into();
459 }
460 match base {
463 Some(base) => base.join(url).into(),
464 None => std::path::PathBuf::from(url).into(),
465 }
466}
467
468pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
470 let doc = crate::parse_with(source, &cx.marks());
471 render(&doc, Caption::default(), window, cx)
472}
473
474pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
476 render_with(
477 doc,
478 Editing {
479 caption,
480 ..Editing::default()
481 },
482 window,
483 cx,
484 )
485}
486
487pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
495 let Editing {
496 selection,
497 caret_on,
498 layouts,
499 annotations,
500 placeholder,
501 caption,
502 typography,
503 toggle,
504 image,
505 image_overlay,
506 image_overlay_corner,
507 fence,
508 table_controls,
509 copy,
510 base,
511 keep,
512 scroll,
513 } = editing;
514 let theme = Theme::of(cx).clone();
517 let typography = typography.unwrap_or_else(|| cx.typography());
518 let highlight = crate::marks::highlight_paint_of(cx);
519 let find = crate::find::find_paint_of(cx);
520 let gaps: Vec<Pixels> = doc
521 .blocks
522 .iter()
523 .enumerate()
524 .map(|(ix, block)| {
525 px(match doc.blocks.get(ix.wrapping_sub(1)) {
526 None => 0.0,
527 Some(previous) if tight(previous, block) => LIST_GAP,
528 Some(_) => BLOCK_GAP,
529 })
530 })
531 .collect();
532
533 let Some(layouts) = layouts else {
534 let mut column = div().flex().flex_col();
535 for (ix, block) in doc.blocks.iter().enumerate() {
536 let overlay = Overlay {
537 block: ix,
538 part: Part::Body,
539 selection,
540 caret_on,
541 caret_shape: cx.caret_shape(),
542 caret_height: cx.caret_height(),
543 window_active: window.is_window_active(),
544 layouts: None,
545 annotations,
546 placeholder: placeholder.as_ref(),
547 caption,
548 toggle: toggle.as_ref(),
549 image: image.as_ref(),
550 image_overlay: image_overlay.as_ref(),
551 image_overlay_corner,
552 fence: fence.as_ref(),
553 table_controls,
554 copy,
555 base,
556 highlight,
557 find,
558 };
559 column = column
560 .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
561 }
562 return column.into_any_element();
563 };
564
565 let keys: Vec<u64> = doc
566 .blocks
567 .iter()
568 .map(|block| block_key(block, &typography, table_controls))
569 .collect();
570 layouts.prune(&keys);
571 let guesses: Vec<Guess> = doc
572 .blocks
573 .iter()
574 .map(|block| {
575 let mut guess = guess(block, &typography);
576 if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
577 guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
578 }
579 guess
580 })
581 .collect();
582 let mut kept = keep.to_vec();
583 kept.extend(selection.map(|selection| selection.head.block));
584 kept.extend(selection.map(|selection| selection.anchor.block));
585 let owned = Owned {
586 blocks: doc.blocks.clone(),
587 selection,
588 caret_on,
589 layouts: layouts.clone(),
590 annotations: annotations.to_vec(),
591 placeholder,
592 caption,
593 toggle,
594 image,
595 image_overlay,
596 image_overlay_corner,
597 fence,
598 table_controls,
599 copy,
600 base: base.map(Path::to_path_buf),
601 highlight,
602 find,
603 typography,
604 theme,
605 };
606 Column {
607 layouts: layouts.clone(),
608 keys: keys.into(),
609 gaps: gaps.into(),
610 guesses: guesses.into(),
611 keep: kept,
612 scroll: scroll.cloned(),
613 item_of: Box::new(|at| at.block),
614 build: Box::new(move |ix, window, cx| {
615 let overlay = Overlay {
616 block: ix,
617 part: Part::Body,
618 selection: owned.selection,
619 caret_on: owned.caret_on,
620 caret_shape: cx.caret_shape(),
621 caret_height: cx.caret_height(),
622 window_active: window.is_window_active(),
623 layouts: Some(&owned.layouts),
624 annotations: &owned.annotations,
625 placeholder: owned.placeholder.as_ref(),
626 caption: owned.caption,
627 toggle: owned.toggle.as_ref(),
628 image: owned.image.as_ref(),
629 image_overlay: owned.image_overlay.as_ref(),
630 image_overlay_corner: owned.image_overlay_corner,
631 fence: owned.fence.as_ref(),
632 table_controls: owned.table_controls,
633 copy: owned.copy,
634 base: owned.base.as_deref(),
635 highlight: owned.highlight,
636 find: owned.find,
637 };
638 block_box(
639 &owned.blocks[ix],
640 overlay,
641 &owned.typography,
642 &owned.theme,
643 window,
644 cx,
645 )
646 .into_any_element()
647 }),
648 }
649 .into_any_element()
650}
651
652struct Owned {
654 blocks: Vec<Block>,
655 selection: Option<Selection>,
656 caret_on: bool,
657 layouts: BlockLayouts,
658 annotations: Vec<(Selection, Annotation)>,
659 placeholder: Option<SharedString>,
660 caption: Caption,
661 toggle: Option<Toggle>,
662 image: Option<OnImage>,
663 image_overlay: Option<ImageOverlay>,
664 image_overlay_corner: ImageOverlayCorner,
665 fence: Option<FenceHost>,
666 table_controls: bool,
667 copy: CopyButton,
668 base: Option<std::path::PathBuf>,
669 highlight: crate::HighlightPaint,
670 find: crate::FindPaint,
671 typography: Typography,
672 theme: Theme,
673}
674
675fn block_box(
678 block: &Block,
679 overlay: Overlay,
680 typography: &Typography,
681 theme: &Theme,
682 window: &mut Window,
683 cx: &mut App,
684) -> gpui::Div {
685 let ix = overlay.block;
686 let frame = overlay.layouts.map(|layouts| {
689 let layouts = layouts.clone();
690 canvas(
691 move |bounds, _, _| layouts.record_block(ix, bounds),
692 |_, _, _, _| (),
693 )
694 .absolute()
695 .size_full()
696 });
697 div()
703 .w_full()
704 .pl(px(block.indent as f32 * INDENT_WIDTH))
705 .child(
706 div()
707 .w_full()
708 .relative()
709 .children(frame)
710 .when(overlay.covers_block() && block.opaque(), |el| {
714 el.rounded(px(4.0)).bg(theme.selection)
715 })
716 .child(block_element(block, overlay, typography, theme, window, cx)),
717 )
718}
719
720fn block_key(block: &Block, typography: &Typography, table_controls: bool) -> u64 {
723 let mut hasher = DefaultHasher::new();
724 table_controls.hash(&mut hasher);
725 block.hash(&mut hasher);
726 typography.body.size().to_bits().hash(&mut hasher);
727 typography.body.line_height().to_bits().hash(&mut hasher);
728 hasher.finish()
729}
730
731fn guess(block: &Block, typography: &Typography) -> Guess {
733 let body = px(typography.body.line_height());
734 let prose = |chars: usize, line: Pixels| Guess {
735 chars,
736 line,
737 rows: 0,
738 extra: px(0.0),
739 indent: px(0.0),
740 };
741 let guess = match &block.kind {
742 BlockKind::Paragraph(text)
743 | BlockKind::Bullet(text)
744 | BlockKind::Ordered { text, .. }
745 | BlockKind::Task { text, .. }
746 | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
747 BlockKind::Heading { level, text } => prose(
748 text.text.len(),
749 px(typography.heading(*level).line_height()),
750 ),
751 BlockKind::Code { code, .. } => Guess {
752 rows: code.text.lines().count().max(1),
753 line: px(typography.code.line_height()),
754 extra: px(2.0 * CODE_PADDING_Y) + body,
755 ..prose(0, body)
756 },
757 BlockKind::Table { rows, .. } => Guess {
758 rows: rows.len() + 1,
759 extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
760 ..prose(0, body)
761 },
762 BlockKind::Image { alt, .. } => Guess {
763 extra: px(240.0),
764 ..prose(alt.text.len(), px(typography.caption.line_height()))
765 },
766 BlockKind::Bookmark { .. } => Guess {
767 extra: body * 3.0,
768 ..prose(0, body)
769 },
770 BlockKind::Rule => Guess {
771 extra: body,
772 ..prose(0, px(0.0))
773 },
774 };
775 Guess {
776 indent: px(block.indent as f32 * INDENT_WIDTH),
777 ..guess
778 }
779}
780
781fn tight(previous: &Block, next: &Block) -> bool {
783 let marker = |block: &Block| {
784 matches!(
785 block.kind,
786 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
787 )
788 };
789 marker(previous) && (marker(next) || next.indent > previous.indent)
790}
791
792fn block_element(
793 block: &Block,
794 overlay: Overlay,
795 typography: &Typography,
796 theme: &Theme,
797 window: &mut Window,
798 cx: &mut App,
799) -> AnyElement {
800 let body = overlay.at(Part::Body);
801 match &block.kind {
802 BlockKind::Paragraph(text) => text_element(
803 text,
804 typography.body.size(),
805 typography.body.line_height(),
806 FontWeight::NORMAL,
807 body,
808 theme,
809 cx,
810 ),
811 BlockKind::Heading { level, text } => {
812 let heading = typography.heading(*level);
813 text_element(
814 text,
815 heading.size(),
816 heading.line_height(),
817 heading.weight,
818 body,
819 theme,
820 cx,
821 )
822 }
823 BlockKind::Bullet(text) => {
824 marker_row(disc(typography, theme), text, body, typography, theme, cx)
825 }
826 BlockKind::Ordered { number, text } => marker_row(
827 div()
828 .flex_none()
829 .w(px(MARKER_WIDTH))
830 .text_size(px(typography.body.size()))
831 .line_height(px(typography.body.line_height()))
832 .text_color(theme.text_muted)
833 .child(SharedString::from(format!("{number}.")))
834 .into_any_element(),
835 text,
836 body,
837 typography,
838 theme,
839 cx,
840 ),
841 BlockKind::Task { checked, text } => marker_row(
842 checkbox(*checked, overlay, typography, theme),
843 text,
844 body,
845 typography,
846 theme,
847 cx,
848 ),
849 BlockKind::Quote { kind, text } => div()
850 .border_l_2()
851 .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
852 .pl(px(12.0))
853 .pr(px(10.0))
854 .py(px(2.0))
855 .text_color(theme.text_muted)
856 .children(kind.map(|kind| {
857 div()
858 .pb(px(2.0))
859 .text_size(px(typography.body.size()))
860 .line_height(px(typography.body.line_height()))
861 .font_weight(FontWeight::SEMIBOLD)
862 .text_color(alert_color(kind, theme))
863 .child(kind.label())
864 }))
865 .child(text_element(
866 text,
867 typography.body.size(),
868 typography.body.line_height(),
869 FontWeight::NORMAL,
870 body,
871 theme,
872 cx,
873 ))
874 .into_any_element(),
875 BlockKind::Code { language, code } => {
876 let overlay = overlay.at(Part::Code);
877 let painted = overlay
881 .caret()
882 .is_none()
883 .then(|| {
884 let fence = block::Fence {
885 language: language.as_deref()?,
886 code: &code.text,
887 rewrite: overlay.fence.map(|host| {
888 let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
889 Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
890 rewrite(ix, code, window, cx)
891 }) as block::Rewrite
892 }),
893 };
894 block::render(&fence, window, cx)
895 })
896 .flatten();
897 match painted {
898 Some(element) => div()
901 .key_context(PAINTED_CONTEXT)
902 .when_some(overlay.fence, |el, host| {
903 let (leave, ix) = (host.leave.clone(), overlay.block);
904 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
905 })
906 .when(overlay.covers_block(), |el| {
907 el.rounded(px(4.0)).bg(theme.selection)
908 })
909 .child(element)
910 .into_any_element(),
911 None => code_block(
912 language.as_deref(),
913 &code.text,
914 overlay,
915 typography,
916 theme,
917 window,
918 cx,
919 ),
920 }
921 }
922 BlockKind::Image { url, alt, width } => {
923 image(url, alt, *width, overlay, typography, theme, window, cx)
924 }
925 BlockKind::Bookmark { url, form } => match crate::preview::card(url, *form, window, cx) {
926 Some(element) => div()
929 .key_context(PAINTED_CONTEXT)
930 .when_some(overlay.fence, |el, host| {
931 let (leave, ix) = (host.leave.clone(), overlay.block);
932 el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
933 })
934 .when(overlay.covers_block(), |el| {
935 el.rounded(px(4.0)).bg(theme.selection)
936 })
937 .child(element)
938 .into_any_element(),
939 None => bookmark(overlay.block, url, *form, typography, theme, cx),
940 },
941 BlockKind::Table {
942 align,
943 header,
944 rows,
945 } => table(align, header, rows, overlay, typography, theme, window, cx),
946 BlockKind::Rule => div()
947 .h(px(1.0))
948 .w_full()
949 .bg(theme.border)
950 .into_any_element(),
951 }
952}
953
954fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
956 div()
957 .flex_none()
958 .w(px(MARKER_WIDTH))
959 .h(px(typography.body.line_height()))
960 .flex()
961 .items_center()
962 .child(
963 div()
964 .ml(px(1.0))
965 .w(px(5.0))
966 .h(px(5.0))
967 .rounded_full()
968 .bg(theme.text_faint),
969 )
970 .into_any_element()
971}
972
973fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
974 let ix = overlay.block;
975 let mut box_ = div()
976 .relative()
977 .w(px(13.0))
978 .h(px(13.0))
979 .rounded(px(3.5))
980 .border_1()
981 .flex()
982 .items_center()
983 .justify_center();
984 box_ = if checked {
985 box_.bg(theme.solid)
986 .border_color(theme.solid)
987 .text_style(TextStyle::Caption)
988 .text_color(theme.on_solid)
989 .child("✓")
990 } else {
991 box_.border_color(theme.border_strong)
992 };
993 box_ = box_.children(overlay.layouts.map(|layouts| {
996 let layouts = layouts.clone();
997 canvas(
998 move |bounds, _, _| layouts.record_checkbox(ix, bounds),
999 |_, _, _, _| (),
1000 )
1001 .absolute()
1002 .size_full()
1003 }));
1004 if overlay.toggle.is_some() {
1007 box_ = box_.cursor_pointer();
1008 }
1009 if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1010 box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1011 cx.stop_propagation();
1015 toggle(ix, window, cx);
1016 });
1017 }
1018
1019 div()
1020 .flex_none()
1021 .w(px(MARKER_WIDTH))
1022 .h(px(typography.body.line_height()))
1023 .flex()
1024 .items_center()
1025 .child(box_)
1026 .into_any_element()
1027}
1028
1029fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1031 match kind {
1032 QuoteKind::Note => theme.accent,
1033 QuoteKind::Tip => theme.success,
1034 QuoteKind::Important => theme.busy,
1035 QuoteKind::Warning => theme.warning,
1036 QuoteKind::Caution => theme.danger,
1037 }
1038}
1039
1040fn marker_row(
1041 marker: AnyElement,
1042 text: &Text,
1043 overlay: Overlay,
1044 typography: &Typography,
1045 theme: &Theme,
1046 cx: &App,
1047) -> AnyElement {
1048 div()
1049 .flex()
1050 .flex_row()
1051 .gap(px(MARKER_GAP))
1052 .child(marker)
1053 .child(div().flex_1().min_w_0().child(text_element(
1054 text,
1055 typography.body.size(),
1056 typography.body.line_height(),
1057 FontWeight::NORMAL,
1058 overlay,
1059 theme,
1060 cx,
1061 )))
1062 .into_any_element()
1063}