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 pub typography: Option<Typography>,
155}
156
157impl Default for Editing<'_> {
158 fn default() -> Self {
159 Self {
160 selection: None,
161 caret_on: true,
164 layouts: None,
165 annotations: &[],
166 placeholder: None,
167 caption: Caption::default(),
168 typography: None,
169 }
170 }
171}
172
173#[derive(Clone, Default)]
180pub struct BlockLayouts(Rc<RefCell<Frames>>);
181
182#[derive(Default)]
183struct Frames {
184 texts: Vec<Painted>,
185 blocks: Vec<(usize, Bounds<Pixels>)>,
188 languages: Vec<(usize, Bounds<Pixels>)>,
191 pictures: Vec<(usize, Bounds<Pixels>)>,
195}
196
197struct Painted {
204 block: usize,
205 part: Part,
206 range: Range<usize>,
207 layout: TextLayout,
208}
209
210impl BlockLayouts {
211 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
217 let entries = &self.0.borrow().texts;
218 let cursor = |painted: &Painted| {
219 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
220 Cursor::new(
221 painted.block,
222 painted.part,
223 painted.range.start + offset.min(painted.range.len()),
224 )
225 };
226 if let Some(painted) = entries
227 .iter()
228 .find(|painted| painted.layout.bounds().contains(&point))
229 {
230 return Some(cursor(painted));
231 }
232 entries
233 .iter()
234 .min_by_key(|painted| {
235 let bounds = painted.layout.bounds();
236 let above = (bounds.origin.y - point.y).abs();
237 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
238 f32::from(above.min(below)) as i64
239 })
240 .map(cursor)
241 }
242
243 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
249 let entries = &self.0.borrow().texts;
250 let painted = entries.iter().find(|painted| {
251 painted.block == at.block
252 && painted.part == at.part
253 && painted.range.start <= at.offset
254 && at.offset <= painted.range.end
255 })?;
256 let point = painted
257 .layout
258 .position_for_index(at.offset - painted.range.start)?;
259 Some((point, painted.layout.line_height()))
260 }
261
262 pub fn step_row(
273 &self,
274 at: Cursor,
275 from: Point<Pixels>,
276 down: bool,
277 ) -> Option<(Cursor, Pixels)> {
278 let entries = &self.0.borrow().texts;
279 let ix = entries.iter().position(|painted| {
280 painted.block == at.block
281 && painted.part == at.part
282 && painted.range.start <= at.offset
283 && at.offset <= painted.range.end
284 })?;
285 let here = &entries[ix];
286 let line = here.layout.line_height();
287 let index_at = |painted: &Painted, y: Pixels| {
288 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
289 (
290 Cursor::new(
291 painted.block,
292 painted.part,
293 painted.range.start + offset.min(painted.range.len()),
294 ),
295 y,
296 )
297 };
298
299 let bounds = here.layout.bounds();
302 let target = if down { from.y + line } else { from.y - line };
303 if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
304 return Some(index_at(here, target));
305 }
306
307 let next = match down {
308 true => entries.get(ix + 1)?,
309 false => entries.get(ix.checked_sub(1)?)?,
310 };
311 let bounds = next.layout.bounds();
313 let row = match down {
314 true => bounds.origin.y,
315 false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
316 };
317 Some(index_at(next, row))
318 }
319
320 pub fn over_text(&self, point: Point<Pixels>) -> bool {
327 self.0
328 .borrow()
329 .texts
330 .iter()
331 .any(|painted| painted.layout.bounds().contains(&point))
332 }
333
334 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
336 let blocks = &self.0.borrow().blocks;
337 blocks
338 .iter()
339 .find(|(_, bounds)| bounds.contains(&point))
340 .or_else(|| {
341 blocks.iter().min_by_key(|(_, bounds)| {
342 let above = (bounds.origin.y - point.y).abs();
343 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
344 f32::from(above.min(below)) as i64
345 })
346 })
347 .map(|(ix, _)| *ix)
348 }
349
350 pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
359 let texts = &self.0.borrow().texts;
360 let painted = texts.iter().find(|painted| painted.block == ix)?;
361 Some((
362 painted.layout.bounds().origin.y,
363 painted.layout.line_height(),
364 ))
365 }
366
367 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
369 self.0
370 .borrow()
371 .blocks
372 .iter()
373 .find(|(block, _)| *block == ix)
374 .map(|(_, bounds)| *bounds)
375 }
376
377 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
382 self.0
383 .borrow()
384 .languages
385 .iter()
386 .find(|(block, _)| *block == ix)
387 .map(|(_, bounds)| *bounds)
388 }
389
390 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
395 self.0
396 .borrow()
397 .pictures
398 .iter()
399 .find(|(block, _)| *block == ix)
400 .map(|(_, bounds)| *bounds)
401 }
402
403 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
404 self.0.borrow_mut().texts.push(Painted {
405 block,
406 part,
407 range,
408 layout,
409 });
410 }
411
412 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
413 self.0.borrow_mut().blocks.push((ix, bounds));
414 }
415
416 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
417 self.0.borrow_mut().languages.push((ix, bounds));
418 }
419
420 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
421 self.0.borrow_mut().pictures.push((ix, bounds));
422 }
423
424 fn clear(&self) {
425 let mut frames = self.0.borrow_mut();
426 frames.texts.clear();
427 frames.blocks.clear();
428 frames.languages.clear();
429 frames.pictures.clear();
430 }
431}
432
433#[derive(Clone, Copy)]
439struct Overlay<'a> {
440 block: usize,
441 part: Part,
442 selection: Option<Selection>,
443 caret_on: bool,
444 layouts: Option<&'a BlockLayouts>,
445 annotations: &'a [(Selection, Annotation)],
447 placeholder: Option<&'a SharedString>,
450 caption: Caption,
451}
452
453impl<'a> Overlay<'a> {
454 fn at(self, part: Part) -> Self {
455 Self { part, ..self }
456 }
457
458 fn here(&self) -> Cursor {
459 Cursor::new(self.block, self.part, 0)
460 }
461
462 fn caret_painted(&self) -> Option<usize> {
468 self.caret_on.then(|| self.caret()).flatten()
469 }
470
471 fn caret(&self) -> Option<usize> {
473 self.selection
474 .map(|selection| selection.head)
475 .filter(|head| head.block == self.block && head.part == self.part)
476 .map(|head| head.offset)
477 }
478
479 fn selected(&self, len: usize) -> Option<Range<usize>> {
481 self.clip(self.selection?, len)
482 }
483
484 fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
487 self.annotations
488 .iter()
489 .filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
490 .collect()
491 }
492
493 fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
499 if selection.is_collapsed() {
500 return None;
501 }
502 let (start, end) = selection.ordered();
503 let here = self.here();
504 let (first, last) = (
505 Cursor::new(start.block, start.part, 0),
506 Cursor::new(end.block, end.part, 0),
507 );
508 if here < first || here > last {
509 return None;
510 }
511 let from = if here == first { start.offset } else { 0 };
512 let to = if here == last { end.offset } else { len };
513 (from < to).then_some(from..to.min(len))
514 }
515
516 fn covers_block(&self) -> bool {
520 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
521 return false;
522 };
523 let (start, end) = selection.ordered();
524 start.block < self.block && self.block < end.block
525 }
526}
527
528pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
530 render(&crate::parse(source), Caption::default(), window, cx)
531}
532
533pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
535 render_with(
536 doc,
537 Editing {
538 caption,
539 ..Editing::default()
540 },
541 window,
542 cx,
543 )
544}
545
546pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
554 let Editing {
555 selection,
556 caret_on,
557 layouts,
558 annotations,
559 placeholder,
560 caption,
561 typography,
562 } = editing;
563 let reset = layouts.map(|layouts| {
569 let layouts = layouts.clone();
570 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
571 .absolute()
572 .size(px(0.0))
573 });
574 let theme = Theme::of(cx).clone();
577 let typography = typography.unwrap_or_else(|| Typography::of(cx));
578 let mut column = div().flex().flex_col().children(reset);
579
580 for (ix, block) in doc.blocks.iter().enumerate() {
581 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
582 None => 0.0,
583 Some(previous) if tight(previous, block) => LIST_GAP,
584 Some(_) => BLOCK_GAP,
585 };
586 let overlay = Overlay {
587 block: ix,
588 part: Part::Body,
589 selection,
590 caret_on,
591 layouts,
592 annotations,
593 placeholder: placeholder.as_ref(),
594 caption,
595 };
596 let frame = layouts.map(|layouts| {
599 let layouts = layouts.clone();
600 canvas(
601 move |bounds, _, _| layouts.record_block(ix, bounds),
602 |_, _, _, _| (),
603 )
604 .absolute()
605 .size_full()
606 });
607 column = column.child(
608 div()
614 .mt(px(gap))
615 .pl(px(block.indent as f32 * INDENT_WIDTH))
616 .child(
617 div()
618 .w_full()
619 .relative()
620 .children(frame)
621 .when(overlay.covers_block() && block.opaque(), |el| {
626 el.rounded(px(4.0)).bg(theme.selection)
627 })
628 .child(block_element(
629 block,
630 overlay,
631 &typography,
632 &theme,
633 window,
634 cx,
635 )),
636 ),
637 );
638 }
639
640 column.into_any_element()
641}
642
643fn tight(previous: &Block, next: &Block) -> bool {
645 let marker = |block: &Block| {
646 matches!(
647 block.kind,
648 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
649 )
650 };
651 marker(previous) && (marker(next) || next.indent > previous.indent)
652}
653
654fn block_element(
655 block: &Block,
656 overlay: Overlay,
657 typography: &Typography,
658 theme: &Theme,
659 window: &mut Window,
660 cx: &mut App,
661) -> AnyElement {
662 let body = overlay.at(Part::Body);
663 match &block.kind {
664 BlockKind::Paragraph(text) => text_element(
665 text,
666 typography.body.size(),
667 typography.body.line_height(),
668 FontWeight::NORMAL,
669 body,
670 theme,
671 ),
672 BlockKind::Heading { level, text } => {
673 let heading = typography.heading(*level);
674 text_element(
675 text,
676 heading.size(),
677 heading.line_height(),
678 heading.weight,
679 body,
680 theme,
681 )
682 }
683 BlockKind::Bullet(text) => {
684 marker_row(disc(typography, theme), text, body, typography, theme)
685 }
686 BlockKind::Ordered { number, text } => marker_row(
687 div()
688 .flex_none()
689 .w(px(MARKER_WIDTH))
690 .text_size(px(typography.body.size()))
691 .line_height(px(typography.body.line_height()))
692 .text_color(theme.text_muted)
693 .child(SharedString::from(format!("{number}.")))
694 .into_any_element(),
695 text,
696 body,
697 typography,
698 theme,
699 ),
700 BlockKind::Task { checked, text } => marker_row(
701 checkbox(*checked, typography, theme),
702 text,
703 body,
704 typography,
705 theme,
706 ),
707 BlockKind::Quote(text) => div()
708 .border_l_2()
709 .border_color(theme.border_strong)
710 .pl(px(12.0))
711 .pr(px(10.0))
712 .py(px(2.0))
713 .text_color(theme.text_muted)
714 .child(text_element(
715 text,
716 typography.body.size(),
717 typography.body.line_height(),
718 FontWeight::NORMAL,
719 body,
720 theme,
721 ))
722 .into_any_element(),
723 BlockKind::Code { language, code } => {
724 let overlay = overlay.at(Part::Code);
725 let painted = overlay
729 .caret()
730 .is_none()
731 .then(|| block::render(language.as_deref(), &code.text, window, cx))
732 .flatten();
733 match painted {
734 Some(element) => div()
737 .when(overlay.covers_block(), |el| {
738 el.rounded(px(4.0)).bg(theme.selection)
739 })
740 .child(element)
741 .into_any_element(),
742 None => code_block(
743 language.as_deref(),
744 &code.text,
745 overlay,
746 typography,
747 theme,
748 window,
749 cx,
750 ),
751 }
752 }
753 BlockKind::Image { url, alt, width } => image(url, alt, *width, overlay, typography, theme),
754 BlockKind::Bookmark { url, form } => {
755 bookmark(overlay.block, url, *form, typography, theme, cx)
756 }
757 BlockKind::Table {
758 align,
759 header,
760 rows,
761 } => table(align, header, rows, overlay, typography, theme, window),
762 BlockKind::Rule => div()
763 .h(px(1.0))
764 .w_full()
765 .bg(theme.border)
766 .into_any_element(),
767 }
768}
769
770fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
772 div()
773 .flex_none()
774 .w(px(MARKER_WIDTH))
775 .h(px(typography.body.line_height()))
776 .flex()
777 .items_center()
778 .child(
779 div()
780 .ml(px(1.0))
781 .w(px(5.0))
782 .h(px(5.0))
783 .rounded_full()
784 .bg(theme.text_faint),
785 )
786 .into_any_element()
787}
788
789fn checkbox(checked: bool, typography: &Typography, theme: &Theme) -> AnyElement {
790 let mut box_ = div()
791 .w(px(13.0))
792 .h(px(13.0))
793 .rounded(px(3.5))
794 .border_1()
795 .flex()
796 .items_center()
797 .justify_center();
798 box_ = if checked {
799 box_.bg(theme.solid)
800 .border_color(theme.solid)
801 .text_style(TextStyle::Caption)
802 .text_color(theme.on_solid)
803 .child("✓")
804 } else {
805 box_.border_color(theme.border_strong)
806 };
807
808 div()
809 .flex_none()
810 .w(px(MARKER_WIDTH))
811 .h(px(typography.body.line_height()))
812 .flex()
813 .items_center()
814 .child(box_)
815 .into_any_element()
816}
817
818fn marker_row(
819 marker: AnyElement,
820 text: &Text,
821 overlay: Overlay,
822 typography: &Typography,
823 theme: &Theme,
824) -> AnyElement {
825 div()
826 .flex()
827 .flex_row()
828 .gap(px(MARKER_GAP))
829 .child(marker)
830 .child(div().flex_1().min_w_0().child(text_element(
831 text,
832 typography.body.size(),
833 typography.body.line_height(),
834 FontWeight::NORMAL,
835 overlay,
836 theme,
837 )))
838 .into_any_element()
839}
840
841pub struct Flat {
844 pub text: SharedString,
845 pub runs: Vec<TextRun>,
846 pub links: Vec<(Range<usize>, String)>,
847 pub code: Vec<Range<usize>>,
848 pub chips: Vec<Range<usize>>,
849}
850
851pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
854 let mut cuts: Vec<usize> = text
855 .marks
856 .iter()
857 .flat_map(|span| [span.range.start, span.range.end])
858 .chain([0, text.text.len()])
859 .filter(|cut| *cut <= text.text.len())
860 .collect();
861 cuts.sort_unstable();
862 cuts.dedup();
863
864 let mut runs = Vec::new();
865 let mut links: Vec<(Range<usize>, String)> = Vec::new();
866 let mut code: Vec<Range<usize>> = Vec::new();
867 let mut chips: Vec<Range<usize>> = Vec::new();
868
869 for pair in cuts.windows(2) {
870 let (start, end) = (pair[0], pair[1]);
871 let covering = text
872 .marks
873 .iter()
874 .filter(|span| span.range.start <= start && span.range.end >= end);
875
876 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
877 let mut chip = false;
878 let mut link = None;
879 for span in covering {
880 match &span.mark {
881 Mark::Bold => bold = true,
882 Mark::Italic => italic = true,
883 Mark::Strike => strike = true,
884 Mark::Code => mono = true,
885 Mark::Mention { url, .. } => {
886 chip = true;
887 link = Some(url.clone());
888 }
889 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
890 }
891 }
892
893 if mono {
894 match code.last_mut() {
895 Some(range) if range.end == start => range.end = end,
896 _ => code.push(start..end),
897 }
898 }
899 if chip {
900 match chips.last_mut() {
901 Some(range) if range.end == start => range.end = end,
902 _ => chips.push(start..end),
903 }
904 }
905 if let Some(url) = &link {
906 match links.last_mut() {
907 Some((range, last)) if range.end == start && last == url => range.end = end,
908 _ => links.push((start..end, url.clone())),
909 }
910 }
911
912 let mut face = font(if mono {
913 theme.font_mono.clone()
914 } else {
915 theme.font_sans.clone()
916 });
917 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
918 FontWeight::SEMIBOLD
919 } else {
920 base_weight
921 };
922 face.style = if italic {
923 FontStyle::Italic
924 } else {
925 FontStyle::Normal
926 };
927
928 runs.push(TextRun {
929 len: end - start,
930 font: face,
931 color: if mono { theme.code_text } else { theme.text },
935 background_color: None,
936 underline: (link.is_some() && !chip).then_some(UnderlineStyle {
937 color: Some(theme.text_muted),
938 thickness: px(1.0),
939 wavy: false,
940 }),
941 strikethrough: strike.then_some(StrikethroughStyle {
942 thickness: px(1.0),
943 color: Some(theme.text_muted),
944 }),
945 });
946 }
947
948 Flat {
949 text: text.text.clone().into(),
950 runs,
951 links,
952 code,
953 chips,
954 }
955}
956
957fn text_element(
958 text: &Text,
959 size: f32,
960 line_height: f32,
961 weight: FontWeight,
962 overlay: Overlay,
963 theme: &Theme,
964) -> AnyElement {
965 let flat = flatten(text, weight, theme);
966 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
967}
968
969fn painted_text(
975 flat: Flat,
976 len: usize,
977 size: f32,
978 line_height: f32,
979 overlay: Overlay,
980 theme: &Theme,
981) -> AnyElement {
982 let (ix, part) = (overlay.block, overlay.part);
983 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
984 let span = 0..len;
985 let hint = overlay
988 .placeholder
989 .filter(|_| len == 0 && overlay.caret().is_some())
992 .map(|hint| {
993 div()
994 .absolute()
995 .text_color(theme.text_faint)
996 .child(hint.clone())
997 });
998 let styled = StyledText::new(flat.text).with_runs(flat.runs);
999 let layout = styled.layout().clone();
1000
1001 let painted: AnyElement = if flat.links.is_empty() {
1002 styled.into_any_element()
1003 } else {
1004 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
1005 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
1006 .on_click(ranges, move |clicked, _window, cx| {
1007 if let Some(url) = urls.get(clicked) {
1008 cx.open_url(url);
1009 }
1010 })
1011 .into_any_element()
1012 };
1013
1014 let wash = theme.code_wash;
1018 let code_ranges = flat.code;
1019 let chip_wash = theme.element_hover;
1020 let chip_edge = theme.border;
1021 let chip_ranges = flat.chips;
1022 let caret_color = theme.caret;
1023 let selection_color = theme.selection;
1024 let annotated = overlay.annotated(len, theme);
1025 let layouts = overlay.layouts.cloned();
1026 let underlay = canvas(
1027 |_, _, _| (),
1028 move |_, _, window, _| {
1029 if let Some(layouts) = &layouts {
1030 layouts.record(ix, part, span.clone(), layout.clone());
1031 }
1032 for (range, wash) in &annotated {
1035 for rect in range_rects(&layout, range, 0.0, 0.0) {
1036 window.paint_quad(quad(
1037 rect,
1038 px(2.0),
1039 *wash,
1040 px(0.0),
1041 gpui::transparent_black(),
1042 BorderStyle::default(),
1043 ));
1044 }
1045 }
1046 if let Some(range) = &selected {
1050 for rect in range_rects(&layout, range, 0.0, 0.0) {
1051 window.paint_quad(quad(
1052 rect,
1053 px(2.0),
1054 selection_color,
1055 px(0.0),
1056 gpui::transparent_black(),
1057 BorderStyle::default(),
1058 ));
1059 }
1060 }
1061 if let Some(offset) = caret
1062 && let Some(head) = layout.position_for_index(offset)
1063 {
1064 window.paint_quad(quad(
1065 caret_quad(head, size, layout.line_height()),
1066 px(0.0),
1067 caret_color,
1068 px(0.0),
1069 gpui::transparent_black(),
1070 BorderStyle::default(),
1071 ));
1072 }
1073 for range in &code_ranges {
1074 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1075 window.paint_quad(quad(
1076 rect,
1077 px(INLINE_CODE_RADIUS),
1078 wash,
1079 px(0.0),
1080 gpui::transparent_black(),
1081 BorderStyle::default(),
1082 ));
1083 }
1084 }
1085 for range in &chip_ranges {
1088 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1089 window.paint_quad(quad(
1090 rect,
1091 px(Theme::control_radius()),
1092 chip_wash,
1093 px(1.0),
1094 chip_edge,
1095 BorderStyle::Solid,
1096 ));
1097 }
1098 }
1099 },
1100 )
1101 .absolute()
1102 .size_full();
1103
1104 div()
1105 .text_size(px(size))
1106 .line_height(px(line_height))
1107 .relative()
1108 .child(underlay)
1109 .children(hint)
1110 .child(painted)
1111 .into_any_element()
1112}
1113
1114fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1120 let inset = (line_height - px(size)) / 2.0;
1121 Bounds::new(
1122 head + point(px(0.0), inset),
1123 gpui::size(px(CARET_WIDTH), px(size)),
1124 )
1125}
1126
1127fn range_rects(
1129 layout: &gpui::TextLayout,
1130 range: &Range<usize>,
1131 pad_x: f32,
1132 inset_y: f32,
1133) -> Vec<Bounds<Pixels>> {
1134 let mut rects = Vec::new();
1135 let line_height = layout.line_height();
1136 let mut cursor = range.start;
1137 let mut guard = 0;
1140 while cursor < range.end && guard < 256 {
1141 guard += 1;
1142 let Some(head) = layout.position_for_index(cursor) else {
1143 break;
1144 };
1145 let (row_end, next) = match layout.position_for_index(range.end) {
1146 Some(tail) if tail.y == head.y => (range.end, range.end),
1147 _ => {
1148 let (mut low, mut high) = (cursor, range.end);
1149 while high - low > 1 {
1150 let mid = low + (high - low) / 2;
1151 match layout.position_for_index(mid) {
1152 Some(probe) if probe.y == head.y => low = mid,
1153 _ => high = mid,
1154 }
1155 }
1156 (low, high)
1157 }
1158 };
1159 if let Some(tail) = layout.position_for_index(row_end)
1160 && tail.x > head.x
1161 {
1162 rects.push(Bounds::new(
1163 point(head.x - px(pad_x), head.y + px(inset_y)),
1164 size(
1165 tail.x - head.x + px(2.0 * pad_x),
1166 line_height - px(2.0 * inset_y),
1167 ),
1168 ));
1169 }
1170 cursor = next.max(cursor + 1);
1171 }
1172 rects
1173}
1174
1175fn code_block(
1176 language: Option<&str>,
1177 code: &str,
1178 overlay: Overlay,
1179 typography: &Typography,
1180 theme: &Theme,
1181 window: &mut Window,
1182 cx: &mut App,
1183) -> AnyElement {
1184 let ix = overlay.block;
1185 let spans = crate::highlight::spans(cx, language, code);
1189 let mono = font(theme.font_mono.clone());
1190 let run = |len: usize, color: Hsla| TextRun {
1191 len,
1192 font: mono.clone(),
1193 color,
1194 background_color: None,
1195 underline: None,
1196 strikethrough: None,
1197 };
1198 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1201 let mut offset = 0usize;
1202 let lines: Vec<AnyElement> = code
1203 .split('\n')
1204 .map(|line| {
1205 let start = offset;
1206 offset += line.len() + 1;
1207 let mut runs = Vec::new();
1208 let mut pos = 0usize;
1211 if let Some(spans) = &spans {
1212 let end = start + line.len();
1213 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1214 let s = range.start.clamp(start, end) - start;
1215 let e = range.end.min(end) - start;
1216 if s > pos {
1217 runs.push(run(s - pos, theme.text));
1218 }
1219 runs.push(run(e - s, theme.syntax.color(*kind)));
1220 pos = e;
1221 }
1222 }
1223 if pos < line.len() {
1224 runs.push(run(line.len() - pos, theme.text));
1225 }
1226 if runs.is_empty() {
1227 runs.push(run(0, theme.text));
1228 }
1229 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1230 rows.push((start..start + line.len(), styled.layout().clone()));
1231 styled.into_any_element()
1232 })
1233 .collect();
1234
1235 let caret = overlay.caret_painted();
1236 let selected = overlay.selected(code.len());
1237 let sink = overlay.layouts.cloned();
1238 let code_size = typography.code.size();
1239 let annotated = overlay.annotated(code.len(), theme);
1240 let (caret_color, selection_color) = (theme.caret, theme.selection);
1241 let underlay = canvas(
1242 |_, _, _| (),
1243 move |_, _, window, _| {
1244 for (span, layout) in &rows {
1245 if let Some(sink) = &sink {
1246 sink.record(ix, Part::Code, span.clone(), layout.clone());
1247 }
1248 for (range, wash) in &annotated {
1249 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1250 if from < to {
1251 for rect in
1252 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1253 {
1254 window.paint_quad(quad(
1255 rect,
1256 px(2.0),
1257 *wash,
1258 px(0.0),
1259 gpui::transparent_black(),
1260 BorderStyle::default(),
1261 ));
1262 }
1263 }
1264 }
1265 if let Some(range) = &selected {
1266 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1267 if from < to {
1268 for rect in
1269 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1270 {
1271 window.paint_quad(quad(
1272 rect,
1273 px(2.0),
1274 selection_color,
1275 px(0.0),
1276 gpui::transparent_black(),
1277 BorderStyle::default(),
1278 ));
1279 }
1280 }
1281 }
1282 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1283 && let Some(head) = layout.position_for_index(offset - span.start)
1284 {
1285 window.paint_quad(quad(
1286 caret_quad(head, code_size, layout.line_height()),
1287 px(0.0),
1288 caret_color,
1289 px(0.0),
1290 gpui::transparent_black(),
1291 BorderStyle::default(),
1292 ));
1293 }
1294 }
1295 },
1296 )
1297 .absolute()
1298 .size_full();
1299
1300 div()
1301 .rounded(px(Theme::panel_radius()))
1302 .bg(theme.ink(0.035))
1303 .border_1()
1304 .border_color(theme.border)
1305 .overflow_hidden()
1306 .relative()
1307 .child(
1311 div()
1312 .relative()
1313 .flex()
1314 .flex_row()
1315 .items_center()
1316 .px(px(CODE_PADDING_X))
1317 .py(px(5.0))
1318 .border_b_1()
1319 .border_color(theme.border)
1320 .bg(theme.ink(0.02))
1321 .text_style(TextStyle::Subheadline)
1322 .text_color(match language {
1323 Some(_) => theme.text_muted,
1324 None => theme.text_faint,
1325 })
1326 .child(
1330 div()
1331 .relative()
1332 .children(overlay.layouts.map(|layouts| {
1333 let layouts = layouts.clone();
1334 canvas(
1335 move |bounds, _, _| layouts.record_language(ix, bounds),
1336 |_, _, _, _| (),
1337 )
1338 .absolute()
1339 .size_full()
1340 }))
1341 .child(SharedString::from(
1342 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1343 )),
1344 ),
1345 )
1346 .child(
1347 contain_sideways(div().id(ElementId::named_usize("md-code", ix)))
1348 .overflow_x_scroll()
1349 .restrict_scroll_to_axis()
1353 .relative()
1354 .flex()
1355 .flex_row()
1356 .py(px(CODE_PADDING_Y))
1357 .text_size(px(typography.code.size()))
1358 .line_height(px(typography.code.line_height()))
1359 .whitespace_nowrap()
1360 .child(underlay)
1361 .child(
1362 div()
1369 .flex()
1370 .flex_col()
1371 .items_start()
1372 .px(px(CODE_PADDING_X))
1373 .children(lines),
1374 ),
1375 )
1376 .child(copy_button(code, ix, theme, window, cx))
1377 .into_any_element()
1378}
1379
1380fn copy_button(
1387 code: &str,
1388 ix: usize,
1389 theme: &Theme,
1390 window: &mut Window,
1391 cx: &mut App,
1392) -> AnyElement {
1393 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1394 let showing = *copied.read(cx);
1395 let text: SharedString = code.to_string().into();
1396
1397 div()
1398 .id(ElementId::named_usize("md-copy", ix))
1399 .absolute()
1400 .top(px(3.0))
1401 .right(px(5.0))
1402 .h(px(20.0))
1403 .px(px(6.0))
1404 .rounded(px(5.0))
1405 .flex()
1406 .items_center()
1407 .cursor_pointer()
1408 .text_style(TextStyle::Caption)
1409 .text_color(theme.text_muted)
1410 .hover(|el| el.bg(theme.element_hover))
1411 .child(if showing { "Copied" } else { "Copy" })
1412 .on_click({
1413 let copied = copied.clone();
1414 move |_, _, cx| {
1415 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1416 copied.update(cx, |state, cx| {
1417 *state = true;
1418 cx.notify();
1419 });
1420 }
1421 })
1422 .on_hover(move |hovering, _, cx| {
1423 if !*hovering && *copied.read(cx) {
1424 copied.update(cx, |state, cx| {
1425 *state = false;
1426 cx.notify();
1427 });
1428 }
1429 })
1430 .into_any_element()
1431}
1432
1433fn image(
1440 url: &str,
1441 alt: &Text,
1442 width: Option<u32>,
1443 overlay: Overlay,
1444 typography: &Typography,
1445 theme: &Theme,
1446) -> AnyElement {
1447 let hint = SharedString::new_static(CAPTION_HINT);
1448 let overlay = Overlay {
1449 placeholder: Some(&hint),
1450 ..overlay.at(Part::Caption)
1451 };
1452 let picture = if url.is_empty() {
1453 div()
1454 .h(px(IMAGE_EMPTY_HEIGHT))
1455 .flex()
1456 .items_center()
1457 .px(px(CARD_PADDING))
1458 .rounded(px(Theme::button_radius()))
1459 .border_1()
1460 .border_dashed()
1461 .border_color(theme.border)
1462 .text_size(px(typography.body.size()))
1463 .text_color(theme.text_muted)
1464 .child(IMAGE_EMPTY)
1465 } else {
1466 let picture = match url.contains("://") {
1470 true => img(SharedString::from(url.to_string())),
1471 false => img(std::path::PathBuf::from(url)),
1472 };
1473 let box_ = div()
1474 .relative()
1475 .rounded(px(Theme::button_radius()))
1476 .overflow_hidden()
1477 .border_1()
1478 .border_color(theme.border)
1479 .children(overlay.layouts.map(|layouts| {
1480 let layouts = layouts.clone();
1481 let ix = overlay.block;
1482 canvas(
1483 move |bounds, _, _| layouts.record_picture(ix, bounds),
1484 |_, _, _, _| (),
1485 )
1486 .absolute()
1487 .size_full()
1488 }));
1489 match width {
1490 Some(width) => box_
1495 .self_start()
1496 .max_w_full()
1497 .w(px(width as f32))
1498 .child(picture.w(px(width as f32)).max_w_full()),
1499 None => box_.child(picture.max_w_full()),
1502 }
1503 };
1504 div()
1505 .flex()
1506 .flex_col()
1507 .gap(px(CAPTION_GAP))
1508 .child(picture)
1509 .when(
1512 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1513 |el| {
1514 el.child(text_element(
1515 alt,
1516 typography.caption.size(),
1517 typography.caption.line_height(),
1518 FontWeight::NORMAL,
1519 overlay,
1520 theme,
1521 ))
1522 },
1523 )
1524 .into_any_element()
1525}
1526
1527fn bookmark(
1539 ix: usize,
1540 url: &str,
1541 form: Form,
1542 typography: &Typography,
1543 theme: &Theme,
1544 cx: &App,
1545) -> AnyElement {
1546 let preview = preview::of(cx, url).unwrap_or_default();
1547 let host = SharedString::from(preview::host(url).to_string());
1548 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1549 let title = preview
1550 .title
1551 .clone()
1552 .unwrap_or_else(|| SharedString::from(url.to_string()));
1553
1554 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1557 let site = host.clone();
1558 let mark = move |size: f32| {
1559 let host = site.clone();
1560 match icon.clone() {
1561 Some(icon) => img(icon)
1562 .size(px(size))
1563 .rounded(px(size / 4.0))
1564 .with_fallback(move || initial(&host, size, muted, wash))
1565 .into_any_element(),
1566 None => initial(&host, size, muted, wash),
1567 }
1568 };
1569
1570 if form == Form::Chip {
1571 let open = url.to_string();
1572 let pill = div()
1573 .id(ElementId::named_usize("md-chip", ix))
1574 .flex()
1575 .flex_row()
1576 .items_center()
1577 .gap(px(6.0))
1578 .px(px(CHIP_BLOCK_PAD_X))
1579 .py(px(CHIP_BLOCK_PAD_Y))
1580 .rounded(px(Theme::control_radius()))
1581 .border_1()
1582 .border_color(theme.border)
1583 .bg(theme.element_hover)
1584 .text_size(px(typography.body.size()))
1585 .line_height(px(typography.body.line_height()))
1586 .text_color(theme.text)
1587 .cursor(CursorStyle::PointingHand)
1588 .hover(|el| el.bg(theme.element_active))
1589 .on_click(move |_, _, cx| cx.open_url(&open))
1590 .child(mark(CHIP_ICON))
1591 .child(
1594 div()
1595 .min_w_0()
1596 .truncate()
1597 .child(preview.title.unwrap_or(label)),
1598 );
1599 return div().flex().flex_row().child(pill).into_any_element();
1602 }
1603
1604 let words = div()
1605 .flex()
1606 .flex_col()
1607 .min_w_0()
1608 .px(px(CARD_PADDING))
1609 .py(px(CARD_PADDING - 2.0))
1610 .child(
1611 div()
1612 .truncate()
1613 .text_size(px(typography.body.size()))
1614 .line_height(px(typography.body.line_height()))
1615 .text_color(theme.text)
1616 .child(title),
1617 )
1618 .children(preview.description.map(|blurb| {
1619 div()
1620 .line_clamp(2)
1621 .text_size(px(typography.card.size()))
1622 .line_height(px(typography.card.line_height()))
1623 .text_color(theme.text_muted)
1624 .child(blurb)
1625 }))
1626 .child(
1627 div()
1628 .mt_auto()
1629 .pt(px(6.0))
1630 .flex()
1631 .items_center()
1632 .gap(px(6.0))
1633 .text_size(px(typography.card.size()))
1634 .text_color(theme.text_muted)
1635 .child(mark(CARD_ICON))
1636 .child(div().truncate().child(label)),
1637 );
1638
1639 let picture = corners(div(), form)
1640 .bg(theme.surface)
1641 .flex()
1642 .items_center()
1643 .justify_center()
1644 .overflow_hidden()
1645 .child(match preview.image {
1646 Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
1647 .with_fallback(move || mark(CARD_COVER))
1648 .into_any_element(),
1649 None => mark(CARD_COVER),
1650 });
1651
1652 let open = url.to_string();
1653 let card = div()
1654 .id(ElementId::named_usize("md-bookmark", ix))
1655 .flex()
1656 .w_full()
1657 .overflow_hidden()
1658 .rounded(px(Theme::button_radius()))
1659 .border(px(CARD_BORDER))
1660 .border_color(theme.border)
1661 .bg(theme.surface_card)
1662 .cursor(CursorStyle::PointingHand)
1663 .hover(|el| el.bg(theme.element_hover))
1664 .on_click(move |_, _, cx| cx.open_url(&open));
1665
1666 if form == Form::Embed {
1667 card.flex_col()
1668 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1669 .child(words.w_full())
1670 } else {
1671 card.h(px(CARD_HEIGHT))
1672 .child(words.flex_1())
1673 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1674 }
1675 .into_any_element()
1676}
1677
1678fn corners<T: Styled>(element: T, form: Form) -> T {
1682 let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
1683 match form {
1684 Form::Embed => element.rounded_t(corner),
1685 _ => element.rounded_r(corner),
1686 }
1687}
1688
1689fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1692 div()
1693 .flex_none()
1694 .size(px(size))
1695 .rounded(px(size / 4.0))
1696 .bg(wash)
1697 .flex()
1698 .items_center()
1699 .justify_center()
1700 .text_size(px(size * 0.55))
1701 .text_color(color)
1702 .child(SharedString::from(
1703 host.chars()
1704 .next()
1705 .unwrap_or('?')
1706 .to_uppercase()
1707 .to_string(),
1708 ))
1709 .into_any_element()
1710}
1711
1712fn table(
1719 align: &[Align],
1720 header: &[Text],
1721 rows: &[Vec<Text>],
1722 overlay: Overlay,
1723 typography: &Typography,
1724 theme: &Theme,
1725 window: &mut Window,
1726) -> AnyElement {
1727 let ix = overlay.block;
1728 let all: Vec<&[Text]> = std::iter::once(header)
1729 .filter(|row| !row.is_empty())
1730 .chain(rows.iter().map(|row| row.as_slice()))
1731 .collect();
1732 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1733 if columns == 0 {
1734 return gpui::Empty.into_any_element();
1735 }
1736 let has_header = !header.is_empty();
1737
1738 let text_system = window.text_system();
1739 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1740 let mut content = vec![0.0f32; columns];
1741 for (r, row) in all.iter().enumerate() {
1742 let weight = if has_header && r == 0 {
1743 FontWeight::BOLD
1744 } else {
1745 FontWeight::NORMAL
1746 };
1747 let mut out = Vec::with_capacity(columns);
1748 for (c, natural) in content.iter_mut().enumerate() {
1749 let Some(cell) = row.get(c) else {
1750 out.push(None);
1751 continue;
1752 };
1753 let flat = flatten(cell, weight, theme);
1754 if !flat.text.is_empty() {
1755 let width = f32::from(
1756 text_system
1757 .shape_line(
1758 flat.text.clone(),
1759 px(typography.body.size()),
1760 &flat.runs,
1761 None,
1762 )
1763 .width(),
1764 );
1765 *natural = natural.max(width);
1766 }
1767 out.push(Some(flat));
1768 }
1769 flats.push(out);
1770 }
1771
1772 let naturals: Vec<f32> = content
1773 .iter()
1774 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
1775 .collect();
1776 let minimums: Vec<f32> = naturals
1777 .iter()
1778 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
1779 .collect();
1780 let hairline = theme.hairline(0.10);
1781
1782 let mut inner = div()
1783 .flex()
1784 .flex_col()
1785 .w_full()
1786 .min_w(px(minimums.iter().sum::<f32>()));
1787 for (r, row) in flats.into_iter().enumerate() {
1788 if r > 0 {
1789 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
1790 }
1791 let mut row_el = div().flex().flex_row();
1792 for (c, cell) in row.into_iter().enumerate() {
1793 let mut cell_el = div()
1794 .flex_grow(naturals[c])
1795 .flex_shrink(naturals[c])
1796 .flex_basis(px(0.0))
1797 .min_w(px(minimums[c]))
1798 .p(px(TABLE_CELL_PADDING))
1799 .text_size(px(typography.body.size()))
1800 .line_height(px(typography.body.line_height()));
1801 cell_el = match align.get(c).copied().unwrap_or_default() {
1802 Align::Left => cell_el,
1803 Align::Center => cell_el.text_center(),
1804 Align::Right => cell_el.text_right(),
1805 };
1806 if let Some(flat) = cell {
1807 let row = if has_header { r } else { r + 1 };
1811 let len = flat.text.len();
1812 cell_el = cell_el.child(painted_text(
1813 flat,
1814 len,
1815 typography.body.size(),
1816 typography.body.line_height(),
1817 overlay.at(Part::Cell { row, column: c }),
1818 theme,
1819 ));
1820 }
1821 row_el = row_el.child(cell_el);
1822 }
1823 inner = inner.child(row_el);
1824 }
1825
1826 contain_sideways(div().id(ElementId::named_usize("md-table", ix)))
1827 .w_full()
1828 .overflow_x_scroll()
1829 .restrict_scroll_to_axis()
1830 .child(inner)
1831 .into_any_element()
1832}
1833
1834fn contain_sideways<E: gpui::InteractiveElement>(el: E) -> E {
1847 el.on_scroll_wheel(|event, window, cx| {
1848 let delta = event.delta.pixel_delta(window.line_height());
1849 if delta.x.abs() > delta.y.abs() {
1853 cx.stop_propagation();
1854 }
1855 })
1856}