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