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, Copy, PartialEq, Eq, Debug, Default)]
102pub enum Caption {
103 #[default]
105 Shown,
106 Hidden,
108}
109
110#[derive(Clone, Default)]
117pub struct BlockLayouts(Rc<RefCell<Frames>>);
118
119#[derive(Default)]
120struct Frames {
121 texts: Vec<Painted>,
122 blocks: Vec<(usize, Bounds<Pixels>)>,
125 languages: Vec<(usize, Bounds<Pixels>)>,
128 pictures: Vec<(usize, Bounds<Pixels>)>,
132}
133
134struct Painted {
141 block: usize,
142 part: Part,
143 range: Range<usize>,
144 layout: TextLayout,
145}
146
147impl BlockLayouts {
148 pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
154 let entries = &self.0.borrow().texts;
155 let cursor = |painted: &Painted| {
156 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
157 Cursor::new(
158 painted.block,
159 painted.part,
160 painted.range.start + offset.min(painted.range.len()),
161 )
162 };
163 if let Some(painted) = entries
164 .iter()
165 .find(|painted| painted.layout.bounds().contains(&point))
166 {
167 return Some(cursor(painted));
168 }
169 entries
170 .iter()
171 .min_by_key(|painted| {
172 let bounds = painted.layout.bounds();
173 let above = (bounds.origin.y - point.y).abs();
174 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
175 f32::from(above.min(below)) as i64
176 })
177 .map(cursor)
178 }
179
180 pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
186 let entries = &self.0.borrow().texts;
187 let painted = entries.iter().find(|painted| {
188 painted.block == at.block
189 && painted.part == at.part
190 && painted.range.start <= at.offset
191 && at.offset <= painted.range.end
192 })?;
193 let point = painted
194 .layout
195 .position_for_index(at.offset - painted.range.start)?;
196 Some((point, painted.layout.line_height()))
197 }
198
199 pub fn step_row(
210 &self,
211 at: Cursor,
212 from: Point<Pixels>,
213 down: bool,
214 ) -> Option<(Cursor, Pixels)> {
215 let entries = &self.0.borrow().texts;
216 let ix = entries.iter().position(|painted| {
217 painted.block == at.block
218 && painted.part == at.part
219 && painted.range.start <= at.offset
220 && at.offset <= painted.range.end
221 })?;
222 let here = &entries[ix];
223 let line = here.layout.line_height();
224 let index_at = |painted: &Painted, y: Pixels| {
225 let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
226 (
227 Cursor::new(
228 painted.block,
229 painted.part,
230 painted.range.start + offset.min(painted.range.len()),
231 ),
232 y,
233 )
234 };
235
236 let bounds = here.layout.bounds();
239 let target = if down { from.y + line } else { from.y - line };
240 if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
241 return Some(index_at(here, target));
242 }
243
244 let next = match down {
245 true => entries.get(ix + 1)?,
246 false => entries.get(ix.checked_sub(1)?)?,
247 };
248 let bounds = next.layout.bounds();
250 let row = match down {
251 true => bounds.origin.y,
252 false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
253 };
254 Some(index_at(next, row))
255 }
256
257 pub fn over_text(&self, point: Point<Pixels>) -> bool {
264 self.0
265 .borrow()
266 .texts
267 .iter()
268 .any(|painted| painted.layout.bounds().contains(&point))
269 }
270
271 pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
273 let blocks = &self.0.borrow().blocks;
274 blocks
275 .iter()
276 .find(|(_, bounds)| bounds.contains(&point))
277 .or_else(|| {
278 blocks.iter().min_by_key(|(_, bounds)| {
279 let above = (bounds.origin.y - point.y).abs();
280 let below = (bounds.origin.y + bounds.size.height - point.y).abs();
281 f32::from(above.min(below)) as i64
282 })
283 })
284 .map(|(ix, _)| *ix)
285 }
286
287 pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
289 self.0
290 .borrow()
291 .blocks
292 .iter()
293 .find(|(block, _)| *block == ix)
294 .map(|(_, bounds)| *bounds)
295 }
296
297 pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
302 self.0
303 .borrow()
304 .languages
305 .iter()
306 .find(|(block, _)| *block == ix)
307 .map(|(_, bounds)| *bounds)
308 }
309
310 pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
315 self.0
316 .borrow()
317 .pictures
318 .iter()
319 .find(|(block, _)| *block == ix)
320 .map(|(_, bounds)| *bounds)
321 }
322
323 fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
324 self.0.borrow_mut().texts.push(Painted {
325 block,
326 part,
327 range,
328 layout,
329 });
330 }
331
332 fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
333 self.0.borrow_mut().blocks.push((ix, bounds));
334 }
335
336 fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
337 self.0.borrow_mut().languages.push((ix, bounds));
338 }
339
340 fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
341 self.0.borrow_mut().pictures.push((ix, bounds));
342 }
343
344 fn clear(&self) {
345 let mut frames = self.0.borrow_mut();
346 frames.texts.clear();
347 frames.blocks.clear();
348 frames.languages.clear();
349 frames.pictures.clear();
350 }
351}
352
353#[derive(Clone, Copy)]
359struct Overlay<'a> {
360 block: usize,
361 part: Part,
362 selection: Option<Selection>,
363 layouts: Option<&'a BlockLayouts>,
364 placeholder: Option<&'a SharedString>,
367 caption: Caption,
368}
369
370impl<'a> Overlay<'a> {
371 fn at(self, part: Part) -> Self {
372 Self { part, ..self }
373 }
374
375 fn here(&self) -> Cursor {
376 Cursor::new(self.block, self.part, 0)
377 }
378
379 fn caret(&self) -> Option<usize> {
381 self.selection
382 .map(|selection| selection.head)
383 .filter(|head| head.block == self.block && head.part == self.part)
384 .map(|head| head.offset)
385 }
386
387 fn selected(&self, len: usize) -> Option<Range<usize>> {
393 let selection = self.selection?;
394 if selection.is_collapsed() {
395 return None;
396 }
397 let (start, end) = selection.ordered();
398 let here = self.here();
399 let (first, last) = (
400 Cursor::new(start.block, start.part, 0),
401 Cursor::new(end.block, end.part, 0),
402 );
403 if here < first || here > last {
404 return None;
405 }
406 let from = if here == first { start.offset } else { 0 };
407 let to = if here == last { end.offset } else { len };
408 (from < to).then_some(from..to.min(len))
409 }
410
411 fn covers_block(&self) -> bool {
415 let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
416 return false;
417 };
418 let (start, end) = selection.ordered();
419 start.block < self.block && self.block < end.block
420 }
421}
422
423pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
425 render(&crate::parse(source), Caption::default(), window, cx)
426}
427
428pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
430 render_with_selection(doc, None, None, None, caption, window, cx)
431}
432
433pub fn render_with_selection(
441 doc: &Doc,
442 selection: Option<Selection>,
443 layouts: Option<&BlockLayouts>,
444 placeholder: Option<SharedString>,
445 caption: Caption,
446 window: &mut Window,
447 cx: &mut App,
448) -> AnyElement {
449 let reset = layouts.map(|layouts| {
455 let layouts = layouts.clone();
456 canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
457 .absolute()
458 .size(px(0.0))
459 });
460 let theme = Theme::of(cx).clone();
463 let mut column = div().flex().flex_col().children(reset);
464
465 for (ix, block) in doc.blocks.iter().enumerate() {
466 let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
467 None => 0.0,
468 Some(previous) if tight(previous, block) => LIST_GAP,
469 Some(_) => BLOCK_GAP,
470 };
471 let overlay = Overlay {
472 block: ix,
473 part: Part::Body,
474 selection,
475 layouts,
476 placeholder: placeholder.as_ref(),
477 caption,
478 };
479 let frame = layouts.map(|layouts| {
482 let layouts = layouts.clone();
483 canvas(
484 move |bounds, _, _| layouts.record_block(ix, bounds),
485 |_, _, _, _| (),
486 )
487 .absolute()
488 .size_full()
489 });
490 column = column.child(
491 div()
492 .mt(px(gap))
493 .pl(px(block.indent as f32 * INDENT_WIDTH))
494 .relative()
495 .children(frame)
496 .when(overlay.covers_block() && block.opaque(), |el| {
500 el.rounded(px(4.0)).bg(theme.selection)
501 })
502 .child(block_element(block, overlay, &theme, window, cx)),
503 );
504 }
505
506 column.into_any_element()
507}
508
509fn tight(previous: &Block, next: &Block) -> bool {
511 let marker = |block: &Block| {
512 matches!(
513 block.kind,
514 BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
515 )
516 };
517 marker(previous) && (marker(next) || next.indent > previous.indent)
518}
519
520fn block_element(
521 block: &Block,
522 overlay: Overlay,
523 theme: &Theme,
524 window: &mut Window,
525 cx: &mut App,
526) -> AnyElement {
527 let body = overlay.at(Part::Body);
528 match &block.kind {
529 BlockKind::Paragraph(text) => text_element(
530 text,
531 TEXT_SIZE,
532 LINE_HEIGHT,
533 FontWeight::NORMAL,
534 body,
535 theme,
536 ),
537 BlockKind::Heading { level, text } => {
538 let (size, line) = heading_metrics(*level);
539 text_element(text, size, line, FontWeight::SEMIBOLD, body, theme)
540 }
541 BlockKind::Bullet(text) => marker_row(disc(theme), text, body, theme),
542 BlockKind::Ordered { number, text } => marker_row(
543 div()
544 .flex_none()
545 .w(px(MARKER_WIDTH))
546 .text_size(px(TEXT_SIZE))
547 .line_height(px(LINE_HEIGHT))
548 .text_color(theme.text_muted)
549 .child(SharedString::from(format!("{number}.")))
550 .into_any_element(),
551 text,
552 body,
553 theme,
554 ),
555 BlockKind::Task { checked, text } => {
556 marker_row(checkbox(*checked, theme), text, body, theme)
557 }
558 BlockKind::Quote(text) => div()
559 .border_l_2()
560 .border_color(theme.border_strong)
561 .pl(px(12.0))
562 .pr(px(10.0))
563 .py(px(2.0))
564 .text_color(theme.text_muted)
565 .child(text_element(
566 text,
567 TEXT_SIZE,
568 LINE_HEIGHT,
569 FontWeight::NORMAL,
570 body,
571 theme,
572 ))
573 .into_any_element(),
574 BlockKind::Code { language, code } => code_block(
575 language.as_deref(),
576 &code.text,
577 overlay.at(Part::Code),
578 theme,
579 window,
580 cx,
581 ),
582 BlockKind::Image { url, alt, width } => image(url, alt, *width, overlay, theme),
583 BlockKind::Bookmark { url, form } => bookmark(overlay.block, url, *form, theme, cx),
584 BlockKind::Table {
585 align,
586 header,
587 rows,
588 } => table(align, header, rows, overlay, theme, window),
589 BlockKind::Rule => div()
590 .h(px(1.0))
591 .w_full()
592 .bg(theme.border)
593 .into_any_element(),
594 }
595}
596
597fn heading_metrics(level: u8) -> (f32, f32) {
599 match level {
600 1 => (19.0, 27.0),
601 2 => (16.0, 24.0),
602 3 => (15.0, 22.0),
603 _ => (14.0, 22.0),
604 }
605}
606
607fn disc(theme: &Theme) -> AnyElement {
609 div()
610 .flex_none()
611 .w(px(MARKER_WIDTH))
612 .h(px(LINE_HEIGHT))
613 .flex()
614 .items_center()
615 .child(
616 div()
617 .ml(px(1.0))
618 .w(px(5.0))
619 .h(px(5.0))
620 .rounded_full()
621 .bg(theme.text_faint),
622 )
623 .into_any_element()
624}
625
626fn checkbox(checked: bool, theme: &Theme) -> AnyElement {
627 let mut box_ = div()
628 .w(px(13.0))
629 .h(px(13.0))
630 .rounded(px(3.5))
631 .border_1()
632 .flex()
633 .items_center()
634 .justify_center();
635 box_ = if checked {
636 box_.bg(theme.solid)
637 .border_color(theme.solid)
638 .text_size(px(9.0))
639 .text_color(theme.on_solid)
640 .child("✓")
641 } else {
642 box_.border_color(theme.border_strong)
643 };
644
645 div()
646 .flex_none()
647 .w(px(MARKER_WIDTH))
648 .h(px(LINE_HEIGHT))
649 .flex()
650 .items_center()
651 .child(box_)
652 .into_any_element()
653}
654
655fn marker_row(marker: AnyElement, text: &Text, overlay: Overlay, theme: &Theme) -> AnyElement {
656 div()
657 .flex()
658 .flex_row()
659 .gap(px(MARKER_GAP))
660 .child(marker)
661 .child(div().flex_1().min_w_0().child(text_element(
662 text,
663 TEXT_SIZE,
664 LINE_HEIGHT,
665 FontWeight::NORMAL,
666 overlay,
667 theme,
668 )))
669 .into_any_element()
670}
671
672pub struct Flat {
675 pub text: SharedString,
676 pub runs: Vec<TextRun>,
677 pub links: Vec<(Range<usize>, String)>,
678 pub code: Vec<Range<usize>>,
679 pub chips: Vec<Range<usize>>,
680}
681
682pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
685 let mut cuts: Vec<usize> = text
686 .marks
687 .iter()
688 .flat_map(|span| [span.range.start, span.range.end])
689 .chain([0, text.text.len()])
690 .filter(|cut| *cut <= text.text.len())
691 .collect();
692 cuts.sort_unstable();
693 cuts.dedup();
694
695 let mut runs = Vec::new();
696 let mut links: Vec<(Range<usize>, String)> = Vec::new();
697 let mut code: Vec<Range<usize>> = Vec::new();
698 let mut chips: Vec<Range<usize>> = Vec::new();
699
700 for pair in cuts.windows(2) {
701 let (start, end) = (pair[0], pair[1]);
702 let covering = text
703 .marks
704 .iter()
705 .filter(|span| span.range.start <= start && span.range.end >= end);
706
707 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
708 let mut chip = false;
709 let mut link = None;
710 for span in covering {
711 match &span.mark {
712 Mark::Bold => bold = true,
713 Mark::Italic => italic = true,
714 Mark::Strike => strike = true,
715 Mark::Code => mono = true,
716 Mark::Mention { url, .. } => {
717 chip = true;
718 link = Some(url.clone());
719 }
720 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
721 }
722 }
723
724 if mono {
725 match code.last_mut() {
726 Some(range) if range.end == start => range.end = end,
727 _ => code.push(start..end),
728 }
729 }
730 if chip {
731 match chips.last_mut() {
732 Some(range) if range.end == start => range.end = end,
733 _ => chips.push(start..end),
734 }
735 }
736 if let Some(url) = &link {
737 match links.last_mut() {
738 Some((range, last)) if range.end == start && last == url => range.end = end,
739 _ => links.push((start..end, url.clone())),
740 }
741 }
742
743 let mut face = font(if mono {
744 theme.font_mono.clone()
745 } else if italic {
746 theme.font_sans_fallback.clone()
749 } else {
750 theme.font_sans.clone()
751 });
752 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
753 FontWeight::SEMIBOLD
754 } else {
755 base_weight
756 };
757 face.style = if italic {
758 FontStyle::Italic
759 } else {
760 FontStyle::Normal
761 };
762
763 runs.push(TextRun {
764 len: end - start,
765 font: face,
766 color: if mono { theme.code_text } else { theme.text },
770 background_color: None,
771 underline: (link.is_some() && !chip).then_some(UnderlineStyle {
772 color: Some(theme.text_muted),
773 thickness: px(1.0),
774 wavy: false,
775 }),
776 strikethrough: strike.then_some(StrikethroughStyle {
777 thickness: px(1.0),
778 color: Some(theme.text_muted),
779 }),
780 });
781 }
782
783 Flat {
784 text: text.text.clone().into(),
785 runs,
786 links,
787 code,
788 chips,
789 }
790}
791
792fn text_element(
793 text: &Text,
794 size: f32,
795 line_height: f32,
796 weight: FontWeight,
797 overlay: Overlay,
798 theme: &Theme,
799) -> AnyElement {
800 let flat = flatten(text, weight, theme);
801 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
802}
803
804fn painted_text(
810 flat: Flat,
811 len: usize,
812 size: f32,
813 line_height: f32,
814 overlay: Overlay,
815 theme: &Theme,
816) -> AnyElement {
817 let (ix, part) = (overlay.block, overlay.part);
818 let (caret, selected) = (overlay.caret(), overlay.selected(len));
819 let span = 0..len;
820 let hint = overlay
823 .placeholder
824 .filter(|_| len == 0 && caret.is_some())
825 .map(|hint| {
826 div()
827 .absolute()
828 .text_color(theme.text_faint)
829 .child(hint.clone())
830 });
831 let styled = StyledText::new(flat.text).with_runs(flat.runs);
832 let layout = styled.layout().clone();
833
834 let painted: AnyElement = if flat.links.is_empty() {
835 styled.into_any_element()
836 } else {
837 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
838 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
839 .on_click(ranges, move |clicked, _window, cx| {
840 if let Some(url) = urls.get(clicked) {
841 cx.open_url(url);
842 }
843 })
844 .into_any_element()
845 };
846
847 let wash = theme.code_wash;
851 let code_ranges = flat.code;
852 let chip_wash = theme.element_hover;
853 let chip_edge = theme.border;
854 let chip_ranges = flat.chips;
855 let caret_color = theme.caret;
856 let selection_color = theme.selection;
857 let layouts = overlay.layouts.cloned();
858 let underlay = canvas(
859 |_, _, _| (),
860 move |_, _, window, _| {
861 if let Some(layouts) = &layouts {
862 layouts.record(ix, part, span.clone(), layout.clone());
863 }
864 if let Some(range) = &selected {
868 for rect in range_rects(&layout, range, 0.0, 0.0) {
869 window.paint_quad(quad(
870 rect,
871 px(2.0),
872 selection_color,
873 px(0.0),
874 gpui::transparent_black(),
875 BorderStyle::default(),
876 ));
877 }
878 }
879 if let Some(offset) = caret
880 && let Some(head) = layout.position_for_index(offset)
881 {
882 window.paint_quad(quad(
883 Bounds::new(head, gpui::size(px(1.5), layout.line_height())),
884 px(0.0),
885 caret_color,
886 px(0.0),
887 gpui::transparent_black(),
888 BorderStyle::default(),
889 ));
890 }
891 for range in &code_ranges {
892 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
893 window.paint_quad(quad(
894 rect,
895 px(INLINE_CODE_RADIUS),
896 wash,
897 px(0.0),
898 gpui::transparent_black(),
899 BorderStyle::default(),
900 ));
901 }
902 }
903 for range in &chip_ranges {
906 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
907 window.paint_quad(quad(
908 rect,
909 px(CHIP_RADIUS),
910 chip_wash,
911 px(1.0),
912 chip_edge,
913 BorderStyle::Solid,
914 ));
915 }
916 }
917 },
918 )
919 .absolute()
920 .size_full();
921
922 div()
923 .text_size(px(size))
924 .line_height(px(line_height))
925 .relative()
926 .child(underlay)
927 .children(hint)
928 .child(painted)
929 .into_any_element()
930}
931
932fn range_rects(
934 layout: &gpui::TextLayout,
935 range: &Range<usize>,
936 pad_x: f32,
937 inset_y: f32,
938) -> Vec<Bounds<Pixels>> {
939 let mut rects = Vec::new();
940 let line_height = layout.line_height();
941 let mut cursor = range.start;
942 let mut guard = 0;
945 while cursor < range.end && guard < 256 {
946 guard += 1;
947 let Some(head) = layout.position_for_index(cursor) else {
948 break;
949 };
950 let (row_end, next) = match layout.position_for_index(range.end) {
951 Some(tail) if tail.y == head.y => (range.end, range.end),
952 _ => {
953 let (mut low, mut high) = (cursor, range.end);
954 while high - low > 1 {
955 let mid = low + (high - low) / 2;
956 match layout.position_for_index(mid) {
957 Some(probe) if probe.y == head.y => low = mid,
958 _ => high = mid,
959 }
960 }
961 (low, high)
962 }
963 };
964 if let Some(tail) = layout.position_for_index(row_end)
965 && tail.x > head.x
966 {
967 rects.push(Bounds::new(
968 point(head.x - px(pad_x), head.y + px(inset_y)),
969 size(
970 tail.x - head.x + px(2.0 * pad_x),
971 line_height - px(2.0 * inset_y),
972 ),
973 ));
974 }
975 cursor = next.max(cursor + 1);
976 }
977 rects
978}
979
980fn code_block(
981 language: Option<&str>,
982 code: &str,
983 overlay: Overlay,
984 theme: &Theme,
985 window: &mut Window,
986 cx: &mut App,
987) -> AnyElement {
988 let ix = overlay.block;
989 let spans = crate::highlight::spans(cx, language, code);
993 let mono = font(theme.font_mono.clone());
994 let run = |len: usize, color: Hsla| TextRun {
995 len,
996 font: mono.clone(),
997 color,
998 background_color: None,
999 underline: None,
1000 strikethrough: None,
1001 };
1002 let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1005 let mut offset = 0usize;
1006 let lines: Vec<AnyElement> = code
1007 .split('\n')
1008 .map(|line| {
1009 let start = offset;
1010 offset += line.len() + 1;
1011 let mut runs = Vec::new();
1012 let mut pos = 0usize;
1015 if let Some(spans) = &spans {
1016 let end = start + line.len();
1017 for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1018 let s = range.start.clamp(start, end) - start;
1019 let e = range.end.min(end) - start;
1020 if s > pos {
1021 runs.push(run(s - pos, theme.text));
1022 }
1023 runs.push(run(e - s, theme.syntax.color(*kind)));
1024 pos = e;
1025 }
1026 }
1027 if pos < line.len() {
1028 runs.push(run(line.len() - pos, theme.text));
1029 }
1030 if runs.is_empty() {
1031 runs.push(run(0, theme.text));
1032 }
1033 let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1034 rows.push((start..start + line.len(), styled.layout().clone()));
1035 styled.into_any_element()
1036 })
1037 .collect();
1038
1039 let caret = overlay.caret();
1040 let selected = overlay.selected(code.len());
1041 let sink = overlay.layouts.cloned();
1042 let (caret_color, selection_color) = (theme.caret, theme.selection);
1043 let underlay = canvas(
1044 |_, _, _| (),
1045 move |_, _, window, _| {
1046 for (span, layout) in &rows {
1047 if let Some(sink) = &sink {
1048 sink.record(ix, Part::Code, span.clone(), layout.clone());
1049 }
1050 if let Some(range) = &selected {
1051 let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1052 if from < to {
1053 for rect in
1054 range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1055 {
1056 window.paint_quad(quad(
1057 rect,
1058 px(2.0),
1059 selection_color,
1060 px(0.0),
1061 gpui::transparent_black(),
1062 BorderStyle::default(),
1063 ));
1064 }
1065 }
1066 }
1067 if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1068 && let Some(head) = layout.position_for_index(offset - span.start)
1069 {
1070 window.paint_quad(quad(
1071 Bounds::new(head, size(px(1.5), layout.line_height())),
1072 px(0.0),
1073 caret_color,
1074 px(0.0),
1075 gpui::transparent_black(),
1076 BorderStyle::default(),
1077 ));
1078 }
1079 }
1080 },
1081 )
1082 .absolute()
1083 .size_full();
1084
1085 div()
1086 .rounded(px(10.0))
1087 .bg(theme.ink(0.035))
1088 .border_1()
1089 .border_color(theme.border)
1090 .overflow_hidden()
1091 .relative()
1092 .child(
1096 div()
1097 .relative()
1098 .flex()
1099 .flex_row()
1100 .items_center()
1101 .px(px(CODE_PADDING_X))
1102 .py(px(5.0))
1103 .border_b_1()
1104 .border_color(theme.border)
1105 .bg(theme.ink(0.02))
1106 .text_size(px(11.0))
1107 .text_color(match language {
1108 Some(_) => theme.text_muted,
1109 None => theme.text_faint,
1110 })
1111 .child(
1115 div()
1116 .relative()
1117 .children(overlay.layouts.map(|layouts| {
1118 let layouts = layouts.clone();
1119 canvas(
1120 move |bounds, _, _| layouts.record_language(ix, bounds),
1121 |_, _, _, _| (),
1122 )
1123 .absolute()
1124 .size_full()
1125 }))
1126 .child(SharedString::from(
1127 language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1128 )),
1129 ),
1130 )
1131 .child(
1132 div()
1133 .id(ElementId::named_usize("md-code", ix))
1134 .overflow_x_scroll()
1135 .restrict_scroll_to_axis()
1139 .relative()
1140 .px(px(CODE_PADDING_X))
1141 .py(px(CODE_PADDING_Y))
1142 .text_size(px(CODE_TEXT_SIZE))
1143 .line_height(px(CODE_LINE_HEIGHT))
1144 .whitespace_nowrap()
1145 .child(underlay)
1146 .children(lines),
1147 )
1148 .child(copy_button(code, ix, theme, window, cx))
1149 .into_any_element()
1150}
1151
1152fn copy_button(
1159 code: &str,
1160 ix: usize,
1161 theme: &Theme,
1162 window: &mut Window,
1163 cx: &mut App,
1164) -> AnyElement {
1165 let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1166 let showing = *copied.read(cx);
1167 let text: SharedString = code.to_string().into();
1168
1169 div()
1170 .id(ElementId::named_usize("md-copy", ix))
1171 .absolute()
1172 .top(px(3.0))
1173 .right(px(5.0))
1174 .h(px(20.0))
1175 .px(px(6.0))
1176 .rounded(px(5.0))
1177 .flex()
1178 .items_center()
1179 .cursor_pointer()
1180 .text_size(px(10.5))
1181 .text_color(theme.text_muted)
1182 .hover(|el| el.bg(theme.ink(0.08)))
1183 .child(if showing { "Copied" } else { "Copy" })
1184 .on_click({
1185 let copied = copied.clone();
1186 move |_, _, cx| {
1187 cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1188 copied.update(cx, |state, cx| {
1189 *state = true;
1190 cx.notify();
1191 });
1192 }
1193 })
1194 .on_hover(move |hovering, _, cx| {
1195 if !*hovering && *copied.read(cx) {
1196 copied.update(cx, |state, cx| {
1197 *state = false;
1198 cx.notify();
1199 });
1200 }
1201 })
1202 .into_any_element()
1203}
1204
1205fn image(url: &str, alt: &Text, width: Option<u32>, overlay: Overlay, theme: &Theme) -> AnyElement {
1212 let hint = SharedString::new_static(CAPTION_HINT);
1213 let overlay = Overlay {
1214 placeholder: Some(&hint),
1215 ..overlay.at(Part::Caption)
1216 };
1217 let picture = if url.is_empty() {
1218 div()
1219 .h(px(IMAGE_EMPTY_HEIGHT))
1220 .flex()
1221 .items_center()
1222 .px(px(CARD_PADDING))
1223 .rounded(px(IMAGE_RADIUS))
1224 .border_1()
1225 .border_dashed()
1226 .border_color(theme.border)
1227 .text_size(px(TEXT_SIZE))
1228 .text_color(theme.text_muted)
1229 .child(IMAGE_EMPTY)
1230 } else {
1231 let picture = match url.contains("://") {
1235 true => img(SharedString::from(url.to_string())),
1236 false => img(std::path::PathBuf::from(url)),
1237 };
1238 let box_ = div()
1239 .relative()
1240 .rounded(px(IMAGE_RADIUS))
1241 .overflow_hidden()
1242 .border_1()
1243 .border_color(theme.border)
1244 .children(overlay.layouts.map(|layouts| {
1245 let layouts = layouts.clone();
1246 let ix = overlay.block;
1247 canvas(
1248 move |bounds, _, _| layouts.record_picture(ix, bounds),
1249 |_, _, _, _| (),
1250 )
1251 .absolute()
1252 .size_full()
1253 }));
1254 match width {
1255 Some(width) => box_
1260 .self_start()
1261 .max_w_full()
1262 .w(px(width as f32))
1263 .child(picture.w(px(width as f32)).max_w_full()),
1264 None => box_.child(picture.max_w_full()),
1267 }
1268 };
1269 div()
1270 .flex()
1271 .flex_col()
1272 .gap(px(CAPTION_GAP))
1273 .child(picture)
1274 .when(
1277 overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1278 |el| {
1279 el.child(text_element(
1280 alt,
1281 CAPTION_TEXT_SIZE,
1282 CAPTION_LINE_HEIGHT,
1283 FontWeight::NORMAL,
1284 overlay,
1285 theme,
1286 ))
1287 },
1288 )
1289 .into_any_element()
1290}
1291
1292fn bookmark(ix: usize, url: &str, form: Form, theme: &Theme, cx: &App) -> AnyElement {
1303 let preview = preview::of(cx, url).unwrap_or_default();
1304 let host = SharedString::from(preview::host(url).to_string());
1305 let label = preview.label.clone().unwrap_or_else(|| host.clone());
1306 let title = preview
1307 .title
1308 .clone()
1309 .unwrap_or_else(|| SharedString::from(url.to_string()));
1310
1311 let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1314 let site = host.clone();
1315 let mark = move |size: f32| {
1316 let host = site.clone();
1317 match icon.clone() {
1318 Some(icon) => img(icon)
1319 .size(px(size))
1320 .rounded(px(size / 4.0))
1321 .with_fallback(move || initial(&host, size, muted, wash))
1322 .into_any_element(),
1323 None => initial(&host, size, muted, wash),
1324 }
1325 };
1326
1327 if form == Form::Chip {
1328 let open = url.to_string();
1329 let pill = div()
1330 .id(ElementId::named_usize("md-chip", ix))
1331 .flex()
1332 .flex_row()
1333 .items_center()
1334 .gap(px(6.0))
1335 .px(px(CHIP_BLOCK_PAD_X))
1336 .py(px(CHIP_BLOCK_PAD_Y))
1337 .rounded(px(CHIP_RADIUS))
1338 .border_1()
1339 .border_color(theme.border)
1340 .bg(theme.element_hover)
1341 .text_size(px(TEXT_SIZE))
1342 .line_height(px(LINE_HEIGHT))
1343 .text_color(theme.text)
1344 .cursor(CursorStyle::PointingHand)
1345 .hover(|el| el.bg(theme.element_active))
1346 .on_click(move |_, _, cx| cx.open_url(&open))
1347 .child(mark(CHIP_ICON))
1348 .child(
1351 div()
1352 .min_w_0()
1353 .truncate()
1354 .child(preview.title.unwrap_or(label)),
1355 );
1356 return div().flex().flex_row().child(pill).into_any_element();
1359 }
1360
1361 let words = div()
1362 .flex()
1363 .flex_col()
1364 .min_w_0()
1365 .h(px(CARD_HEIGHT))
1366 .px(px(CARD_PADDING))
1367 .py(px(CARD_PADDING - 2.0))
1368 .child(
1369 div()
1370 .truncate()
1371 .text_size(px(TEXT_SIZE))
1372 .line_height(px(LINE_HEIGHT))
1373 .text_color(theme.text)
1374 .child(title),
1375 )
1376 .children(preview.description.map(|blurb| {
1377 div()
1378 .line_clamp(2)
1379 .text_size(px(CARD_TEXT_SIZE))
1380 .line_height(px(CARD_LINE_HEIGHT))
1381 .text_color(theme.text_muted)
1382 .child(blurb)
1383 }))
1384 .child(
1385 div()
1386 .mt_auto()
1387 .pt(px(6.0))
1388 .flex()
1389 .items_center()
1390 .gap(px(6.0))
1391 .text_size(px(CARD_TEXT_SIZE))
1392 .text_color(theme.text_muted)
1393 .child(mark(CARD_ICON))
1394 .child(div().truncate().child(label)),
1395 );
1396
1397 let picture = div()
1398 .bg(theme.surface)
1399 .flex()
1400 .items_center()
1401 .justify_center()
1402 .overflow_hidden()
1403 .child(match preview.image {
1404 Some(image) => img(image)
1405 .size_full()
1406 .object_fit(ObjectFit::Cover)
1407 .with_fallback(move || mark(CARD_COVER))
1408 .into_any_element(),
1409 None => mark(CARD_COVER),
1410 });
1411
1412 let open = url.to_string();
1413 let card = div()
1414 .id(ElementId::named_usize("md-bookmark", ix))
1415 .flex()
1416 .w_full()
1417 .overflow_hidden()
1418 .rounded(px(8.0))
1419 .border_1()
1420 .border_color(theme.border)
1421 .bg(theme.surface_card)
1422 .cursor(CursorStyle::PointingHand)
1423 .hover(|el| el.bg(theme.element_hover))
1424 .on_click(move |_, _, cx| cx.open_url(&open));
1425
1426 if form == Form::Embed {
1427 card.flex_col()
1428 .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1429 .child(words.w_full())
1430 } else {
1431 card.h(px(CARD_HEIGHT))
1432 .child(words.flex_1())
1433 .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1434 }
1435 .into_any_element()
1436}
1437
1438fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1441 div()
1442 .flex_none()
1443 .size(px(size))
1444 .rounded(px(size / 4.0))
1445 .bg(wash)
1446 .flex()
1447 .items_center()
1448 .justify_center()
1449 .text_size(px(size * 0.55))
1450 .text_color(color)
1451 .child(SharedString::from(
1452 host.chars()
1453 .next()
1454 .unwrap_or('?')
1455 .to_uppercase()
1456 .to_string(),
1457 ))
1458 .into_any_element()
1459}
1460
1461fn table(
1468 align: &[Align],
1469 header: &[Text],
1470 rows: &[Vec<Text>],
1471 overlay: Overlay,
1472 theme: &Theme,
1473 window: &mut Window,
1474) -> AnyElement {
1475 let ix = overlay.block;
1476 let all: Vec<&[Text]> = std::iter::once(header)
1477 .filter(|row| !row.is_empty())
1478 .chain(rows.iter().map(|row| row.as_slice()))
1479 .collect();
1480 let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1481 if columns == 0 {
1482 return gpui::Empty.into_any_element();
1483 }
1484 let has_header = !header.is_empty();
1485
1486 let text_system = window.text_system();
1487 let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1488 let mut content = vec![0.0f32; columns];
1489 for (r, row) in all.iter().enumerate() {
1490 let weight = if has_header && r == 0 {
1491 FontWeight::BOLD
1492 } else {
1493 FontWeight::NORMAL
1494 };
1495 let mut out = Vec::with_capacity(columns);
1496 for (c, natural) in content.iter_mut().enumerate() {
1497 let Some(cell) = row.get(c) else {
1498 out.push(None);
1499 continue;
1500 };
1501 let flat = flatten(cell, weight, theme);
1502 if !flat.text.is_empty() {
1503 let width = f32::from(
1504 text_system
1505 .shape_line(flat.text.clone(), px(TEXT_SIZE), &flat.runs, None)
1506 .width(),
1507 );
1508 *natural = natural.max(width);
1509 }
1510 out.push(Some(flat));
1511 }
1512 flats.push(out);
1513 }
1514
1515 let naturals: Vec<f32> = content
1516 .iter()
1517 .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
1518 .collect();
1519 let minimums: Vec<f32> = naturals
1520 .iter()
1521 .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
1522 .collect();
1523 let hairline = theme.hairline(0.10);
1524
1525 let mut inner = div()
1526 .flex()
1527 .flex_col()
1528 .w_full()
1529 .min_w(px(minimums.iter().sum::<f32>()));
1530 for (r, row) in flats.into_iter().enumerate() {
1531 if r > 0 {
1532 inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
1533 }
1534 let mut row_el = div().flex().flex_row();
1535 for (c, cell) in row.into_iter().enumerate() {
1536 let mut cell_el = div()
1537 .flex_grow(naturals[c])
1538 .flex_shrink(naturals[c])
1539 .flex_basis(px(0.0))
1540 .min_w(px(minimums[c]))
1541 .p(px(TABLE_CELL_PADDING))
1542 .text_size(px(TEXT_SIZE))
1543 .line_height(px(LINE_HEIGHT));
1544 cell_el = match align.get(c).copied().unwrap_or_default() {
1545 Align::Left => cell_el,
1546 Align::Center => cell_el.text_center(),
1547 Align::Right => cell_el.text_right(),
1548 };
1549 if let Some(flat) = cell {
1550 let row = if has_header { r } else { r + 1 };
1554 let len = flat.text.len();
1555 cell_el = cell_el.child(painted_text(
1556 flat,
1557 len,
1558 TEXT_SIZE,
1559 LINE_HEIGHT,
1560 overlay.at(Part::Cell { row, column: c }),
1561 theme,
1562 ));
1563 }
1564 row_el = row_el.child(cell_el);
1565 }
1566 inner = inner.child(row_el);
1567 }
1568
1569 div()
1570 .id(ElementId::named_usize("md-table", ix))
1571 .w_full()
1572 .overflow_x_scroll()
1573 .restrict_scroll_to_axis()
1574 .child(inner)
1575 .into_any_element()
1576}