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::Theme;
19
20use crate::{
21 doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, Text},
22 preview,
23 select::{Cursor, Selection},
24};
25
26const BLOCK_GAP: f32 = 12.0;
28const LIST_GAP: f32 = 4.0;
29const TEXT_SIZE: f32 = 14.0;
31const LINE_HEIGHT: f32 = 22.0;
32const INDENT_WIDTH: f32 = 22.0;
34const MARKER_WIDTH: f32 = 18.0;
36const MARKER_GAP: f32 = 8.0;
37const CODE_TEXT_SIZE: f32 = 12.5;
39const CODE_LINE_HEIGHT: f32 = 18.0;
40const CODE_PADDING_X: f32 = 12.0;
41const CODE_PADDING_Y: f32 = 10.0;
42pub const PLAIN_LANGUAGE: &str = "Plain";
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_RADIUS: f32 = 6.0;
53const CHIP_PAD_X: f32 = 4.0;
54const CHIP_INSET_Y: f32 = 1.0;
55const CHIP_BLOCK_PAD_X: f32 = 8.0;
58const CHIP_BLOCK_PAD_Y: f32 = 3.0;
59const CHIP_ICON: f32 = 15.0;
60const CARD_HEIGHT: f32 = 116.0;
64const CARD_IMAGE_WIDTH: f32 = 180.0;
65const CARD_COVER_HEIGHT: f32 = 200.0;
66const CARD_PADDING: f32 = 14.0;
67const CARD_TEXT_SIZE: f32 = 12.0;
68const CARD_LINE_HEIGHT: f32 = 17.0;
69const CARD_ICON: f32 = 16.0;
70const CARD_COVER: f32 = 44.0;
71const IMAGE_RADIUS: f32 = 8.0;
74const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
75const CAPTION_TEXT_SIZE: f32 = 11.5;
76const CAPTION_LINE_HEIGHT: f32 = 17.0;
77const CAPTION_GAP: f32 = 4.0;
78const IMAGE_EMPTY: &str = "Add an image";
80const CAPTION_HINT: &str = "Write a caption";
81const TABLE_CELL_PADDING: f32 = 12.0;
84const TABLE_DIVIDER: f32 = 1.0;
85const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
88const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
90
91#[derive(Clone, Default)]
98pub struct BlockLayouts(Rc<RefCell<Frames>>);
99
100#[derive(Default)]
101struct Frames {
102 texts: Vec<Painted>,
103 blocks: Vec<(usize, Bounds<Pixels>)>,
106 languages: Vec<(usize, Bounds<Pixels>)>,
109}
110
111struct Painted {
118 block: usize,
119 part: Part,
120 range: Range<usize>,
121 layout: TextLayout,
122}
123
124impl BlockLayouts {
125 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
131 let entries = &self.0.borrow().texts;
132 let cursor = |painted: &Painted| {
133 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
134 Cursor::new(
135 painted.block,
136 painted.part,
137 painted.range.start + offset.min(painted.range.len()),
138 )
139 };
140 if let Some(painted) = entries
141 .iter()
142 .find(|painted| painted.layout.bounds().contains(&point))
143 {
144 return Some(cursor(painted));
145 }
146 entries
147 .iter()
148 .min_by_key(|painted| {
149 let bounds = painted.layout.bounds();
150 let above = (bounds.origin.y - point.y).abs();
151 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
152 f32::from(above.min(below)) as i64
153 })
154 .map(cursor)
155 }
156
157 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
163 let entries = &self.0.borrow().texts;
164 let painted = entries.iter().find(|painted| {
165 painted.block == at.block
166 && painted.part == at.part
167 && painted.range.start <= at.offset
168 && at.offset <= painted.range.end
169 })?;
170 let point = painted
171 .layout
172 .position_for_index(at.offset - painted.range.start)?;
173 Some((point, painted.layout.line_height()))
174 }
175
176 pub fn step_row(
187 &self,
188 at: Cursor,
189 from: Point<Pixels>,
190 down: bool,
191 ) -> Option<(Cursor, Pixels)> {
192 let entries = &self.0.borrow().texts;
193 let ix = entries.iter().position(|painted| {
194 painted.block == at.block
195 && painted.part == at.part
196 && painted.range.start <= at.offset
197 && at.offset <= painted.range.end
198 })?;
199 let here = &entries[ix];
200 let line = here.layout.line_height();
201 let index_at = |painted: &Painted, y: Pixels| {
202 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
203 (
204 Cursor::new(
205 painted.block,
206 painted.part,
207 painted.range.start + offset.min(painted.range.len()),
208 ),
209 y,
210 )
211 };
212
213 let bounds = here.layout.bounds();
216 let target = if down { from.y + line } else { from.y - line };
217 if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
218 return Some(index_at(here, target));
219 }
220
221 let next = match down {
222 true => entries.get(ix + 1)?,
223 false => entries.get(ix.checked_sub(1)?)?,
224 };
225 let bounds = next.layout.bounds();
227 let row = match down {
228 true => bounds.origin.y,
229 false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
230 };
231 Some(index_at(next, row))
232 }
233
234 pub fn over_text(&self, point: Point<Pixels>) -> bool {
241 self.0
242 .borrow()
243 .texts
244 .iter()
245 .any(|painted| painted.layout.bounds().contains(&point))
246 }
247
248 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
250 let blocks = &self.0.borrow().blocks;
251 blocks
252 .iter()
253 .find(|(_, bounds)| bounds.contains(&point))
254 .or_else(|| {
255 blocks.iter().min_by_key(|(_, bounds)| {
256 let above = (bounds.origin.y - point.y).abs();
257 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
258 f32::from(above.min(below)) as i64
259 })
260 })
261 .map(|(ix, _)| *ix)
262 }
263
264 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
266 self.0
267 .borrow()
268 .blocks
269 .iter()
270 .find(|(block, _)| *block == ix)
271 .map(|(_, bounds)| *bounds)
272 }
273
274 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
279 self.0
280 .borrow()
281 .languages
282 .iter()
283 .find(|(block, _)| *block == ix)
284 .map(|(_, bounds)| *bounds)
285 }
286
287 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
288 self.0.borrow_mut().texts.push(Painted {
289 block,
290 part,
291 range,
292 layout,
293 });
294 }
295
296 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
297 self.0.borrow_mut().blocks.push((ix, bounds));
298 }
299
300 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
301 self.0.borrow_mut().languages.push((ix, bounds));
302 }
303
304 fn clear(&self) {
305 let mut frames = self.0.borrow_mut();
306 frames.texts.clear();
307 frames.blocks.clear();
308 frames.languages.clear();
309 }
310}
311
312#[derive(Clone, Copy)]
318struct Overlay<'a> {
319 block: usize,
320 part: Part,
321 selection: Option<Selection>,
322 layouts: Option<&'a BlockLayouts>,
323 placeholder: Option<&'a SharedString>,
326}
327
328impl<'a> Overlay<'a> {
329 fn at(self, part: Part) -> Self {
330 Self { part, ..self }
331 }
332
333 fn here(&self) -> Cursor {
334 Cursor::new(self.block, self.part, 0)
335 }
336
337 fn caret(&self) -> Option<usize> {
339 self.selection
340 .map(|selection| selection.head)
341 .filter(|head| head.block == self.block && head.part == self.part)
342 .map(|head| head.offset)
343 }
344
345 fn selected(&self, len: usize) -> Option<Range<usize>> {
351 let selection = self.selection?;
352 if selection.is_collapsed() {
353 return None;
354 }
355 let (start, end) = selection.ordered();
356 let here = self.here();
357 let (first, last) = (
358 Cursor::new(start.block, start.part, 0),
359 Cursor::new(end.block, end.part, 0),
360 );
361 if here < first || here > last {
362 return None;
363 }
364 let from = if here == first { start.offset } else { 0 };
365 let to = if here == last { end.offset } else { len };
366 (from < to).then_some(from..to.min(len))
367 }
368
369 fn covers_block(&self) -> bool {
373 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
374 return false;
375 };
376 let (start, end) = selection.ordered();
377 start.block < self.block && self.block < end.block
378 }
379}
380
381pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
383 render(&crate::parse(source), window, cx)
384}
385
386pub fn render(doc: &Doc, window: &mut Window, cx: &mut App) -> AnyElement {
388 render_with_selection(doc, None, None, None, window, cx)
389}
390
391pub fn render_with_selection(
399 doc: &Doc,
400 selection: Option<Selection>,
401 layouts: Option<&BlockLayouts>,
402 placeholder: Option<SharedString>,
403 window: &mut Window,
404 cx: &mut App,
405) -> AnyElement {
406 let reset = layouts.map(|layouts| {
412 let layouts = layouts.clone();
413 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
414 .absolute()
415 .size(px(0.0))
416 });
417 let theme = Theme::of(cx).clone();
420 let mut column = div().flex().flex_col().children(reset);
421
422 for (ix, block) in doc.blocks.iter().enumerate() {
423 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
424 None => 0.0,
425 Some(previous) if tight(previous, block) => LIST_GAP,
426 Some(_) => BLOCK_GAP,
427 };
428 let overlay = Overlay {
429 block: ix,
430 part: Part::Body,
431 selection,
432 layouts,
433 placeholder: placeholder.as_ref(),
434 };
435 let frame = layouts.map(|layouts| {
438 let layouts = layouts.clone();
439 canvas(
440 move |bounds, _, _| layouts.record_block(ix, bounds),
441 |_, _, _, _| (),
442 )
443 .absolute()
444 .size_full()
445 });
446 column = column.child(
447 div()
448 .mt(px(gap))
449 .pl(px(block.indent as f32 * INDENT_WIDTH))
450 .relative()
451 .children(frame)
452 .when(overlay.covers_block() && block.opaque(), |el| {
456 el.rounded(px(4.0)).bg(theme.selection)
457 })
458 .child(block_element(block, overlay, &theme, window, cx)),
459 );
460 }
461
462 column.into_any_element()
463}
464
465fn tight(previous: &Block, next: &Block) -> bool {
467 let marker = |block: &Block| {
468 matches!(
469 block.kind,
470 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
471 )
472 };
473 marker(previous) && (marker(next) || next.indent > previous.indent)
474}
475
476fn block_element(
477 block: &Block,
478 overlay: Overlay,
479 theme: &Theme,
480 window: &mut Window,
481 cx: &mut App,
482) -> AnyElement {
483 let body = overlay.at(Part::Body);
484 match &block.kind {
485 BlockKind::Paragraph(text) => text_element(
486 text,
487 TEXT_SIZE,
488 LINE_HEIGHT,
489 FontWeight::NORMAL,
490 body,
491 theme,
492 ),
493 BlockKind::Heading { level, text } => {
494 let (size, line) = heading_metrics(*level);
495 text_element(text, size, line, FontWeight::SEMIBOLD, body, theme)
496 }
497 BlockKind::Bullet(text) => marker_row(disc(theme), text, body, theme),
498 BlockKind::Ordered { number, text } => marker_row(
499 div()
500 .flex_none()
501 .w(px(MARKER_WIDTH))
502 .text_size(px(TEXT_SIZE))
503 .line_height(px(LINE_HEIGHT))
504 .text_color(theme.text_muted)
505 .child(SharedString::from(format!("{number}.")))
506 .into_any_element(),
507 text,
508 body,
509 theme,
510 ),
511 BlockKind::Task { checked, text } => {
512 marker_row(checkbox(*checked, theme), text, body, theme)
513 }
514 BlockKind::Quote(text) => div()
515 .border_l_2()
516 .border_color(theme.border_strong)
517 .pl(px(12.0))
518 .pr(px(10.0))
519 .py(px(2.0))
520 .text_color(theme.text_muted)
521 .child(text_element(
522 text,
523 TEXT_SIZE,
524 LINE_HEIGHT,
525 FontWeight::NORMAL,
526 body,
527 theme,
528 ))
529 .into_any_element(),
530 BlockKind::Code { language, code } => code_block(
531 language.as_deref(),
532 &code.text,
533 overlay.at(Part::Code),
534 theme,
535 window,
536 cx,
537 ),
538 BlockKind::Image { url, alt } => image(url, alt, overlay, theme),
539 BlockKind::Bookmark { url, form } => bookmark(overlay.block, url, *form, theme, cx),
540 BlockKind::Table {
541 align,
542 header,
543 rows,
544 } => table(align, header, rows, overlay, theme, window),
545 BlockKind::Rule => div()
546 .h(px(1.0))
547 .w_full()
548 .bg(theme.border)
549 .into_any_element(),
550 }
551}
552
553fn heading_metrics(level: u8) -> (f32, f32) {
555 match level {
556 1 => (19.0, 27.0),
557 2 => (16.0, 24.0),
558 3 => (15.0, 22.0),
559 _ => (14.0, 22.0),
560 }
561}
562
563fn disc(theme: &Theme) -> AnyElement {
565 div()
566 .flex_none()
567 .w(px(MARKER_WIDTH))
568 .h(px(LINE_HEIGHT))
569 .flex()
570 .items_center()
571 .child(
572 div()
573 .ml(px(1.0))
574 .w(px(5.0))
575 .h(px(5.0))
576 .rounded_full()
577 .bg(theme.text_faint),
578 )
579 .into_any_element()
580}
581
582fn checkbox(checked: bool, theme: &Theme) -> AnyElement {
583 let mut box_ = div()
584 .w(px(13.0))
585 .h(px(13.0))
586 .rounded(px(3.5))
587 .border_1()
588 .flex()
589 .items_center()
590 .justify_center();
591 box_ = if checked {
592 box_.bg(theme.solid)
593 .border_color(theme.solid)
594 .text_size(px(9.0))
595 .text_color(theme.on_solid)
596 .child("✓")
597 } else {
598 box_.border_color(theme.border_strong)
599 };
600
601 div()
602 .flex_none()
603 .w(px(MARKER_WIDTH))
604 .h(px(LINE_HEIGHT))
605 .flex()
606 .items_center()
607 .child(box_)
608 .into_any_element()
609}
610
611fn marker_row(marker: AnyElement, text: &Text, overlay: Overlay, theme: &Theme) -> AnyElement {
612 div()
613 .flex()
614 .flex_row()
615 .gap(px(MARKER_GAP))
616 .child(marker)
617 .child(div().flex_1().min_w_0().child(text_element(
618 text,
619 TEXT_SIZE,
620 LINE_HEIGHT,
621 FontWeight::NORMAL,
622 overlay,
623 theme,
624 )))
625 .into_any_element()
626}
627
628pub struct Flat {
631 pub text: SharedString,
632 pub runs: Vec<TextRun>,
633 pub links: Vec<(Range<usize>, String)>,
634 pub code: Vec<Range<usize>>,
635 pub chips: Vec<Range<usize>>,
636}
637
638pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
641 let mut cuts: Vec<usize> = text
642 .marks
643 .iter()
644 .flat_map(|span| [span.range.start, span.range.end])
645 .chain([0, text.text.len()])
646 .filter(|cut| *cut <= text.text.len())
647 .collect();
648 cuts.sort_unstable();
649 cuts.dedup();
650
651 let mut runs = Vec::new();
652 let mut links: Vec<(Range<usize>, String)> = Vec::new();
653 let mut code: Vec<Range<usize>> = Vec::new();
654 let mut chips: Vec<Range<usize>> = Vec::new();
655
656 for pair in cuts.windows(2) {
657 let (start, end) = (pair[0], pair[1]);
658 let covering = text
659 .marks
660 .iter()
661 .filter(|span| span.range.start <= start && span.range.end >= end);
662
663 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
664 let mut chip = false;
665 let mut link = None;
666 for span in covering {
667 match &span.mark {
668 Mark::Bold => bold = true,
669 Mark::Italic => italic = true,
670 Mark::Strike => strike = true,
671 Mark::Code => mono = true,
672 Mark::Mention { url, .. } => {
673 chip = true;
674 link = Some(url.clone());
675 }
676 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
677 }
678 }
679
680 if mono {
681 match code.last_mut() {
682 Some(range) if range.end == start => range.end = end,
683 _ => code.push(start..end),
684 }
685 }
686 if chip {
687 match chips.last_mut() {
688 Some(range) if range.end == start => range.end = end,
689 _ => chips.push(start..end),
690 }
691 }
692 if let Some(url) = &link {
693 match links.last_mut() {
694 Some((range, last)) if range.end == start && last == url => range.end = end,
695 _ => links.push((start..end, url.clone())),
696 }
697 }
698
699 let mut face = font(if mono {
700 theme.font_mono.clone()
701 } else if italic {
702 theme.font_sans_fallback.clone()
705 } else {
706 theme.font_sans.clone()
707 });
708 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
709 FontWeight::SEMIBOLD
710 } else {
711 base_weight
712 };
713 face.style = if italic {
714 FontStyle::Italic
715 } else {
716 FontStyle::Normal
717 };
718
719 runs.push(TextRun {
720 len: end - start,
721 font: face,
722 color: if mono { theme.code_text } else { theme.text },
726 background_color: None,
727 underline: (link.is_some() && !chip).then_some(UnderlineStyle {
728 color: Some(theme.text_muted),
729 thickness: px(1.0),
730 wavy: false,
731 }),
732 strikethrough: strike.then_some(StrikethroughStyle {
733 thickness: px(1.0),
734 color: Some(theme.text_muted),
735 }),
736 });
737 }
738
739 Flat {
740 text: text.text.clone().into(),
741 runs,
742 links,
743 code,
744 chips,
745 }
746}
747
748fn text_element(
749 text: &Text,
750 size: f32,
751 line_height: f32,
752 weight: FontWeight,
753 overlay: Overlay,
754 theme: &Theme,
755) -> AnyElement {
756 let flat = flatten(text, weight, theme);
757 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
758}
759
760fn painted_text(
766 flat: Flat,
767 len: usize,
768 size: f32,
769 line_height: f32,
770 overlay: Overlay,
771 theme: &Theme,
772) -> AnyElement {
773 let (ix, part) = (overlay.block, overlay.part);
774 let (caret, selected) = (overlay.caret(), overlay.selected(len));
775 let span = 0..len;
776 let hint = overlay
779 .placeholder
780 .filter(|_| len == 0 && caret.is_some())
781 .map(|hint| {
782 div()
783 .absolute()
784 .text_color(theme.text_faint)
785 .child(hint.clone())
786 });
787 let styled = StyledText::new(flat.text).with_runs(flat.runs);
788 let layout = styled.layout().clone();
789
790 let painted: AnyElement = if flat.links.is_empty() {
791 styled.into_any_element()
792 } else {
793 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
794 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
795 .on_click(ranges, move |clicked, _window, cx| {
796 if let Some(url) = urls.get(clicked) {
797 cx.open_url(url);
798 }
799 })
800 .into_any_element()
801 };
802
803 let wash = theme.code_wash;
807 let code_ranges = flat.code;
808 let chip_wash = theme.element_hover;
809 let chip_edge = theme.border;
810 let chip_ranges = flat.chips;
811 let caret_color = theme.caret;
812 let selection_color = theme.selection;
813 let layouts = overlay.layouts.cloned();
814 let underlay = canvas(
815 |_, _, _| (),
816 move |_, _, window, _| {
817 if let Some(layouts) = &layouts {
818 layouts.record(ix, part, span.clone(), layout.clone());
819 }
820 if let Some(range) = &selected {
824 for rect in range_rects(&layout, range, 0.0, 0.0) {
825 window.paint_quad(quad(
826 rect,
827 px(2.0),
828 selection_color,
829 px(0.0),
830 gpui::transparent_black(),
831 BorderStyle::default(),
832 ));
833 }
834 }
835 if let Some(offset) = caret
836 && let Some(head) = layout.position_for_index(offset)
837 {
838 window.paint_quad(quad(
839 Bounds::new(head, gpui::size(px(1.5), layout.line_height())),
840 px(0.0),
841 caret_color,
842 px(0.0),
843 gpui::transparent_black(),
844 BorderStyle::default(),
845 ));
846 }
847 for range in &code_ranges {
848 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
849 window.paint_quad(quad(
850 rect,
851 px(INLINE_CODE_RADIUS),
852 wash,
853 px(0.0),
854 gpui::transparent_black(),
855 BorderStyle::default(),
856 ));
857 }
858 }
859 for range in &chip_ranges {
862 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
863 window.paint_quad(quad(
864 rect,
865 px(CHIP_RADIUS),
866 chip_wash,
867 px(1.0),
868 chip_edge,
869 BorderStyle::Solid,
870 ));
871 }
872 }
873 },
874 )
875 .absolute()
876 .size_full();
877
878 div()
879 .text_size(px(size))
880 .line_height(px(line_height))
881 .relative()
882 .child(underlay)
883 .children(hint)
884 .child(painted)
885 .into_any_element()
886}
887
888fn range_rects(
890 layout: &gpui::TextLayout,
891 range: &Range<usize>,
892 pad_x: f32,
893 inset_y: f32,
894) -> Vec<Bounds<Pixels>> {
895 let mut rects = Vec::new();
896 let line_height = layout.line_height();
897 let mut cursor = range.start;
898 let mut guard = 0;
901 while cursor < range.end && guard < 256 {
902 guard += 1;
903 let Some(head) = layout.position_for_index(cursor) else {
904 break;
905 };
906 let (row_end, next) = match layout.position_for_index(range.end) {
907 Some(tail) if tail.y == head.y => (range.end, range.end),
908 _ => {
909 let (mut low, mut high) = (cursor, range.end);
910 while high - low > 1 {
911 let mid = low + (high - low) / 2;
912 match layout.position_for_index(mid) {
913 Some(probe) if probe.y == head.y => low = mid,
914 _ => high = mid,
915 }
916 }
917 (low, high)
918 }
919 };
920 if let Some(tail) = layout.position_for_index(row_end)
921 && tail.x > head.x
922 {
923 rects.push(Bounds::new(
924 point(head.x - px(pad_x), head.y + px(inset_y)),
925 size(
926 tail.x - head.x + px(2.0 * pad_x),
927 line_height - px(2.0 * inset_y),
928 ),
929 ));
930 }
931 cursor = next.max(cursor + 1);
932 }
933 rects
934}
935
936fn code_block(
937 language: Option<&str>,
938 code: &str,
939 overlay: Overlay,
940 theme: &Theme,
941 window: &mut Window,
942 cx: &mut App,
943) -> AnyElement {
944 let ix = overlay.block;
945 let spans = crate::highlight::spans(cx, language, code);
949 let mono = font(theme.font_mono.clone());
950 let run = |len: usize, color: Hsla| TextRun {
951 len,
952 font: mono.clone(),
953 color,
954 background_color: None,
955 underline: None,
956 strikethrough: None,
957 };
958 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
961 let mut offset = 0usize;
962 let lines: Vec<AnyElement> = code
963 .split('\n')
964 .map(|line| {
965 let start = offset;
966 offset += line.len() + 1;
967 let mut runs = Vec::new();
968 let mut pos = 0usize;
971 if let Some(spans) = &spans {
972 let end = start + line.len();
973 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
974 let s = range.start.clamp(start, end) - start;
975 let e = range.end.min(end) - start;
976 if s > pos {
977 runs.push(run(s - pos, theme.text));
978 }
979 runs.push(run(e - s, theme.syntax.color(*kind)));
980 pos = e;
981 }
982 }
983 if pos < line.len() {
984 runs.push(run(line.len() - pos, theme.text));
985 }
986 if runs.is_empty() {
987 runs.push(run(0, theme.text));
988 }
989 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
990 rows.push((start..start + line.len(), styled.layout().clone()));
991 styled.into_any_element()
992 })
993 .collect();
994
995 let caret = overlay.caret();
996 let selected = overlay.selected(code.len());
997 let sink = overlay.layouts.cloned();
998 let (caret_color, selection_color) = (theme.caret, theme.selection);
999 let underlay = canvas(
1000 |_, _, _| (),
1001 move |_, _, window, _| {
1002 for (span, layout) in &rows {
1003 if let Some(sink) = &sink {
1004 sink.record(ix, Part::Code, span.clone(), layout.clone());
1005 }
1006 if let Some(range) = &selected {
1007 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1008 if from < to {
1009 for rect in
1010 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1011 {
1012 window.paint_quad(quad(
1013 rect,
1014 px(2.0),
1015 selection_color,
1016 px(0.0),
1017 gpui::transparent_black(),
1018 BorderStyle::default(),
1019 ));
1020 }
1021 }
1022 }
1023 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1024 && let Some(head) = layout.position_for_index(offset - span.start)
1025 {
1026 window.paint_quad(quad(
1027 Bounds::new(head, size(px(1.5), layout.line_height())),
1028 px(0.0),
1029 caret_color,
1030 px(0.0),
1031 gpui::transparent_black(),
1032 BorderStyle::default(),
1033 ));
1034 }
1035 }
1036 },
1037 )
1038 .absolute()
1039 .size_full();
1040
1041 div()
1042 .rounded(px(10.0))
1043 .bg(theme.ink(0.035))
1044 .border_1()
1045 .border_color(theme.border)
1046 .overflow_hidden()
1047 .relative()
1048 .child(
1052 div()
1053 .relative()
1054 .flex()
1055 .flex_row()
1056 .items_center()
1057 .px(px(CODE_PADDING_X))
1058 .py(px(5.0))
1059 .border_b_1()
1060 .border_color(theme.border)
1061 .bg(theme.ink(0.02))
1062 .text_size(px(11.0))
1063 .text_color(match language {
1064 Some(_) => theme.text_muted,
1065 None => theme.text_faint,
1066 })
1067 .child(
1071 div()
1072 .relative()
1073 .children(overlay.layouts.map(|layouts| {
1074 let layouts = layouts.clone();
1075 canvas(
1076 move |bounds, _, _| layouts.record_language(ix, bounds),
1077 |_, _, _, _| (),
1078 )
1079 .absolute()
1080 .size_full()
1081 }))
1082 .child(SharedString::from(
1083 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1084 )),
1085 ),
1086 )
1087 .child(
1088 div()
1089 .id(ElementId::named_usize("md-code", ix))
1090 .overflow_x_scroll()
1091 .relative()
1092 .px(px(CODE_PADDING_X))
1093 .py(px(CODE_PADDING_Y))
1094 .text_size(px(CODE_TEXT_SIZE))
1095 .line_height(px(CODE_LINE_HEIGHT))
1096 .whitespace_nowrap()
1097 .child(underlay)
1098 .children(lines),
1099 )
1100 .child(copy_button(code, ix, theme, window, cx))
1101 .into_any_element()
1102}
1103
1104fn copy_button(
1111 code: &str,
1112 ix: usize,
1113 theme: &Theme,
1114 window: &mut Window,
1115 cx: &mut App,
1116) -> AnyElement {
1117 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1118 let showing = *copied.read(cx);
1119 let text: SharedString = code.to_string().into();
1120
1121 div()
1122 .id(ElementId::named_usize("md-copy", ix))
1123 .absolute()
1124 .top(px(3.0))
1125 .right(px(5.0))
1126 .h(px(20.0))
1127 .px(px(6.0))
1128 .rounded(px(5.0))
1129 .flex()
1130 .items_center()
1131 .cursor_pointer()
1132 .text_size(px(10.5))
1133 .text_color(theme.text_muted)
1134 .hover(|el| el.bg(theme.ink(0.08)))
1135 .child(if showing { "Copied" } else { "Copy" })
1136 .on_click({
1137 let copied = copied.clone();
1138 move |_, _, cx| {
1139 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1140 copied.update(cx, |state, cx| {
1141 *state = true;
1142 cx.notify();
1143 });
1144 }
1145 })
1146 .on_hover(move |hovering, _, cx| {
1147 if !*hovering && *copied.read(cx) {
1148 copied.update(cx, |state, cx| {
1149 *state = false;
1150 cx.notify();
1151 });
1152 }
1153 })
1154 .into_any_element()
1155}
1156
1157fn image(url: &str, alt: &Text, overlay: Overlay, theme: &Theme) -> AnyElement {
1164 let hint = SharedString::new_static(CAPTION_HINT);
1165 let overlay = Overlay {
1166 placeholder: Some(&hint),
1167 ..overlay.at(Part::Caption)
1168 };
1169 let picture = if url.is_empty() {
1170 div()
1171 .h(px(IMAGE_EMPTY_HEIGHT))
1172 .flex()
1173 .items_center()
1174 .px(px(CARD_PADDING))
1175 .rounded(px(IMAGE_RADIUS))
1176 .border_1()
1177 .border_dashed()
1178 .border_color(theme.border)
1179 .text_size(px(TEXT_SIZE))
1180 .text_color(theme.text_muted)
1181 .child(IMAGE_EMPTY)
1182 } else {
1183 div()
1184 .rounded(px(IMAGE_RADIUS))
1185 .overflow_hidden()
1186 .border_1()
1187 .border_color(theme.border)
1188 .child(match url.contains("://") {
1192 true => img(SharedString::from(url.to_string())).max_w_full(),
1193 false => img(std::path::PathBuf::from(url)).max_w_full(),
1194 })
1195 };
1196 div()
1197 .flex()
1198 .flex_col()
1199 .gap(px(CAPTION_GAP))
1200 .child(picture)
1201 .when(!alt.is_empty() || overlay.caret().is_some(), |el| {
1202 el.child(text_element(
1203 alt,
1204 CAPTION_TEXT_SIZE,
1205 CAPTION_LINE_HEIGHT,
1206 FontWeight::NORMAL,
1207 overlay,
1208 theme,
1209 ))
1210 })
1211 .into_any_element()
1212}
1213
1214fn bookmark(ix: usize, url: &str, form: Form, theme: &Theme, cx: &App) -> AnyElement {
1225 let preview = preview::of(cx, url).unwrap_or_default();
1226 let host = SharedString::from(preview::host(url).to_string());
1227 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1228 let title = preview
1229 .title
1230 .clone()
1231 .unwrap_or_else(|| SharedString::from(url.to_string()));
1232
1233 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1236 let site = host.clone();
1237 let mark = move |size: f32| {
1238 let host = site.clone();
1239 match icon.clone() {
1240 Some(icon) => img(icon)
1241 .size(px(size))
1242 .rounded(px(size / 4.0))
1243 .with_fallback(move || initial(&host, size, muted, wash))
1244 .into_any_element(),
1245 None => initial(&host, size, muted, wash),
1246 }
1247 };
1248
1249 if form == Form::Chip {
1250 let open = url.to_string();
1251 let pill = div()
1252 .id(ElementId::named_usize("md-chip", ix))
1253 .flex()
1254 .flex_row()
1255 .items_center()
1256 .gap(px(6.0))
1257 .px(px(CHIP_BLOCK_PAD_X))
1258 .py(px(CHIP_BLOCK_PAD_Y))
1259 .rounded(px(CHIP_RADIUS))
1260 .border_1()
1261 .border_color(theme.border)
1262 .bg(theme.element_hover)
1263 .text_size(px(TEXT_SIZE))
1264 .line_height(px(LINE_HEIGHT))
1265 .text_color(theme.text)
1266 .cursor(CursorStyle::PointingHand)
1267 .hover(|el| el.bg(theme.element_active))
1268 .on_click(move |_, _, cx| cx.open_url(&open))
1269 .child(mark(CHIP_ICON))
1270 .child(
1273 div()
1274 .min_w_0()
1275 .truncate()
1276 .child(preview.title.unwrap_or(label)),
1277 );
1278 return div().flex().flex_row().child(pill).into_any_element();
1281 }
1282
1283 let words = div()
1284 .flex()
1285 .flex_col()
1286 .min_w_0()
1287 .h(px(CARD_HEIGHT))
1288 .px(px(CARD_PADDING))
1289 .py(px(CARD_PADDING - 2.0))
1290 .child(
1291 div()
1292 .truncate()
1293 .text_size(px(TEXT_SIZE))
1294 .line_height(px(LINE_HEIGHT))
1295 .text_color(theme.text)
1296 .child(title),
1297 )
1298 .children(preview.description.map(|blurb| {
1299 div()
1300 .line_clamp(2)
1301 .text_size(px(CARD_TEXT_SIZE))
1302 .line_height(px(CARD_LINE_HEIGHT))
1303 .text_color(theme.text_muted)
1304 .child(blurb)
1305 }))
1306 .child(
1307 div()
1308 .mt_auto()
1309 .pt(px(6.0))
1310 .flex()
1311 .items_center()
1312 .gap(px(6.0))
1313 .text_size(px(CARD_TEXT_SIZE))
1314 .text_color(theme.text_muted)
1315 .child(mark(CARD_ICON))
1316 .child(div().truncate().child(label)),
1317 );
1318
1319 let picture = div()
1320 .bg(theme.surface)
1321 .flex()
1322 .items_center()
1323 .justify_center()
1324 .overflow_hidden()
1325 .child(match preview.image {
1326 Some(image) => img(image)
1327 .size_full()
1328 .object_fit(ObjectFit::Cover)
1329 .with_fallback(move || mark(CARD_COVER))
1330 .into_any_element(),
1331 None => mark(CARD_COVER),
1332 });
1333
1334 let open = url.to_string();
1335 let card = div()
1336 .id(ElementId::named_usize("md-bookmark", ix))
1337 .flex()
1338 .w_full()
1339 .overflow_hidden()
1340 .rounded(px(8.0))
1341 .border_1()
1342 .border_color(theme.border)
1343 .bg(theme.surface_card)
1344 .cursor(CursorStyle::PointingHand)
1345 .hover(|el| el.bg(theme.element_hover))
1346 .on_click(move |_, _, cx| cx.open_url(&open));
1347
1348 if form == Form::Embed {
1349 card.flex_col()
1350 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1351 .child(words.w_full())
1352 } else {
1353 card.h(px(CARD_HEIGHT))
1354 .child(words.flex_1())
1355 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1356 }
1357 .into_any_element()
1358}
1359
1360fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1363 div()
1364 .flex_none()
1365 .size(px(size))
1366 .rounded(px(size / 4.0))
1367 .bg(wash)
1368 .flex()
1369 .items_center()
1370 .justify_center()
1371 .text_size(px(size * 0.55))
1372 .text_color(color)
1373 .child(SharedString::from(
1374 host.chars()
1375 .next()
1376 .unwrap_or('?')
1377 .to_uppercase()
1378 .to_string(),
1379 ))
1380 .into_any_element()
1381}
1382
1383fn table(
1390 align: &[Align],
1391 header: &[Text],
1392 rows: &[Vec<Text>],
1393 overlay: Overlay,
1394 theme: &Theme,
1395 window: &mut Window,
1396) -> AnyElement {
1397 let ix = overlay.block;
1398 let all: Vec<&[Text]> = std::iter::once(header)
1399 .filter(|row| !row.is_empty())
1400 .chain(rows.iter().map(|row| row.as_slice()))
1401 .collect();
1402 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1403 if columns == 0 {
1404 return gpui::Empty.into_any_element();
1405 }
1406 let has_header = !header.is_empty();
1407
1408 let text_system = window.text_system();
1409 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1410 let mut content = vec![0.0f32; columns];
1411 for (r, row) in all.iter().enumerate() {
1412 let weight = if has_header && r == 0 {
1413 FontWeight::BOLD
1414 } else {
1415 FontWeight::NORMAL
1416 };
1417 let mut out = Vec::with_capacity(columns);
1418 for (c, natural) in content.iter_mut().enumerate() {
1419 let Some(cell) = row.get(c) else {
1420 out.push(None);
1421 continue;
1422 };
1423 let flat = flatten(cell, weight, theme);
1424 if !flat.text.is_empty() {
1425 let width = f32::from(
1426 text_system
1427 .shape_line(flat.text.clone(), px(TEXT_SIZE), &flat.runs, None)
1428 .width(),
1429 );
1430 *natural = natural.max(width);
1431 }
1432 out.push(Some(flat));
1433 }
1434 flats.push(out);
1435 }
1436
1437 let naturals: Vec<f32> = content
1438 .iter()
1439 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
1440 .collect();
1441 let minimums: Vec<f32> = naturals
1442 .iter()
1443 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
1444 .collect();
1445 let hairline = theme.hairline(0.10);
1446
1447 let mut inner = div()
1448 .flex()
1449 .flex_col()
1450 .w_full()
1451 .min_w(px(minimums.iter().sum::<f32>()));
1452 for (r, row) in flats.into_iter().enumerate() {
1453 if r > 0 {
1454 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
1455 }
1456 let mut row_el = div().flex().flex_row();
1457 for (c, cell) in row.into_iter().enumerate() {
1458 let mut cell_el = div()
1459 .flex_grow(naturals[c])
1460 .flex_shrink(naturals[c])
1461 .flex_basis(px(0.0))
1462 .min_w(px(minimums[c]))
1463 .p(px(TABLE_CELL_PADDING))
1464 .text_size(px(TEXT_SIZE))
1465 .line_height(px(LINE_HEIGHT));
1466 cell_el = match align.get(c).copied().unwrap_or_default() {
1467 Align::Left => cell_el,
1468 Align::Center => cell_el.text_center(),
1469 Align::Right => cell_el.text_right(),
1470 };
1471 if let Some(flat) = cell {
1472 let row = if has_header { r } else { r + 1 };
1476 let len = flat.text.len();
1477 cell_el = cell_el.child(painted_text(
1478 flat,
1479 len,
1480 TEXT_SIZE,
1481 LINE_HEIGHT,
1482 overlay.at(Part::Cell { row, column: c }),
1483 theme,
1484 ));
1485 }
1486 row_el = row_el.child(cell_el);
1487 }
1488 inner = inner.child(row_el);
1489 }
1490
1491 div()
1492 .id(ElementId::named_usize("md-table", ix))
1493 .w_full()
1494 .overflow_x_scroll()
1495 .child(inner)
1496 .into_any_element()
1497}