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