1use super::*;
4use ui::input::overdraw::Layers;
5
6pub struct Flat {
12 pub text: SharedString,
13 pub runs: Vec<TextRun>,
14 pub links: Vec<(Range<usize>, String)>,
15 pub code: Vec<Range<usize>>,
16 pub washes: Vec<(Range<usize>, Hsla)>,
19 pub mentions: Vec<Mention>,
20 pub shown: Shown,
21}
22
23#[derive(Clone, Debug)]
25pub struct Mention {
26 pub url: String,
27 pub range: Range<usize>,
29 pub icon: Range<usize>,
31 pub favicon: Option<SharedString>,
32 pub glyph: Option<ui::icons::Icon>,
34 pub initial: SharedString,
36}
37
38pub use ui::input::Shown;
39
40const ICON_SLOT: &str = "\u{2003}";
43
44#[derive(Clone, Copy, Debug)]
47struct Rows {
48 radius: f32,
49 pad_x: f32,
50 inset_y: f32,
51}
52
53impl Rows {
54 const FLUSH: Self = Self {
56 radius: 0.0,
57 pad_x: 0.0,
58 inset_y: 0.0,
59 };
60 const ROUNDED: Self = Self {
62 radius: 2.0,
63 pad_x: 0.0,
64 inset_y: 0.0,
65 };
66 const CODE: Self = Self {
68 radius: INLINE_CODE_RADIUS,
69 pad_x: INLINE_CODE_PAD_X,
70 inset_y: INLINE_CODE_INSET_Y,
71 };
72}
73
74pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
77 flatten_with(text, base_weight, theme, |_| None, |_| None)
78}
79
80pub fn flatten_with(
84 text: &Text,
85 base_weight: FontWeight,
86 theme: &Theme,
87 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
88 preview: impl Fn(&str) -> Option<preview::Preview>,
89) -> Flat {
90 let mut cuts: Vec<usize> = text
91 .marks
92 .iter()
93 .flat_map(|span| [span.range.start, span.range.end])
94 .chain([0, text.text.len()])
95 .filter(|cut| *cut <= text.text.len())
96 .collect();
97 cuts.sort_unstable();
98 cuts.dedup();
99
100 let mut atoms: Vec<(Range<usize>, &str)> = text
102 .marks
103 .iter()
104 .filter_map(|span| match &span.mark {
105 Mark::Mention { url, .. } if !span.range.is_empty() => {
106 Some((span.range.clone(), url.as_str()))
107 }
108 _ => None,
109 })
110 .collect();
111 atoms.sort_by_key(|(range, _)| range.start);
112 atoms.dedup_by(|later, earlier| later.0.start < earlier.0.end);
113
114 let mut shown = String::with_capacity(text.text.len());
115 let mut runs = Vec::new();
116 let mut links: Vec<(Range<usize>, String)> = Vec::new();
117 let mut code: Vec<Range<usize>> = Vec::new();
118 let mut washes: Vec<(Range<usize>, Hsla)> = Vec::new();
119 let mut mentions: Vec<Mention> = Vec::new();
120 let mut map: Vec<(Range<usize>, Range<usize>)> = Vec::new();
121
122 for pair in cuts.windows(2) {
123 let (start, end) = (pair[0], pair[1]);
124 let atom = atoms
125 .iter()
126 .find(|(range, _)| range.start <= start && end <= range.end);
127 if atom.is_some_and(|(range, _)| range.start != start) {
128 continue;
129 }
130 let covering = text
131 .marks
132 .iter()
133 .filter(|span| span.range.start <= start && span.range.end >= end);
134
135 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
136 let mut link = None;
137 let mut custom = crate::MarkPaint::default();
140 for span in covering {
141 match &span.mark {
142 Mark::Bold => bold = true,
143 Mark::Italic => italic = true,
144 Mark::Strike => strike = true,
145 Mark::Code => mono = true,
146 Mark::Mention { .. } => {}
147 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
148 Mark::Custom(name) => {
149 let Some(painted) = paint(name) else { continue };
150 custom.color = painted.color.or(custom.color);
151 custom.background = painted.background.or(custom.background);
152 custom.weight = painted.weight.or(custom.weight);
153 custom.italic |= painted.italic;
154 custom.underline |= painted.underline;
155 custom.strikethrough |= painted.strikethrough;
156 }
157 }
158 }
159 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
160
161 let mut face = font(if mono && atom.is_none() {
162 theme.font_mono.clone()
163 } else {
164 theme.font_body.clone()
165 });
166 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
167 FontWeight::SEMIBOLD
168 } else {
169 custom.weight.unwrap_or(base_weight)
170 };
171 face.style = if italic {
172 FontStyle::Italic
173 } else {
174 FontStyle::Normal
175 };
176 let underline = UnderlineStyle {
177 color: Some(theme.text_muted),
178 thickness: px(1.0),
179 wavy: false,
180 };
181
182 if let Some((range, url)) = atom {
183 let described = preview(url).unwrap_or_default();
184 let host = preview::host(url);
185 let own = &text.text[range.clone()];
186 let title: SharedString = match &described.title {
187 Some(title) => title.clone(),
188 None if own != *url => own.to_string().into(),
189 None => host.to_string().into(),
190 };
191 let from = shown.len();
192 let mut run = |shown: &mut String,
193 piece: &str,
194 color: Hsla,
195 underline: Option<UnderlineStyle>| {
196 shown.push_str(piece);
197 runs.push(TextRun {
198 len: piece.len(),
199 font: face.clone(),
200 color,
201 background_color: None,
202 underline,
203 strikethrough: None,
204 });
205 };
206 run(&mut shown, ICON_SLOT, gpui::transparent_black(), None);
207 run(&mut shown, " ", theme.text, None);
208 run(&mut shown, &title, theme.text, Some(underline));
209 let to = shown.len();
210 links.push((from..to, url.to_string()));
211 mentions.push(Mention {
212 url: url.to_string(),
213 range: from..to,
214 icon: from..from + ICON_SLOT.len(),
215 favicon: described.icon,
216 glyph: described.glyph,
217 initial: host
218 .chars()
219 .next()
220 .unwrap_or('?')
221 .to_uppercase()
222 .to_string()
223 .into(),
224 });
225 map.push((range.clone(), from..to));
226 continue;
227 }
228
229 let (from, to) = (shown.len(), shown.len() + end - start);
230 shown.push_str(&text.text[start..end]);
231 if mono {
232 match code.last_mut() {
233 Some(range) if range.end == from => range.end = to,
234 _ => code.push(from..to),
235 }
236 }
237 if let Some(background) = custom.background {
238 match washes.last_mut() {
239 Some((range, last)) if range.end == from && *last == background => range.end = to,
240 _ => washes.push((from..to, background)),
241 }
242 }
243 if let Some(url) = &link {
244 match links.last_mut() {
245 Some((range, last)) if range.end == from && last == url => range.end = to,
246 _ => links.push((from..to, url.clone())),
247 }
248 }
249
250 runs.push(TextRun {
251 len: end - start,
252 font: face,
253 color: match (mono, custom.color) {
256 (_, Some(color)) => color,
257 (true, None) => theme.code_text,
258 (false, None) => theme.text,
259 },
260 background_color: None,
261 underline: (link.is_some() || custom.underline).then_some(underline),
262 strikethrough: strike.then_some(StrikethroughStyle {
263 thickness: px(1.0),
264 color: Some(theme.text_muted),
265 }),
266 });
267 }
268
269 Flat {
270 text: shown.into(),
271 runs,
272 links,
273 code,
274 washes,
275 mentions,
276 shown: Shown::new(map),
277 }
278}
279
280pub(super) fn text_element(
281 text: &Text,
282 size: f32,
283 line_height: f32,
284 weight: FontWeight,
285 overlay: Overlay,
286 theme: &Theme,
287 cx: &App,
288) -> AnyElement {
289 let flat = flatten_with(
290 text,
291 weight,
292 theme,
293 |name| crate::paint::marks::paint_of(cx, name, theme),
294 |url| crate::app::link::preview(crate::Link::new(url, overlay.base), cx),
295 );
296 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
297}
298
299pub(super) fn painted_text(
305 flat: Flat,
306 len: usize,
307 size: f32,
308 line_height: f32,
309 overlay: Overlay,
310 theme: &Theme,
311) -> AnyElement {
312 let (ix, part) = (overlay.block, overlay.part);
313 let shown = flat.shown.clone();
314 let caret = overlay.caret_painted().map(|offset| shown.at(offset));
315 let affinity = overlay.affinity();
316 let selected = overlay.selected(len).map(|range| shown.range(&range));
317 let span = 0..len;
318 let hint = overlay
321 .placeholder
322 .filter(|_| len == 0 && overlay.caret().is_some())
325 .map(|hint| {
326 div()
327 .absolute()
328 .text_color(theme.text_faint)
329 .child(hint.clone())
330 });
331 let (shape, height, hollow) = (
332 overlay.caret_shape,
333 overlay.caret_height,
334 overlay.caret_hollow(),
335 );
336 let glyph = caret
337 .filter(|_| shape.cuts_out(hollow))
338 .and_then(|offset| overlay.covered(&flat.text, offset));
339 let runs = flat.runs.clone();
340 let styled = StyledText::new(flat.text).with_runs(flat.runs);
341 let layout = styled.layout().clone();
342
343 let mentions = flat.mentions;
344 let link_ranges: Vec<Range<usize>> =
345 flat.links.iter().map(|(range, _)| range.clone()).collect();
346 let press_layout = layout.clone();
347 let painted: AnyElement = if flat.links.is_empty() {
348 styled.into_any_element()
349 } else {
350 let hovered = flat.links.clone();
351 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
352 let jump = overlay.jump.cloned();
353 let base = overlay.base.map(Path::to_path_buf);
354 let hover_base = base.clone();
355 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
356 .on_click(ranges, move |clicked, window, cx| {
357 if let Some(url) = urls.get(clicked) {
358 let link = crate::Link::new(url, base.as_deref());
359 crate::app::link::follow(link, jump.as_ref(), window, cx);
360 }
361 })
362 .hoverable_tooltip(move |at, _window, cx| {
363 let (_, url) = hovered.iter().find(|(range, _)| range.contains(&at))?;
364 Some(HoverCard::view(url, hover_base.clone(), cx))
365 })
366 .into_any_element()
367 };
368
369 let wash = theme.code_wash;
373 let code_ranges = flat.code;
374 let washes = flat.washes;
375 let (icon_color, icon_wash) = (theme.text_muted, theme.element_hover);
376 let caret_color = theme.caret;
377 let annotated: Vec<_> = overlay
378 .annotated(len, theme)
379 .into_iter()
380 .map(|(range, wash)| (shown.range(&range), wash))
381 .collect();
382 let layouts = overlay.layouts.cloned();
383 let mut layers = Layers::new();
386 for (range, color) in washes {
387 layers = layers.wash(range, color, Rows::FLUSH);
388 }
389 for (range, color) in annotated {
390 layers = layers.wash(range, color, Rows::ROUNDED);
391 }
392 for range in code_ranges {
393 layers = layers.wash(range, wash, Rows::CODE);
394 }
395 if let Some(range) = selected {
396 layers = layers.select(range, theme.selection, Rows::ROUNDED);
397 }
398 let overdraws = ui::input::overdraw::overdraws(&runs, theme.bg, &layers);
399 let caret_layout = layout.clone();
402 let cover = caret.map_or(caret_color, |offset| {
403 ui::input::caret::cover(&runs, offset, theme.bg, &layers, caret_color)
404 });
405 let over = canvas(
406 |_, _, _| (),
407 move |_, _, window, cx| {
408 ui::input::overdraw::paint(
409 caret_layout.line_layouts().iter().map(|line| &**line),
410 caret_layout.bounds().origin,
411 caret_layout.line_height(),
412 px(2.0),
413 &overdraws,
414 window,
415 );
416 let Some(offset) = caret else {
417 return;
418 };
419 let face = font(Theme::of(cx).font_body.clone());
420 paint_caret(
421 &caret_layout,
422 offset,
423 affinity,
424 glyph.as_ref(),
425 CaretPaint {
426 shape,
427 height,
428 hollow,
429 color: caret_color,
430 cover,
431 size,
432 face,
433 },
434 window,
435 );
436 },
437 )
438 .absolute()
439 .size_full();
440 let underlay = canvas(
441 {
442 let mentions = mentions.clone();
443 move |_, window, cx| {
444 mentions
445 .iter()
446 .map(|mention| {
447 let favicon = mention.favicon.clone()?;
448 let source = gpui::Resource::Uri(favicon.to_string().into());
449 window
450 .use_asset::<gpui::ImgResourceLoader>(&source, cx)?
451 .ok()
452 })
453 .collect::<Vec<_>>()
454 }
455 },
456 move |_, favicons, window, cx| {
457 if let Some(layouts) = &layouts {
458 layouts.record(ix, part, span.clone(), layout.clone(), shown.clone());
459 }
460 for (range, color, rows) in layers.painted() {
462 for rect in range_rects(&layout, range, rows.pad_x, rows.inset_y) {
463 window.paint_quad(quad(
464 rect,
465 px(rows.radius),
466 color,
467 px(0.0),
468 gpui::transparent_black(),
469 BorderStyle::default(),
470 ));
471 }
472 }
473 for (mention, favicon) in mentions.iter().zip(favicons) {
474 let Some(slot) = range_rects(&layout, &mention.icon, 0.0, 0.0).pop() else {
475 continue;
476 };
477 let side = slot.size.width.min(px(size));
478 let icon = Bounds::new(
479 slot.origin + point(px(0.0), (slot.size.height - side) / 2.0),
480 gpui::size(side, side),
481 );
482 let radius = gpui::Corners::all(side / 4.0);
483 if let Some(data) = mention.glyph.as_ref().and_then(|glyph| glyph.data()) {
484 let path = SharedString::from(format!("markdown-glyph-{:p}", data.as_ptr()));
485 window
486 .paint_svg(
487 icon,
488 path,
489 Some(data),
490 gpui::TransformationMatrix::unit(),
491 icon_color,
492 cx,
493 )
494 .ok();
495 continue;
496 }
497 if let Some(favicon) = favicon {
498 window
499 .paint_image(icon, icon, radius, favicon, 0, false)
500 .ok();
501 continue;
502 }
503 window.paint_quad(quad(
504 icon,
505 side / 4.0,
506 icon_wash,
507 px(0.0),
508 gpui::transparent_black(),
509 BorderStyle::default(),
510 ));
511 let font = font(Theme::of(cx).font_body.clone());
512 let letter = window.text_system().shape_line(
513 mention.initial.clone(),
514 side * 0.55,
515 &[TextRun {
516 len: mention.initial.len(),
517 font,
518 color: icon_color,
519 background_color: None,
520 underline: None,
521 strikethrough: None,
522 }],
523 None,
524 );
525 let at = point(icon.center().x - letter.width / 2.0, icon.origin.y);
526 letter
527 .paint(at, side, gpui::TextAlign::Left, None, window, cx)
528 .ok();
529 }
530 },
531 )
532 .absolute()
533 .size_full();
534
535 div()
536 .text_size(px(size))
537 .line_height(px(line_height))
538 .relative()
539 .child(underlay)
540 .children(hint)
541 .child(painted)
542 .child(over)
543 .on_mouse_down(MouseButton::Left, move |event, _, cx| {
547 let on_link = press_layout
548 .index_for_position(event.position)
549 .is_ok_and(|at| link_ranges.iter().any(|range| range.contains(&at)));
550 if event.modifiers.shift && on_link {
551 cx.stop_propagation();
552 }
553 })
554 .into_any_element()
555}
556
557pub(super) fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
563 let inset = (line_height - px(size)) / 2.0;
564 Bounds::new(
565 head + point(px(0.0), inset),
566 gpui::size(px(CARET_WIDTH), px(size)),
567 )
568}
569
570pub(super) fn glyph_at(text: &str, offset: usize) -> Option<Range<usize>> {
572 (offset < text.len() && text.is_char_boundary(offset) && !text[offset..].starts_with('\n'))
573 .then(|| offset..ui::input::next_boundary(text, offset))
574}
575
576pub(super) struct CaretPaint {
578 pub shape: ui::input::CaretShape,
579 pub height: ui::input::CaretHeight,
580 pub hollow: bool,
581 pub color: Hsla,
582 pub cover: Hsla,
584 pub size: f32,
586 pub face: gpui::Font,
588}
589
590pub(super) fn caret_position(
593 layout: &TextLayout,
594 offset: usize,
595 affinity: Affinity,
596) -> Option<Point<Pixels>> {
597 if affinity == Affinity::Downstream
598 && let Some(start) = wrapped_row_start(layout, offset)
599 {
600 return Some(start);
601 }
602 layout.position_for_index(offset)
603}
604
605fn wrapped_row_start(layout: &TextLayout, offset: usize) -> Option<Point<Pixels>> {
608 let line_height = layout.line_height();
609 let mut origin = layout.bounds().origin;
610 let mut line_start = 0;
611 for line in layout.line_layouts() {
612 if offset > line_start + line.len() {
613 origin.y += line.size(line_height).height;
614 line_start += line.len() + 1;
615 continue;
616 }
617 let shaped = &line.unwrapped_layout;
618 let row = line.wrap_boundaries().iter().position(|wrap| {
619 line_start + shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index == offset
620 })?;
621 return Some(origin + point(px(0.0), line_height * (row + 1) as f32));
622 }
623 None
624}
625
626pub(super) fn paint_caret(
629 layout: &TextLayout,
630 offset: usize,
631 affinity: Affinity,
632 glyph: Option<&Range<usize>>,
633 paint: CaretPaint,
634 window: &mut Window,
635) {
636 let line_height = layout.line_height();
637 let covered = glyph.and_then(|glyph| range_rects(layout, glyph, 0.0, 0.0).into_iter().next());
638 let (head, width) = match covered {
639 Some(rect) => (rect.origin, rect.size.width),
640 None => {
641 let Some(head) = caret_position(layout, offset, affinity) else {
642 return;
643 };
644 let width = match paint.shape {
645 ui::input::CaretShape::Bar => px(0.0),
646 _ => caret_advance(layout, offset).unwrap_or_else(|| {
647 ui::input::caret::zero_width(paint.face, px(paint.size), window)
648 }),
649 };
650 (head, width)
651 }
652 };
653 window.paint_quad(paint.shape.quad(
654 caret_quad(head, paint.size, line_height),
655 line_height,
656 paint.height,
657 width,
658 paint.color,
659 paint.hollow,
660 ));
661 if let Some(glyph) = glyph
662 && covered.is_some()
663 && let Some((line, range)) = line_of(layout, glyph)
664 {
665 ui::input::caret::paint_covered(
666 &line.unwrapped_layout,
667 &range,
668 head,
669 line_height,
670 paint.cover,
671 window,
672 );
673 }
674}
675
676fn line_of(
678 layout: &TextLayout,
679 range: &Range<usize>,
680) -> Option<(Arc<gpui::WrappedLineLayout>, Range<usize>)> {
681 let mut start = 0;
682 for line in layout.line_layouts() {
683 let end = start + line.len();
684 if range.start <= end {
685 return (range.end <= end).then(|| (line, range.start - start..range.end - start));
686 }
687 start = end + 1;
688 }
689 None
690}
691
692pub(super) fn range_rects(
694 layout: &gpui::TextLayout,
695 range: &Range<usize>,
696 pad_x: f32,
697 inset_y: f32,
698) -> Vec<Bounds<Pixels>> {
699 let mut rects = Vec::new();
700 let line_height = layout.line_height();
701 let mut origin = layout.bounds().origin;
702 let mut line_start = 0;
703 for line in layout.line_layouts() {
704 let shaped = &line.unwrapped_layout;
705 let row_ends = line
708 .wrap_boundaries()
709 .iter()
710 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
711 .chain([line.len()]);
712 let mut row_start = 0;
713 for (row, row_end) in row_ends.enumerate() {
714 let from = range
715 .start
716 .saturating_sub(line_start)
717 .clamp(row_start, row_end);
718 let to = range.end.saturating_sub(line_start).min(row_end);
719 let row_x = shaped.x_for_index(row_start);
720 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
721 if from < to && right > left {
722 rects.push(Bounds::new(
723 origin
724 + point(
725 left - row_x - px(pad_x),
726 line_height * row as f32 + px(inset_y),
727 ),
728 size(
729 right - left + px(2.0 * pad_x),
730 line_height - px(2.0 * inset_y),
731 ),
732 ));
733 }
734 row_start = row_end;
735 }
736 origin.y += line.size(line_height).height;
737 line_start += line.len() + 1;
739 }
740 rects
741}
742
743pub(super) fn caret_advance(layout: &TextLayout, offset: usize) -> Option<Pixels> {
745 let text = layout.text();
746 let mut start = 0;
747 for line in layout.line_layouts() {
748 let end = start + line.len();
749 if offset <= end {
750 return ui::input::caret::character_advance(&line, &text[start..end], offset - start);
751 }
752 start = end + 1;
753 }
754 None
755}