1use super::*;
4
5pub struct Flat {
11 pub text: SharedString,
12 pub runs: Vec<TextRun>,
13 pub links: Vec<(Range<usize>, String)>,
14 pub code: Vec<Range<usize>>,
15 pub mentions: Vec<Mention>,
16 pub shown: Shown,
17}
18
19#[derive(Clone, Debug)]
21pub struct Mention {
22 pub url: String,
23 pub range: Range<usize>,
25 pub icon: Range<usize>,
27 pub favicon: Option<SharedString>,
28 pub glyph: Option<ui::icons::Icon>,
30 pub initial: SharedString,
32}
33
34#[derive(Clone, Debug, Default)]
40pub struct Shown(Rc<[(Range<usize>, Range<usize>)]>);
41
42impl Shown {
43 pub fn at(&self, offset: usize) -> usize {
45 self.map(offset, false)
46 }
47
48 pub fn range(&self, range: &Range<usize>) -> Range<usize> {
50 self.map(range.start, false)..self.map(range.end, true)
51 }
52
53 fn map(&self, offset: usize, end: bool) -> usize {
54 let mut shift = 0isize;
55 for (text, flat) in self.0.iter() {
56 if offset <= text.start {
57 break;
58 }
59 if offset < text.end {
60 return if end { flat.end } else { flat.start };
61 }
62 shift = flat.end as isize - text.end as isize;
63 }
64 offset.saturating_add_signed(shift)
65 }
66
67 pub fn offset(&self, shown: usize) -> usize {
70 let mut shift = 0isize;
71 for (text, flat) in self.0.iter() {
72 if shown <= flat.start {
73 break;
74 }
75 if shown < flat.end {
76 return if shown - flat.start <= flat.end - shown {
77 text.start
78 } else {
79 text.end
80 };
81 }
82 shift = text.end as isize - flat.end as isize;
83 }
84 shown.saturating_add_signed(shift)
85 }
86}
87
88const ICON_SLOT: &str = "\u{2003}";
91
92pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
95 flatten_with(text, base_weight, theme, |_| None, |_| None)
96}
97
98pub fn flatten_with(
102 text: &Text,
103 base_weight: FontWeight,
104 theme: &Theme,
105 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
106 preview: impl Fn(&str) -> Option<preview::Preview>,
107) -> Flat {
108 let mut cuts: Vec<usize> = text
109 .marks
110 .iter()
111 .flat_map(|span| [span.range.start, span.range.end])
112 .chain([0, text.text.len()])
113 .filter(|cut| *cut <= text.text.len())
114 .collect();
115 cuts.sort_unstable();
116 cuts.dedup();
117
118 let mut atoms: Vec<(Range<usize>, &str)> = text
120 .marks
121 .iter()
122 .filter_map(|span| match &span.mark {
123 Mark::Mention { url, .. } if !span.range.is_empty() => {
124 Some((span.range.clone(), url.as_str()))
125 }
126 _ => None,
127 })
128 .collect();
129 atoms.sort_by_key(|(range, _)| range.start);
130 atoms.dedup_by(|later, earlier| later.0.start < earlier.0.end);
131
132 let mut shown = String::with_capacity(text.text.len());
133 let mut runs = Vec::new();
134 let mut links: Vec<(Range<usize>, String)> = Vec::new();
135 let mut code: Vec<Range<usize>> = Vec::new();
136 let mut mentions: Vec<Mention> = Vec::new();
137 let mut map: Vec<(Range<usize>, Range<usize>)> = Vec::new();
138
139 for pair in cuts.windows(2) {
140 let (start, end) = (pair[0], pair[1]);
141 let atom = atoms
142 .iter()
143 .find(|(range, _)| range.start <= start && end <= range.end);
144 if atom.is_some_and(|(range, _)| range.start != start) {
145 continue;
146 }
147 let covering = text
148 .marks
149 .iter()
150 .filter(|span| span.range.start <= start && span.range.end >= end);
151
152 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
153 let mut link = None;
154 let mut custom = crate::MarkPaint::default();
157 for span in covering {
158 match &span.mark {
159 Mark::Bold => bold = true,
160 Mark::Italic => italic = true,
161 Mark::Strike => strike = true,
162 Mark::Code => mono = true,
163 Mark::Mention { .. } => {}
164 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
165 Mark::Custom(name) => {
166 let Some(painted) = paint(name) else { continue };
167 custom.color = painted.color.or(custom.color);
168 custom.background = painted.background.or(custom.background);
169 custom.weight = painted.weight.or(custom.weight);
170 custom.italic |= painted.italic;
171 custom.underline |= painted.underline;
172 custom.strikethrough |= painted.strikethrough;
173 }
174 }
175 }
176 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
177
178 let mut face = font(if mono && atom.is_none() {
179 theme.font_mono.clone()
180 } else {
181 theme.font_body.clone()
182 });
183 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
184 FontWeight::SEMIBOLD
185 } else {
186 custom.weight.unwrap_or(base_weight)
187 };
188 face.style = if italic {
189 FontStyle::Italic
190 } else {
191 FontStyle::Normal
192 };
193 let underline = UnderlineStyle {
194 color: Some(theme.text_muted),
195 thickness: px(1.0),
196 wavy: false,
197 };
198
199 if let Some((range, url)) = atom {
200 let described = preview(url).unwrap_or_default();
201 let host = preview::host(url);
202 let own = &text.text[range.clone()];
203 let title: SharedString = match &described.title {
204 Some(title) => title.clone(),
205 None if own != *url => own.to_string().into(),
206 None => host.to_string().into(),
207 };
208 let from = shown.len();
209 let mut run = |shown: &mut String,
210 piece: &str,
211 color: Hsla,
212 underline: Option<UnderlineStyle>| {
213 shown.push_str(piece);
214 runs.push(TextRun {
215 len: piece.len(),
216 font: face.clone(),
217 color,
218 background_color: None,
219 underline,
220 strikethrough: None,
221 });
222 };
223 run(&mut shown, ICON_SLOT, gpui::transparent_black(), None);
224 run(&mut shown, " ", theme.text, None);
225 if let Some(label) = described.label.filter(|_| described.title.is_some()) {
226 run(&mut shown, &label, theme.text_muted, None);
227 run(&mut shown, " ", theme.text, None);
228 }
229 run(&mut shown, &title, theme.text, Some(underline));
230 let to = shown.len();
231 links.push((from..to, url.to_string()));
232 mentions.push(Mention {
233 url: url.to_string(),
234 range: from..to,
235 icon: from..from + ICON_SLOT.len(),
236 favicon: described.icon,
237 glyph: described.glyph,
238 initial: host
239 .chars()
240 .next()
241 .unwrap_or('?')
242 .to_uppercase()
243 .to_string()
244 .into(),
245 });
246 map.push((range.clone(), from..to));
247 continue;
248 }
249
250 let (from, to) = (shown.len(), shown.len() + end - start);
251 shown.push_str(&text.text[start..end]);
252 if mono {
253 match code.last_mut() {
254 Some(range) if range.end == from => range.end = to,
255 _ => code.push(from..to),
256 }
257 }
258 if let Some(url) = &link {
259 match links.last_mut() {
260 Some((range, last)) if range.end == from && last == url => range.end = to,
261 _ => links.push((from..to, url.clone())),
262 }
263 }
264
265 runs.push(TextRun {
266 len: end - start,
267 font: face,
268 color: match (mono, custom.color) {
271 (_, Some(color)) => color,
272 (true, None) => theme.code_text,
273 (false, None) => theme.text,
274 },
275 background_color: custom.background,
276 underline: (link.is_some() || custom.underline).then_some(underline),
277 strikethrough: strike.then_some(StrikethroughStyle {
278 thickness: px(1.0),
279 color: Some(theme.text_muted),
280 }),
281 });
282 }
283
284 Flat {
285 text: shown.into(),
286 runs,
287 links,
288 code,
289 mentions,
290 shown: Shown(map.into()),
291 }
292}
293
294pub(super) fn text_element(
295 text: &Text,
296 size: f32,
297 line_height: f32,
298 weight: FontWeight,
299 overlay: Overlay,
300 theme: &Theme,
301 cx: &App,
302) -> AnyElement {
303 let flat = flatten_with(
304 text,
305 weight,
306 theme,
307 |name| crate::marks::paint_of(cx, name, theme),
308 |url| preview::of(cx, url),
309 );
310 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
311}
312
313pub(super) fn painted_text(
319 flat: Flat,
320 len: usize,
321 size: f32,
322 line_height: f32,
323 overlay: Overlay,
324 theme: &Theme,
325) -> AnyElement {
326 let (ix, part) = (overlay.block, overlay.part);
327 let shown = flat.shown.clone();
328 let caret = overlay.caret_painted().map(|offset| shown.at(offset));
329 let selected = overlay.selected(len).map(|range| shown.range(&range));
330 let span = 0..len;
331 let hint = overlay
334 .placeholder
335 .filter(|_| len == 0 && overlay.caret().is_some())
338 .map(|hint| {
339 div()
340 .absolute()
341 .text_color(theme.text_faint)
342 .child(hint.clone())
343 });
344 let (shape, height, hollow) = (
345 overlay.caret_shape,
346 overlay.caret_height,
347 overlay.caret_hollow(),
348 );
349 let glyph = caret
350 .filter(|_| shape.cuts_out(hollow))
351 .and_then(|offset| glyph_at(&flat.text, offset));
352 let runs = match &glyph {
353 Some(glyph) => ui::input::caret::recoloured(flat.runs, glyph, theme.bg),
354 None => flat.runs,
355 };
356 let styled = StyledText::new(flat.text).with_runs(runs);
357 let layout = styled.layout().clone();
358
359 let mentions = flat.mentions;
360 let painted: AnyElement =
361 if flat.links.is_empty() {
362 styled.into_any_element()
363 } else {
364 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
365 let hovered: Vec<(Range<usize>, String)> = mentions
366 .iter()
367 .filter(|mention| mention.glyph.is_none())
368 .map(|mention| (mention.range.clone(), mention.url.clone()))
369 .collect();
370 let text = InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
371 .on_click(ranges, move |clicked, window, cx| {
372 if let Some(url) = urls.get(clicked) {
373 crate::link::open(url, window, cx);
374 }
375 });
376 match hovered.is_empty() {
377 true => text.into_any_element(),
378 false => text
379 .tooltip(move |at, _window, cx| {
380 let (_, url) = hovered.iter().find(|(range, _)| range.contains(&at))?;
381 Some(MentionCard::view(url, cx))
382 })
383 .into_any_element(),
384 }
385 };
386
387 let wash = theme.code_wash;
391 let code_ranges = flat.code;
392 let (icon_color, icon_wash) = (theme.text_muted, theme.element_hover);
393 let caret_color = theme.caret;
394 let selection_color = theme.selection;
395 let annotated: Vec<_> = overlay
396 .annotated(len, theme)
397 .into_iter()
398 .map(|(range, wash)| (shown.range(&range), wash))
399 .collect();
400 let layouts = overlay.layouts.cloned();
401 let underlay = canvas(
402 {
403 let mentions = mentions.clone();
404 move |_, window, cx| {
405 mentions
406 .iter()
407 .map(|mention| {
408 let favicon = mention.favicon.clone()?;
409 let source = gpui::Resource::Uri(favicon.to_string().into());
410 window
411 .use_asset::<gpui::ImgResourceLoader>(&source, cx)?
412 .ok()
413 })
414 .collect::<Vec<_>>()
415 }
416 },
417 move |_, favicons, window, cx| {
418 if let Some(layouts) = &layouts {
419 layouts.record(ix, part, span.clone(), layout.clone(), shown.clone());
420 }
421 for (range, wash) in &annotated {
424 for rect in range_rects(&layout, range, 0.0, 0.0) {
425 window.paint_quad(quad(
426 rect,
427 px(2.0),
428 *wash,
429 px(0.0),
430 gpui::transparent_black(),
431 BorderStyle::default(),
432 ));
433 }
434 }
435 if let Some(range) = &selected {
439 for rect in range_rects(&layout, range, 0.0, 0.0) {
440 window.paint_quad(quad(
441 rect,
442 px(2.0),
443 selection_color,
444 px(0.0),
445 gpui::transparent_black(),
446 BorderStyle::default(),
447 ));
448 }
449 }
450 if let Some(offset) = caret {
451 let face = font(Theme::of(cx).font_body.clone());
452 paint_caret(
453 &layout,
454 offset,
455 glyph.as_ref(),
456 CaretPaint {
457 shape,
458 height,
459 hollow,
460 color: caret_color,
461 size,
462 face,
463 },
464 window,
465 );
466 }
467 for range in &code_ranges {
468 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
469 window.paint_quad(quad(
470 rect,
471 px(INLINE_CODE_RADIUS),
472 wash,
473 px(0.0),
474 gpui::transparent_black(),
475 BorderStyle::default(),
476 ));
477 }
478 }
479 for (mention, favicon) in mentions.iter().zip(favicons) {
480 let Some(slot) = range_rects(&layout, &mention.icon, 0.0, 0.0).pop() else {
481 continue;
482 };
483 let side = slot.size.width.min(px(size));
484 let icon = Bounds::new(
485 slot.origin + point(px(0.0), (slot.size.height - side) / 2.0),
486 gpui::size(side, side),
487 );
488 let radius = gpui::Corners::all(side / 4.0);
489 if let Some(data) = mention.glyph.as_ref().and_then(|glyph| glyph.data()) {
490 let path = SharedString::from(format!("markdown-glyph-{:p}", data.as_ptr()));
491 window
492 .paint_svg(
493 icon,
494 path,
495 Some(data),
496 gpui::TransformationMatrix::unit(),
497 icon_color,
498 cx,
499 )
500 .ok();
501 continue;
502 }
503 if let Some(favicon) = favicon {
504 window
505 .paint_image(icon, icon, radius, favicon, 0, false)
506 .ok();
507 continue;
508 }
509 window.paint_quad(quad(
510 icon,
511 side / 4.0,
512 icon_wash,
513 px(0.0),
514 gpui::transparent_black(),
515 BorderStyle::default(),
516 ));
517 let font = font(Theme::of(cx).font_body.clone());
518 let letter = window.text_system().shape_line(
519 mention.initial.clone(),
520 side * 0.55,
521 &[TextRun {
522 len: mention.initial.len(),
523 font,
524 color: icon_color,
525 background_color: None,
526 underline: None,
527 strikethrough: None,
528 }],
529 None,
530 );
531 let at = point(icon.center().x - letter.width / 2.0, icon.origin.y);
532 letter
533 .paint(at, side, gpui::TextAlign::Left, None, window, cx)
534 .ok();
535 }
536 },
537 )
538 .absolute()
539 .size_full();
540
541 div()
542 .text_size(px(size))
543 .line_height(px(line_height))
544 .relative()
545 .child(underlay)
546 .children(hint)
547 .child(painted)
548 .into_any_element()
549}
550
551pub(super) fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
557 let inset = (line_height - px(size)) / 2.0;
558 Bounds::new(
559 head + point(px(0.0), inset),
560 gpui::size(px(CARET_WIDTH), px(size)),
561 )
562}
563
564pub(super) fn glyph_at(text: &str, offset: usize) -> Option<Range<usize>> {
566 (offset < text.len() && text.is_char_boundary(offset) && !text[offset..].starts_with('\n'))
567 .then(|| offset..ui::input::next_boundary(text, offset))
568}
569
570pub(super) struct CaretPaint {
572 pub shape: ui::input::CaretShape,
573 pub height: ui::input::CaretHeight,
574 pub hollow: bool,
575 pub color: Hsla,
576 pub size: f32,
578 pub face: gpui::Font,
580}
581
582pub(super) fn paint_caret(
585 layout: &TextLayout,
586 offset: usize,
587 glyph: Option<&Range<usize>>,
588 paint: CaretPaint,
589 window: &mut Window,
590) {
591 let line_height = layout.line_height();
592 let covered = glyph.and_then(|glyph| range_rects(layout, glyph, 0.0, 0.0).into_iter().next());
593 let (head, width) = match covered {
594 Some(rect) => (rect.origin, rect.size.width),
595 None => {
596 let Some(head) = layout.position_for_index(offset) else {
597 return;
598 };
599 let width = match paint.shape {
600 ui::input::CaretShape::Bar => px(0.0),
601 _ => caret_advance(layout, offset).unwrap_or_else(|| {
602 ui::input::caret::zero_width(paint.face, px(paint.size), window)
603 }),
604 };
605 (head, width)
606 }
607 };
608 window.paint_quad(paint.shape.quad(
609 caret_quad(head, paint.size, line_height),
610 line_height,
611 paint.height,
612 width,
613 paint.color,
614 paint.hollow,
615 ));
616}
617
618pub(super) fn range_rects(
620 layout: &gpui::TextLayout,
621 range: &Range<usize>,
622 pad_x: f32,
623 inset_y: f32,
624) -> Vec<Bounds<Pixels>> {
625 let mut rects = Vec::new();
626 let line_height = layout.line_height();
627 let mut origin = layout.bounds().origin;
628 let mut line_start = 0;
629 for line in layout.line_layouts() {
630 let shaped = &line.unwrapped_layout;
631 let row_ends = line
634 .wrap_boundaries()
635 .iter()
636 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
637 .chain([line.len()]);
638 let mut row_start = 0;
639 for (row, row_end) in row_ends.enumerate() {
640 let from = range
641 .start
642 .saturating_sub(line_start)
643 .clamp(row_start, row_end);
644 let to = range.end.saturating_sub(line_start).min(row_end);
645 let row_x = shaped.x_for_index(row_start);
646 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
647 if from < to && right > left {
648 rects.push(Bounds::new(
649 origin
650 + point(
651 left - row_x - px(pad_x),
652 line_height * row as f32 + px(inset_y),
653 ),
654 size(
655 right - left + px(2.0 * pad_x),
656 line_height - px(2.0 * inset_y),
657 ),
658 ));
659 }
660 row_start = row_end;
661 }
662 origin.y += line.size(line_height).height;
663 line_start += line.len() + 1;
665 }
666 rects
667}
668
669pub(super) fn caret_advance(layout: &TextLayout, offset: usize) -> Option<Pixels> {
671 let text = layout.text();
672 let mut start = 0;
673 for line in layout.line_layouts() {
674 let end = start + line.len();
675 if offset <= end {
676 return ui::input::caret::character_advance(&line, &text[start..end], offset - start);
677 }
678 start = end + 1;
679 }
680 None
681}