1use std::{cell::RefCell, ops::Range, rc::Rc};
11
12use gpui::{
13 AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
14 InteractiveText, ObjectFit, Pixels, Point, SharedString, StrikethroughStyle, StyledImage as _,
15 StyledText, TextLayout, TextRun, UnderlineStyle, Window, canvas, div, font, img, point,
16 prelude::*, px, quad, size,
17};
18use theme::{TextStyle, Theme, Typeset};
19
20use crate::{
21 block,
22 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, Text},
23 preview,
24 select::{Cursor, Selection},
25 typography::Typography,
26};
27
28const BLOCK_GAP: f32 = 12.0;
30const LIST_GAP: f32 = 4.0;
31const INDENT_WIDTH: f32 = 22.0;
33const MARKER_WIDTH: f32 = 18.0;
35const MARKER_GAP: f32 = 8.0;
36const CODE_PADDING_X: f32 = 12.0;
38const CODE_PADDING_Y: f32 = 10.0;
39pub const PLAIN_LANGUAGE: &str = "Plain";
42const CARET_WIDTH: f32 = 1.5;
45const INLINE_CODE_RADIUS: f32 = 4.5;
48const INLINE_CODE_PAD_X: f32 = 2.0;
49const INLINE_CODE_INSET_Y: f32 = 2.0;
50const CHIP_PAD_X: f32 = 4.0;
53const CHIP_INSET_Y: f32 = 1.0;
54const CHIP_BLOCK_PAD_X: f32 = 8.0;
57const CHIP_BLOCK_PAD_Y: f32 = 3.0;
58const CHIP_ICON: f32 = 15.0;
59const CARD_HEIGHT: f32 = 116.0;
63const CARD_IMAGE_WIDTH: f32 = 180.0;
64const CARD_COVER_HEIGHT: f32 = 200.0;
65const CARD_PADDING: f32 = 14.0;
66const CARD_BORDER: f32 = 1.0;
67const CARD_ICON: f32 = 16.0;
68const CARD_COVER: f32 = 44.0;
69const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
71const CAPTION_GAP: f32 = 4.0;
72const IMAGE_EMPTY: &str = "Add an image";
74const CAPTION_HINT: &str = "Write a caption";
75const TABLE_CELL_PADDING: f32 = 12.0;
78const TABLE_DIVIDER: f32 = 1.0;
79const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
82const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
84
85#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
96pub enum Caption {
97 #[default]
99 Shown,
100 Hidden,
102}
103
104#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
111pub enum Annotation {
112 #[default]
114 Open,
115 Resolved,
117 Active,
119}
120
121impl Annotation {
122 fn wash(self, theme: &Theme) -> Hsla {
123 match self {
124 Self::Open => theme.warning.opacity(0.20),
125 Self::Resolved => theme.warning.opacity(0.08),
126 Self::Active => theme.warning.opacity(0.38),
127 }
128 }
129}
130
131#[derive(Clone)]
137pub struct Editing<'a> {
138 pub selection: Option<Selection>,
141 pub caret_on: bool,
144 pub layouts: Option<&'a BlockLayouts>,
146 pub annotations: &'a [(Selection, Annotation)],
148 pub placeholder: Option<SharedString>,
150 pub caption: Caption,
151}
152
153impl Default for Editing<'_> {
154 fn default() -> Self {
155 Self {
156 selection: None,
157 caret_on: true,
160 layouts: None,
161 annotations: &[],
162 placeholder: None,
163 caption: Caption::default(),
164 }
165 }
166}
167
168#[derive(Clone, Default)]
175pub struct BlockLayouts(Rc<RefCell<Frames>>);
176
177#[derive(Default)]
178struct Frames {
179 texts: Vec<Painted>,
180 blocks: Vec<(usize, Bounds<Pixels>)>,
183 languages: Vec<(usize, Bounds<Pixels>)>,
186 pictures: Vec<(usize, Bounds<Pixels>)>,
190}
191
192struct Painted {
199 block: usize,
200 part: Part,
201 range: Range<usize>,
202 layout: TextLayout,
203}
204
205impl BlockLayouts {
206 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
212 let entries = &self.0.borrow().texts;
213 let cursor = |painted: &Painted| {
214 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
215 Cursor::new(
216 painted.block,
217 painted.part,
218 painted.range.start + offset.min(painted.range.len()),
219 )
220 };
221 if let Some(painted) = entries
222 .iter()
223 .find(|painted| painted.layout.bounds().contains(&point))
224 {
225 return Some(cursor(painted));
226 }
227 entries
228 .iter()
229 .min_by_key(|painted| {
230 let bounds = painted.layout.bounds();
231 let above = (bounds.origin.y - point.y).abs();
232 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
233 f32::from(above.min(below)) as i64
234 })
235 .map(cursor)
236 }
237
238 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
244 let entries = &self.0.borrow().texts;
245 let painted = entries.iter().find(|painted| {
246 painted.block == at.block
247 && painted.part == at.part
248 && painted.range.start <= at.offset
249 && at.offset <= painted.range.end
250 })?;
251 let point = painted
252 .layout
253 .position_for_index(at.offset - painted.range.start)?;
254 Some((point, painted.layout.line_height()))
255 }
256
257 pub fn step_row(
268 &self,
269 at: Cursor,
270 from: Point<Pixels>,
271 down: bool,
272 ) -> Option<(Cursor, Pixels)> {
273 let entries = &self.0.borrow().texts;
274 let ix = entries.iter().position(|painted| {
275 painted.block == at.block
276 && painted.part == at.part
277 && painted.range.start <= at.offset
278 && at.offset <= painted.range.end
279 })?;
280 let here = &entries[ix];
281 let line = here.layout.line_height();
282 let index_at = |painted: &Painted, y: Pixels| {
283 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
284 (
285 Cursor::new(
286 painted.block,
287 painted.part,
288 painted.range.start + offset.min(painted.range.len()),
289 ),
290 y,
291 )
292 };
293
294 let bounds = here.layout.bounds();
297 let target = if down { from.y + line } else { from.y - line };
298 if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
299 return Some(index_at(here, target));
300 }
301
302 let next = match down {
303 true => entries.get(ix + 1)?,
304 false => entries.get(ix.checked_sub(1)?)?,
305 };
306 let bounds = next.layout.bounds();
308 let row = match down {
309 true => bounds.origin.y,
310 false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
311 };
312 Some(index_at(next, row))
313 }
314
315 pub fn over_text(&self, point: Point<Pixels>) -> bool {
322 self.0
323 .borrow()
324 .texts
325 .iter()
326 .any(|painted| painted.layout.bounds().contains(&point))
327 }
328
329 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
331 let blocks = &self.0.borrow().blocks;
332 blocks
333 .iter()
334 .find(|(_, bounds)| bounds.contains(&point))
335 .or_else(|| {
336 blocks.iter().min_by_key(|(_, bounds)| {
337 let above = (bounds.origin.y - point.y).abs();
338 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
339 f32::from(above.min(below)) as i64
340 })
341 })
342 .map(|(ix, _)| *ix)
343 }
344
345 pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
354 let texts = &self.0.borrow().texts;
355 let painted = texts.iter().find(|painted| painted.block == ix)?;
356 Some((
357 painted.layout.bounds().origin.y,
358 painted.layout.line_height(),
359 ))
360 }
361
362 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
364 self.0
365 .borrow()
366 .blocks
367 .iter()
368 .find(|(block, _)| *block == ix)
369 .map(|(_, bounds)| *bounds)
370 }
371
372 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
377 self.0
378 .borrow()
379 .languages
380 .iter()
381 .find(|(block, _)| *block == ix)
382 .map(|(_, bounds)| *bounds)
383 }
384
385 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
390 self.0
391 .borrow()
392 .pictures
393 .iter()
394 .find(|(block, _)| *block == ix)
395 .map(|(_, bounds)| *bounds)
396 }
397
398 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
399 self.0.borrow_mut().texts.push(Painted {
400 block,
401 part,
402 range,
403 layout,
404 });
405 }
406
407 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
408 self.0.borrow_mut().blocks.push((ix, bounds));
409 }
410
411 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
412 self.0.borrow_mut().languages.push((ix, bounds));
413 }
414
415 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
416 self.0.borrow_mut().pictures.push((ix, bounds));
417 }
418
419 fn clear(&self) {
420 let mut frames = self.0.borrow_mut();
421 frames.texts.clear();
422 frames.blocks.clear();
423 frames.languages.clear();
424 frames.pictures.clear();
425 }
426}
427
428#[derive(Clone, Copy)]
434struct Overlay<'a> {
435 block: usize,
436 part: Part,
437 selection: Option<Selection>,
438 caret_on: bool,
439 layouts: Option<&'a BlockLayouts>,
440 annotations: &'a [(Selection, Annotation)],
442 placeholder: Option<&'a SharedString>,
445 caption: Caption,
446}
447
448impl<'a> Overlay<'a> {
449 fn at(self, part: Part) -> Self {
450 Self { part, ..self }
451 }
452
453 fn here(&self) -> Cursor {
454 Cursor::new(self.block, self.part, 0)
455 }
456
457 fn caret_painted(&self) -> Option<usize> {
463 self.caret_on.then(|| self.caret()).flatten()
464 }
465
466 fn caret(&self) -> Option<usize> {
468 self.selection
469 .map(|selection| selection.head)
470 .filter(|head| head.block == self.block && head.part == self.part)
471 .map(|head| head.offset)
472 }
473
474 fn selected(&self, len: usize) -> Option<Range<usize>> {
476 self.clip(self.selection?, len)
477 }
478
479 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
482 self.annotations
483 .iter()
484 .filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
485 .collect()
486 }
487
488 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
494 if selection.is_collapsed() {
495 return None;
496 }
497 let (start, end) = selection.ordered();
498 let here = self.here();
499 let (first, last) = (
500 Cursor::new(start.block, start.part, 0),
501 Cursor::new(end.block, end.part, 0),
502 );
503 if here < first || here > last {
504 return None;
505 }
506 let from = if here == first { start.offset } else { 0 };
507 let to = if here == last { end.offset } else { len };
508 (from < to).then_some(from..to.min(len))
509 }
510
511 fn covers_block(&self) -> bool {
515 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
516 return false;
517 };
518 let (start, end) = selection.ordered();
519 start.block < self.block && self.block < end.block
520 }
521}
522
523pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
525 render(&crate::parse(source), Caption::default(), window, cx)
526}
527
528pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
530 render_with(
531 doc,
532 Editing {
533 caption,
534 ..Editing::default()
535 },
536 window,
537 cx,
538 )
539}
540
541pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
549 let Editing {
550 selection,
551 caret_on,
552 layouts,
553 annotations,
554 placeholder,
555 caption,
556 } = editing;
557 let reset = layouts.map(|layouts| {
563 let layouts = layouts.clone();
564 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
565 .absolute()
566 .size(px(0.0))
567 });
568 let theme = Theme::of(cx).clone();
571 let typography = Typography::of(cx);
572 let mut column = div().flex().flex_col().children(reset);
573
574 for (ix, block) in doc.blocks.iter().enumerate() {
575 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
576 None => 0.0,
577 Some(previous) if tight(previous, block) => LIST_GAP,
578 Some(_) => BLOCK_GAP,
579 };
580 let overlay = Overlay {
581 block: ix,
582 part: Part::Body,
583 selection,
584 caret_on,
585 layouts,
586 annotations,
587 placeholder: placeholder.as_ref(),
588 caption,
589 };
590 let frame = layouts.map(|layouts| {
593 let layouts = layouts.clone();
594 canvas(
595 move |bounds, _, _| layouts.record_block(ix, bounds),
596 |_, _, _, _| (),
597 )
598 .absolute()
599 .size_full()
600 });
601 column = column.child(
602 div()
603 .mt(px(gap))
604 .pl(px(block.indent as f32 * INDENT_WIDTH))
605 .relative()
606 .children(frame)
607 .when(overlay.covers_block() && block.opaque(), |el| {
611 el.rounded(px(4.0)).bg(theme.selection)
612 })
613 .child(block_element(
614 block,
615 overlay,
616 &typography,
617 &theme,
618 window,
619 cx,
620 )),
621 );
622 }
623
624 column.into_any_element()
625}
626
627fn tight(previous: &Block, next: &Block) -> bool {
629 let marker = |block: &Block| {
630 matches!(
631 block.kind,
632 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
633 )
634 };
635 marker(previous) && (marker(next) || next.indent > previous.indent)
636}
637
638fn block_element(
639 block: &Block,
640 overlay: Overlay,
641 typography: &Typography,
642 theme: &Theme,
643 window: &mut Window,
644 cx: &mut App,
645) -> AnyElement {
646 let body = overlay.at(Part::Body);
647 match &block.kind {
648 BlockKind::Paragraph(text) => text_element(
649 text,
650 typography.body.size(),
651 typography.body.line_height(),
652 FontWeight::NORMAL,
653 body,
654 theme,
655 ),
656 BlockKind::Heading { level, text } => {
657 let heading = typography.heading(*level);
658 text_element(
659 text,
660 heading.size(),
661 heading.line_height(),
662 heading.weight,
663 body,
664 theme,
665 )
666 }
667 BlockKind::Bullet(text) => {
668 marker_row(disc(typography, theme), text, body, typography, theme)
669 }
670 BlockKind::Ordered { number, text } => marker_row(
671 div()
672 .flex_none()
673 .w(px(MARKER_WIDTH))
674 .text_size(px(typography.body.size()))
675 .line_height(px(typography.body.line_height()))
676 .text_color(theme.text_muted)
677 .child(SharedString::from(format!("{number}.")))
678 .into_any_element(),
679 text,
680 body,
681 typography,
682 theme,
683 ),
684 BlockKind::Task { checked, text } => marker_row(
685 checkbox(*checked, typography, theme),
686 text,
687 body,
688 typography,
689 theme,
690 ),
691 BlockKind::Quote(text) => div()
692 .border_l_2()
693 .border_color(theme.border_strong)
694 .pl(px(12.0))
695 .pr(px(10.0))
696 .py(px(2.0))
697 .text_color(theme.text_muted)
698 .child(text_element(
699 text,
700 typography.body.size(),
701 typography.body.line_height(),
702 FontWeight::NORMAL,
703 body,
704 theme,
705 ))
706 .into_any_element(),
707 BlockKind::Code { language, code } => {
708 let overlay = overlay.at(Part::Code);
709 let painted = overlay
713 .caret()
714 .is_none()
715 .then(|| block::render(language.as_deref(), &code.text, window, cx))
716 .flatten();
717 match painted {
718 Some(element) => div()
721 .when(overlay.covers_block(), |el| {
722 el.rounded(px(4.0)).bg(theme.selection)
723 })
724 .child(element)
725 .into_any_element(),
726 None => code_block(
727 language.as_deref(),
728 &code.text,
729 overlay,
730 typography,
731 theme,
732 window,
733 cx,
734 ),
735 }
736 }
737 BlockKind::Image { url, alt, width } => image(url, alt, *width, overlay, typography, theme),
738 BlockKind::Bookmark { url, form } => {
739 bookmark(overlay.block, url, *form, typography, theme, cx)
740 }
741 BlockKind::Table {
742 align,
743 header,
744 rows,
745 } => table(align, header, rows, overlay, typography, theme, window),
746 BlockKind::Rule => div()
747 .h(px(1.0))
748 .w_full()
749 .bg(theme.border)
750 .into_any_element(),
751 }
752}
753
754fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
756 div()
757 .flex_none()
758 .w(px(MARKER_WIDTH))
759 .h(px(typography.body.line_height()))
760 .flex()
761 .items_center()
762 .child(
763 div()
764 .ml(px(1.0))
765 .w(px(5.0))
766 .h(px(5.0))
767 .rounded_full()
768 .bg(theme.text_faint),
769 )
770 .into_any_element()
771}
772
773fn checkbox(checked: bool, typography: &Typography, theme: &Theme) -> AnyElement {
774 let mut box_ = div()
775 .w(px(13.0))
776 .h(px(13.0))
777 .rounded(px(3.5))
778 .border_1()
779 .flex()
780 .items_center()
781 .justify_center();
782 box_ = if checked {
783 box_.bg(theme.solid)
784 .border_color(theme.solid)
785 .text_style(TextStyle::Caption)
786 .text_color(theme.on_solid)
787 .child("✓")
788 } else {
789 box_.border_color(theme.border_strong)
790 };
791
792 div()
793 .flex_none()
794 .w(px(MARKER_WIDTH))
795 .h(px(typography.body.line_height()))
796 .flex()
797 .items_center()
798 .child(box_)
799 .into_any_element()
800}
801
802fn marker_row(
803 marker: AnyElement,
804 text: &Text,
805 overlay: Overlay,
806 typography: &Typography,
807 theme: &Theme,
808) -> AnyElement {
809 div()
810 .flex()
811 .flex_row()
812 .gap(px(MARKER_GAP))
813 .child(marker)
814 .child(div().flex_1().min_w_0().child(text_element(
815 text,
816 typography.body.size(),
817 typography.body.line_height(),
818 FontWeight::NORMAL,
819 overlay,
820 theme,
821 )))
822 .into_any_element()
823}
824
825pub struct Flat {
828 pub text: SharedString,
829 pub runs: Vec<TextRun>,
830 pub links: Vec<(Range<usize>, String)>,
831 pub code: Vec<Range<usize>>,
832 pub chips: Vec<Range<usize>>,
833}
834
835pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
838 let mut cuts: Vec<usize> = text
839 .marks
840 .iter()
841 .flat_map(|span| [span.range.start, span.range.end])
842 .chain([0, text.text.len()])
843 .filter(|cut| *cut <= text.text.len())
844 .collect();
845 cuts.sort_unstable();
846 cuts.dedup();
847
848 let mut runs = Vec::new();
849 let mut links: Vec<(Range<usize>, String)> = Vec::new();
850 let mut code: Vec<Range<usize>> = Vec::new();
851 let mut chips: Vec<Range<usize>> = Vec::new();
852
853 for pair in cuts.windows(2) {
854 let (start, end) = (pair[0], pair[1]);
855 let covering = text
856 .marks
857 .iter()
858 .filter(|span| span.range.start <= start && span.range.end >= end);
859
860 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
861 let mut chip = false;
862 let mut link = None;
863 for span in covering {
864 match &span.mark {
865 Mark::Bold => bold = true,
866 Mark::Italic => italic = true,
867 Mark::Strike => strike = true,
868 Mark::Code => mono = true,
869 Mark::Mention { url, .. } => {
870 chip = true;
871 link = Some(url.clone());
872 }
873 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
874 }
875 }
876
877 if mono {
878 match code.last_mut() {
879 Some(range) if range.end == start => range.end = end,
880 _ => code.push(start..end),
881 }
882 }
883 if chip {
884 match chips.last_mut() {
885 Some(range) if range.end == start => range.end = end,
886 _ => chips.push(start..end),
887 }
888 }
889 if let Some(url) = &link {
890 match links.last_mut() {
891 Some((range, last)) if range.end == start && last == url => range.end = end,
892 _ => links.push((start..end, url.clone())),
893 }
894 }
895
896 let mut face = font(if mono {
897 theme.font_mono.clone()
898 } else if italic {
899 theme.font_sans_fallback.clone()
902 } else {
903 theme.font_sans.clone()
904 });
905 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
906 FontWeight::SEMIBOLD
907 } else {
908 base_weight
909 };
910 face.style = if italic {
911 FontStyle::Italic
912 } else {
913 FontStyle::Normal
914 };
915
916 runs.push(TextRun {
917 len: end - start,
918 font: face,
919 color: if mono { theme.code_text } else { theme.text },
923 background_color: None,
924 underline: (link.is_some() && !chip).then_some(UnderlineStyle {
925 color: Some(theme.text_muted),
926 thickness: px(1.0),
927 wavy: false,
928 }),
929 strikethrough: strike.then_some(StrikethroughStyle {
930 thickness: px(1.0),
931 color: Some(theme.text_muted),
932 }),
933 });
934 }
935
936 Flat {
937 text: text.text.clone().into(),
938 runs,
939 links,
940 code,
941 chips,
942 }
943}
944
945fn text_element(
946 text: &Text,
947 size: f32,
948 line_height: f32,
949 weight: FontWeight,
950 overlay: Overlay,
951 theme: &Theme,
952) -> AnyElement {
953 let flat = flatten(text, weight, theme);
954 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
955}
956
957fn painted_text(
963 flat: Flat,
964 len: usize,
965 size: f32,
966 line_height: f32,
967 overlay: Overlay,
968 theme: &Theme,
969) -> AnyElement {
970 let (ix, part) = (overlay.block, overlay.part);
971 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
972 let span = 0..len;
973 let hint = overlay
976 .placeholder
977 .filter(|_| len == 0 && overlay.caret().is_some())
980 .map(|hint| {
981 div()
982 .absolute()
983 .text_color(theme.text_faint)
984 .child(hint.clone())
985 });
986 let styled = StyledText::new(flat.text).with_runs(flat.runs);
987 let layout = styled.layout().clone();
988
989 let painted: AnyElement = if flat.links.is_empty() {
990 styled.into_any_element()
991 } else {
992 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
993 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
994 .on_click(ranges, move |clicked, _window, cx| {
995 if let Some(url) = urls.get(clicked) {
996 cx.open_url(url);
997 }
998 })
999 .into_any_element()
1000 };
1001
1002 let wash = theme.code_wash;
1006 let code_ranges = flat.code;
1007 let chip_wash = theme.element_hover;
1008 let chip_edge = theme.border;
1009 let chip_ranges = flat.chips;
1010 let caret_color = theme.caret;
1011 let selection_color = theme.selection;
1012 let annotated = overlay.annotated(len, theme);
1013 let layouts = overlay.layouts.cloned();
1014 let underlay = canvas(
1015 |_, _, _| (),
1016 move |_, _, window, _| {
1017 if let Some(layouts) = &layouts {
1018 layouts.record(ix, part, span.clone(), layout.clone());
1019 }
1020 for (range, wash) in &annotated {
1023 for rect in range_rects(&layout, range, 0.0, 0.0) {
1024 window.paint_quad(quad(
1025 rect,
1026 px(2.0),
1027 *wash,
1028 px(0.0),
1029 gpui::transparent_black(),
1030 BorderStyle::default(),
1031 ));
1032 }
1033 }
1034 if let Some(range) = &selected {
1038 for rect in range_rects(&layout, range, 0.0, 0.0) {
1039 window.paint_quad(quad(
1040 rect,
1041 px(2.0),
1042 selection_color,
1043 px(0.0),
1044 gpui::transparent_black(),
1045 BorderStyle::default(),
1046 ));
1047 }
1048 }
1049 if let Some(offset) = caret
1050 && let Some(head) = layout.position_for_index(offset)
1051 {
1052 window.paint_quad(quad(
1053 caret_quad(head, size, layout.line_height()),
1054 px(0.0),
1055 caret_color,
1056 px(0.0),
1057 gpui::transparent_black(),
1058 BorderStyle::default(),
1059 ));
1060 }
1061 for range in &code_ranges {
1062 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1063 window.paint_quad(quad(
1064 rect,
1065 px(INLINE_CODE_RADIUS),
1066 wash,
1067 px(0.0),
1068 gpui::transparent_black(),
1069 BorderStyle::default(),
1070 ));
1071 }
1072 }
1073 for range in &chip_ranges {
1076 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1077 window.paint_quad(quad(
1078 rect,
1079 px(Theme::control_radius()),
1080 chip_wash,
1081 px(1.0),
1082 chip_edge,
1083 BorderStyle::Solid,
1084 ));
1085 }
1086 }
1087 },
1088 )
1089 .absolute()
1090 .size_full();
1091
1092 div()
1093 .text_size(px(size))
1094 .line_height(px(line_height))
1095 .relative()
1096 .child(underlay)
1097 .children(hint)
1098 .child(painted)
1099 .into_any_element()
1100}
1101
1102fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1108 let inset = (line_height - px(size)) / 2.0;
1109 Bounds::new(
1110 head + point(px(0.0), inset),
1111 gpui::size(px(CARET_WIDTH), px(size)),
1112 )
1113}
1114
1115fn range_rects(
1117 layout: &gpui::TextLayout,
1118 range: &Range<usize>,
1119 pad_x: f32,
1120 inset_y: f32,
1121) -> Vec<Bounds<Pixels>> {
1122 let mut rects = Vec::new();
1123 let line_height = layout.line_height();
1124 let mut cursor = range.start;
1125 let mut guard = 0;
1128 while cursor < range.end && guard < 256 {
1129 guard += 1;
1130 let Some(head) = layout.position_for_index(cursor) else {
1131 break;
1132 };
1133 let (row_end, next) = match layout.position_for_index(range.end) {
1134 Some(tail) if tail.y == head.y => (range.end, range.end),
1135 _ => {
1136 let (mut low, mut high) = (cursor, range.end);
1137 while high - low > 1 {
1138 let mid = low + (high - low) / 2;
1139 match layout.position_for_index(mid) {
1140 Some(probe) if probe.y == head.y => low = mid,
1141 _ => high = mid,
1142 }
1143 }
1144 (low, high)
1145 }
1146 };
1147 if let Some(tail) = layout.position_for_index(row_end)
1148 && tail.x > head.x
1149 {
1150 rects.push(Bounds::new(
1151 point(head.x - px(pad_x), head.y + px(inset_y)),
1152 size(
1153 tail.x - head.x + px(2.0 * pad_x),
1154 line_height - px(2.0 * inset_y),
1155 ),
1156 ));
1157 }
1158 cursor = next.max(cursor + 1);
1159 }
1160 rects
1161}
1162
1163fn code_block(
1164 language: Option<&str>,
1165 code: &str,
1166 overlay: Overlay,
1167 typography: &Typography,
1168 theme: &Theme,
1169 window: &mut Window,
1170 cx: &mut App,
1171) -> AnyElement {
1172 let ix = overlay.block;
1173 let spans = crate::highlight::spans(cx, language, code);
1177 let mono = font(theme.font_mono.clone());
1178 let run = |len: usize, color: Hsla| TextRun {
1179 len,
1180 font: mono.clone(),
1181 color,
1182 background_color: None,
1183 underline: None,
1184 strikethrough: None,
1185 };
1186 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1189 let mut offset = 0usize;
1190 let lines: Vec<AnyElement> = code
1191 .split('\n')
1192 .map(|line| {
1193 let start = offset;
1194 offset += line.len() + 1;
1195 let mut runs = Vec::new();
1196 let mut pos = 0usize;
1199 if let Some(spans) = &spans {
1200 let end = start + line.len();
1201 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1202 let s = range.start.clamp(start, end) - start;
1203 let e = range.end.min(end) - start;
1204 if s > pos {
1205 runs.push(run(s - pos, theme.text));
1206 }
1207 runs.push(run(e - s, theme.syntax.color(*kind)));
1208 pos = e;
1209 }
1210 }
1211 if pos < line.len() {
1212 runs.push(run(line.len() - pos, theme.text));
1213 }
1214 if runs.is_empty() {
1215 runs.push(run(0, theme.text));
1216 }
1217 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1218 rows.push((start..start + line.len(), styled.layout().clone()));
1219 styled.into_any_element()
1220 })
1221 .collect();
1222
1223 let caret = overlay.caret_painted();
1224 let selected = overlay.selected(code.len());
1225 let sink = overlay.layouts.cloned();
1226 let code_size = typography.code.size();
1227 let annotated = overlay.annotated(code.len(), theme);
1228 let (caret_color, selection_color) = (theme.caret, theme.selection);
1229 let underlay = canvas(
1230 |_, _, _| (),
1231 move |_, _, window, _| {
1232 for (span, layout) in &rows {
1233 if let Some(sink) = &sink {
1234 sink.record(ix, Part::Code, span.clone(), layout.clone());
1235 }
1236 for (range, wash) in &annotated {
1237 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1238 if from < to {
1239 for rect in
1240 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1241 {
1242 window.paint_quad(quad(
1243 rect,
1244 px(2.0),
1245 *wash,
1246 px(0.0),
1247 gpui::transparent_black(),
1248 BorderStyle::default(),
1249 ));
1250 }
1251 }
1252 }
1253 if let Some(range) = &selected {
1254 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1255 if from < to {
1256 for rect in
1257 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1258 {
1259 window.paint_quad(quad(
1260 rect,
1261 px(2.0),
1262 selection_color,
1263 px(0.0),
1264 gpui::transparent_black(),
1265 BorderStyle::default(),
1266 ));
1267 }
1268 }
1269 }
1270 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1271 && let Some(head) = layout.position_for_index(offset - span.start)
1272 {
1273 window.paint_quad(quad(
1274 caret_quad(head, code_size, layout.line_height()),
1275 px(0.0),
1276 caret_color,
1277 px(0.0),
1278 gpui::transparent_black(),
1279 BorderStyle::default(),
1280 ));
1281 }
1282 }
1283 },
1284 )
1285 .absolute()
1286 .size_full();
1287
1288 div()
1289 .rounded(px(Theme::panel_radius()))
1290 .bg(theme.ink(0.035))
1291 .border_1()
1292 .border_color(theme.border)
1293 .overflow_hidden()
1294 .relative()
1295 .child(
1299 div()
1300 .relative()
1301 .flex()
1302 .flex_row()
1303 .items_center()
1304 .px(px(CODE_PADDING_X))
1305 .py(px(5.0))
1306 .border_b_1()
1307 .border_color(theme.border)
1308 .bg(theme.ink(0.02))
1309 .text_style(TextStyle::Subheadline)
1310 .text_color(match language {
1311 Some(_) => theme.text_muted,
1312 None => theme.text_faint,
1313 })
1314 .child(
1318 div()
1319 .relative()
1320 .children(overlay.layouts.map(|layouts| {
1321 let layouts = layouts.clone();
1322 canvas(
1323 move |bounds, _, _| layouts.record_language(ix, bounds),
1324 |_, _, _, _| (),
1325 )
1326 .absolute()
1327 .size_full()
1328 }))
1329 .child(SharedString::from(
1330 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1331 )),
1332 ),
1333 )
1334 .child(
1335 div()
1336 .id(ElementId::named_usize("md-code", ix))
1337 .overflow_x_scroll()
1338 .restrict_scroll_to_axis()
1342 .relative()
1343 .px(px(CODE_PADDING_X))
1344 .py(px(CODE_PADDING_Y))
1345 .text_size(px(typography.code.size()))
1346 .line_height(px(typography.code.line_height()))
1347 .whitespace_nowrap()
1348 .child(underlay)
1349 .children(lines),
1350 )
1351 .child(copy_button(code, ix, theme, window, cx))
1352 .into_any_element()
1353}
1354
1355fn copy_button(
1362 code: &str,
1363 ix: usize,
1364 theme: &Theme,
1365 window: &mut Window,
1366 cx: &mut App,
1367) -> AnyElement {
1368 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1369 let showing = *copied.read(cx);
1370 let text: SharedString = code.to_string().into();
1371
1372 div()
1373 .id(ElementId::named_usize("md-copy", ix))
1374 .absolute()
1375 .top(px(3.0))
1376 .right(px(5.0))
1377 .h(px(20.0))
1378 .px(px(6.0))
1379 .rounded(px(5.0))
1380 .flex()
1381 .items_center()
1382 .cursor_pointer()
1383 .text_style(TextStyle::Caption)
1384 .text_color(theme.text_muted)
1385 .hover(|el| el.bg(theme.element_hover))
1386 .child(if showing { "Copied" } else { "Copy" })
1387 .on_click({
1388 let copied = copied.clone();
1389 move |_, _, cx| {
1390 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1391 copied.update(cx, |state, cx| {
1392 *state = true;
1393 cx.notify();
1394 });
1395 }
1396 })
1397 .on_hover(move |hovering, _, cx| {
1398 if !*hovering && *copied.read(cx) {
1399 copied.update(cx, |state, cx| {
1400 *state = false;
1401 cx.notify();
1402 });
1403 }
1404 })
1405 .into_any_element()
1406}
1407
1408fn image(
1415 url: &str,
1416 alt: &Text,
1417 width: Option<u32>,
1418 overlay: Overlay,
1419 typography: &Typography,
1420 theme: &Theme,
1421) -> AnyElement {
1422 let hint = SharedString::new_static(CAPTION_HINT);
1423 let overlay = Overlay {
1424 placeholder: Some(&hint),
1425 ..overlay.at(Part::Caption)
1426 };
1427 let picture = if url.is_empty() {
1428 div()
1429 .h(px(IMAGE_EMPTY_HEIGHT))
1430 .flex()
1431 .items_center()
1432 .px(px(CARD_PADDING))
1433 .rounded(px(Theme::button_radius()))
1434 .border_1()
1435 .border_dashed()
1436 .border_color(theme.border)
1437 .text_size(px(typography.body.size()))
1438 .text_color(theme.text_muted)
1439 .child(IMAGE_EMPTY)
1440 } else {
1441 let picture = match url.contains("://") {
1445 true => img(SharedString::from(url.to_string())),
1446 false => img(std::path::PathBuf::from(url)),
1447 };
1448 let box_ = div()
1449 .relative()
1450 .rounded(px(Theme::button_radius()))
1451 .overflow_hidden()
1452 .border_1()
1453 .border_color(theme.border)
1454 .children(overlay.layouts.map(|layouts| {
1455 let layouts = layouts.clone();
1456 let ix = overlay.block;
1457 canvas(
1458 move |bounds, _, _| layouts.record_picture(ix, bounds),
1459 |_, _, _, _| (),
1460 )
1461 .absolute()
1462 .size_full()
1463 }));
1464 match width {
1465 Some(width) => box_
1470 .self_start()
1471 .max_w_full()
1472 .w(px(width as f32))
1473 .child(picture.w(px(width as f32)).max_w_full()),
1474 None => box_.child(picture.max_w_full()),
1477 }
1478 };
1479 div()
1480 .flex()
1481 .flex_col()
1482 .gap(px(CAPTION_GAP))
1483 .child(picture)
1484 .when(
1487 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1488 |el| {
1489 el.child(text_element(
1490 alt,
1491 typography.caption.size(),
1492 typography.caption.line_height(),
1493 FontWeight::NORMAL,
1494 overlay,
1495 theme,
1496 ))
1497 },
1498 )
1499 .into_any_element()
1500}
1501
1502fn bookmark(
1514 ix: usize,
1515 url: &str,
1516 form: Form,
1517 typography: &Typography,
1518 theme: &Theme,
1519 cx: &App,
1520) -> AnyElement {
1521 let preview = preview::of(cx, url).unwrap_or_default();
1522 let host = SharedString::from(preview::host(url).to_string());
1523 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1524 let title = preview
1525 .title
1526 .clone()
1527 .unwrap_or_else(|| SharedString::from(url.to_string()));
1528
1529 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1532 let site = host.clone();
1533 let mark = move |size: f32| {
1534 let host = site.clone();
1535 match icon.clone() {
1536 Some(icon) => img(icon)
1537 .size(px(size))
1538 .rounded(px(size / 4.0))
1539 .with_fallback(move || initial(&host, size, muted, wash))
1540 .into_any_element(),
1541 None => initial(&host, size, muted, wash),
1542 }
1543 };
1544
1545 if form == Form::Chip {
1546 let open = url.to_string();
1547 let pill = div()
1548 .id(ElementId::named_usize("md-chip", ix))
1549 .flex()
1550 .flex_row()
1551 .items_center()
1552 .gap(px(6.0))
1553 .px(px(CHIP_BLOCK_PAD_X))
1554 .py(px(CHIP_BLOCK_PAD_Y))
1555 .rounded(px(Theme::control_radius()))
1556 .border_1()
1557 .border_color(theme.border)
1558 .bg(theme.element_hover)
1559 .text_size(px(typography.body.size()))
1560 .line_height(px(typography.body.line_height()))
1561 .text_color(theme.text)
1562 .cursor(CursorStyle::PointingHand)
1563 .hover(|el| el.bg(theme.element_active))
1564 .on_click(move |_, _, cx| cx.open_url(&open))
1565 .child(mark(CHIP_ICON))
1566 .child(
1569 div()
1570 .min_w_0()
1571 .truncate()
1572 .child(preview.title.unwrap_or(label)),
1573 );
1574 return div().flex().flex_row().child(pill).into_any_element();
1577 }
1578
1579 let words = div()
1580 .flex()
1581 .flex_col()
1582 .min_w_0()
1583 .px(px(CARD_PADDING))
1584 .py(px(CARD_PADDING - 2.0))
1585 .child(
1586 div()
1587 .truncate()
1588 .text_size(px(typography.body.size()))
1589 .line_height(px(typography.body.line_height()))
1590 .text_color(theme.text)
1591 .child(title),
1592 )
1593 .children(preview.description.map(|blurb| {
1594 div()
1595 .line_clamp(2)
1596 .text_size(px(typography.card.size()))
1597 .line_height(px(typography.card.line_height()))
1598 .text_color(theme.text_muted)
1599 .child(blurb)
1600 }))
1601 .child(
1602 div()
1603 .mt_auto()
1604 .pt(px(6.0))
1605 .flex()
1606 .items_center()
1607 .gap(px(6.0))
1608 .text_size(px(typography.card.size()))
1609 .text_color(theme.text_muted)
1610 .child(mark(CARD_ICON))
1611 .child(div().truncate().child(label)),
1612 );
1613
1614 let picture = corners(div(), form)
1615 .bg(theme.surface)
1616 .flex()
1617 .items_center()
1618 .justify_center()
1619 .overflow_hidden()
1620 .child(match preview.image {
1621 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
1622 .with_fallback(move || mark(CARD_COVER))
1623 .into_any_element(),
1624 None => mark(CARD_COVER),
1625 });
1626
1627 let open = url.to_string();
1628 let card = div()
1629 .id(ElementId::named_usize("md-bookmark", ix))
1630 .flex()
1631 .w_full()
1632 .overflow_hidden()
1633 .rounded(px(Theme::button_radius()))
1634 .border(px(CARD_BORDER))
1635 .border_color(theme.border)
1636 .bg(theme.surface_card)
1637 .cursor(CursorStyle::PointingHand)
1638 .hover(|el| el.bg(theme.element_hover))
1639 .on_click(move |_, _, cx| cx.open_url(&open));
1640
1641 if form == Form::Embed {
1642 card.flex_col()
1643 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1644 .child(words.w_full())
1645 } else {
1646 card.h(px(CARD_HEIGHT))
1647 .child(words.flex_1())
1648 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1649 }
1650 .into_any_element()
1651}
1652
1653fn corners<T: Styled>(element: T, form: Form) -> T {
1657 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
1658 match form {
1659 Form::Embed => element.rounded_t(corner),
1660 _ => element.rounded_r(corner),
1661 }
1662}
1663
1664fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1667 div()
1668 .flex_none()
1669 .size(px(size))
1670 .rounded(px(size / 4.0))
1671 .bg(wash)
1672 .flex()
1673 .items_center()
1674 .justify_center()
1675 .text_size(px(size * 0.55))
1676 .text_color(color)
1677 .child(SharedString::from(
1678 host.chars()
1679 .next()
1680 .unwrap_or('?')
1681 .to_uppercase()
1682 .to_string(),
1683 ))
1684 .into_any_element()
1685}
1686
1687fn table(
1694 align: &[Align],
1695 header: &[Text],
1696 rows: &[Vec<Text>],
1697 overlay: Overlay,
1698 typography: &Typography,
1699 theme: &Theme,
1700 window: &mut Window,
1701) -> AnyElement {
1702 let ix = overlay.block;
1703 let all: Vec<&[Text]> = std::iter::once(header)
1704 .filter(|row| !row.is_empty())
1705 .chain(rows.iter().map(|row| row.as_slice()))
1706 .collect();
1707 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1708 if columns == 0 {
1709 return gpui::Empty.into_any_element();
1710 }
1711 let has_header = !header.is_empty();
1712
1713 let text_system = window.text_system();
1714 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1715 let mut content = vec![0.0f32; columns];
1716 for (r, row) in all.iter().enumerate() {
1717 let weight = if has_header && r == 0 {
1718 FontWeight::BOLD
1719 } else {
1720 FontWeight::NORMAL
1721 };
1722 let mut out = Vec::with_capacity(columns);
1723 for (c, natural) in content.iter_mut().enumerate() {
1724 let Some(cell) = row.get(c) else {
1725 out.push(None);
1726 continue;
1727 };
1728 let flat = flatten(cell, weight, theme);
1729 if !flat.text.is_empty() {
1730 let width = f32::from(
1731 text_system
1732 .shape_line(
1733 flat.text.clone(),
1734 px(typography.body.size()),
1735 &flat.runs,
1736 None,
1737 )
1738 .width(),
1739 );
1740 *natural = natural.max(width);
1741 }
1742 out.push(Some(flat));
1743 }
1744 flats.push(out);
1745 }
1746
1747 let naturals: Vec<f32> = content
1748 .iter()
1749 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
1750 .collect();
1751 let minimums: Vec<f32> = naturals
1752 .iter()
1753 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
1754 .collect();
1755 let hairline = theme.hairline(0.10);
1756
1757 let mut inner = div()
1758 .flex()
1759 .flex_col()
1760 .w_full()
1761 .min_w(px(minimums.iter().sum::<f32>()));
1762 for (r, row) in flats.into_iter().enumerate() {
1763 if r > 0 {
1764 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
1765 }
1766 let mut row_el = div().flex().flex_row();
1767 for (c, cell) in row.into_iter().enumerate() {
1768 let mut cell_el = div()
1769 .flex_grow(naturals[c])
1770 .flex_shrink(naturals[c])
1771 .flex_basis(px(0.0))
1772 .min_w(px(minimums[c]))
1773 .p(px(TABLE_CELL_PADDING))
1774 .text_size(px(typography.body.size()))
1775 .line_height(px(typography.body.line_height()));
1776 cell_el = match align.get(c).copied().unwrap_or_default() {
1777 Align::Left => cell_el,
1778 Align::Center => cell_el.text_center(),
1779 Align::Right => cell_el.text_right(),
1780 };
1781 if let Some(flat) = cell {
1782 let row = if has_header { r } else { r + 1 };
1786 let len = flat.text.len();
1787 cell_el = cell_el.child(painted_text(
1788 flat,
1789 len,
1790 typography.body.size(),
1791 typography.body.line_height(),
1792 overlay.at(Part::Cell { row, column: c }),
1793 theme,
1794 ));
1795 }
1796 row_el = row_el.child(cell_el);
1797 }
1798 inner = inner.child(row_el);
1799 }
1800
1801 div()
1802 .id(ElementId::named_usize("md-table", ix))
1803 .w_full()
1804 .overflow_x_scroll()
1805 .restrict_scroll_to_axis()
1806 .child(inner)
1807 .into_any_element()
1808}