1use super::*;
4
5pub struct Flat {
8 pub text: SharedString,
9 pub runs: Vec<TextRun>,
10 pub links: Vec<(Range<usize>, String)>,
11 pub code: Vec<Range<usize>>,
12 pub chips: Vec<Range<usize>>,
13}
14
15pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
18 flatten_with(text, base_weight, theme, |_| None)
19}
20
21pub fn flatten_with(
24 text: &Text,
25 base_weight: FontWeight,
26 theme: &Theme,
27 paint: impl Fn(&str) -> Option<crate::MarkPaint>,
28) -> Flat {
29 let mut cuts: Vec<usize> = text
30 .marks
31 .iter()
32 .flat_map(|span| [span.range.start, span.range.end])
33 .chain([0, text.text.len()])
34 .filter(|cut| *cut <= text.text.len())
35 .collect();
36 cuts.sort_unstable();
37 cuts.dedup();
38
39 let mut runs = Vec::new();
40 let mut links: Vec<(Range<usize>, String)> = Vec::new();
41 let mut code: Vec<Range<usize>> = Vec::new();
42 let mut chips: Vec<Range<usize>> = Vec::new();
43
44 for pair in cuts.windows(2) {
45 let (start, end) = (pair[0], pair[1]);
46 let covering = text
47 .marks
48 .iter()
49 .filter(|span| span.range.start <= start && span.range.end >= end);
50
51 let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
52 let mut chip = false;
53 let mut link = None;
54 let mut custom = crate::MarkPaint::default();
57 for span in covering {
58 match &span.mark {
59 Mark::Bold => bold = true,
60 Mark::Italic => italic = true,
61 Mark::Strike => strike = true,
62 Mark::Code => mono = true,
63 Mark::Mention { url, .. } => {
64 chip = true;
65 link = Some(url.clone());
66 }
67 Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
68 Mark::Custom(name) => {
69 let Some(painted) = paint(name) else { continue };
70 custom.color = painted.color.or(custom.color);
71 custom.background = painted.background.or(custom.background);
72 custom.weight = painted.weight.or(custom.weight);
73 custom.italic |= painted.italic;
74 custom.underline |= painted.underline;
75 custom.strikethrough |= painted.strikethrough;
76 }
77 }
78 }
79 let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
80
81 if mono {
82 match code.last_mut() {
83 Some(range) if range.end == start => range.end = end,
84 _ => code.push(start..end),
85 }
86 }
87 if chip {
88 match chips.last_mut() {
89 Some(range) if range.end == start => range.end = end,
90 _ => chips.push(start..end),
91 }
92 }
93 if let Some(url) = &link {
94 match links.last_mut() {
95 Some((range, last)) if range.end == start && last == url => range.end = end,
96 _ => links.push((start..end, url.clone())),
97 }
98 }
99
100 let mut face = font(if mono {
101 theme.font_mono.clone()
102 } else {
103 theme.font_body.clone()
104 });
105 face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
106 FontWeight::SEMIBOLD
107 } else {
108 custom.weight.unwrap_or(base_weight)
109 };
110 face.style = if italic {
111 FontStyle::Italic
112 } else {
113 FontStyle::Normal
114 };
115
116 runs.push(TextRun {
117 len: end - start,
118 font: face,
119 color: match (mono, custom.color) {
123 (_, Some(color)) => color,
124 (true, None) => theme.code_text,
125 (false, None) => theme.text,
126 },
127 background_color: custom.background,
128 underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
129 color: Some(theme.text_muted),
130 thickness: px(1.0),
131 wavy: false,
132 }),
133 strikethrough: strike.then_some(StrikethroughStyle {
134 thickness: px(1.0),
135 color: Some(theme.text_muted),
136 }),
137 });
138 }
139
140 Flat {
141 text: text.text.clone().into(),
142 runs,
143 links,
144 code,
145 chips,
146 }
147}
148
149pub(super) fn text_element(
150 text: &Text,
151 size: f32,
152 line_height: f32,
153 weight: FontWeight,
154 overlay: Overlay,
155 theme: &Theme,
156 cx: &App,
157) -> AnyElement {
158 let flat = flatten_with(text, weight, theme, |name| {
159 crate::marks::paint_of(cx, name, theme)
160 });
161 painted_text(flat, text.text.len(), size, line_height, overlay, theme)
162}
163
164pub(super) fn painted_text(
170 flat: Flat,
171 len: usize,
172 size: f32,
173 line_height: f32,
174 overlay: Overlay,
175 theme: &Theme,
176) -> AnyElement {
177 let (ix, part) = (overlay.block, overlay.part);
178 let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
179 let span = 0..len;
180 let hint = overlay
183 .placeholder
184 .filter(|_| len == 0 && overlay.caret().is_some())
187 .map(|hint| {
188 div()
189 .absolute()
190 .text_color(theme.text_faint)
191 .child(hint.clone())
192 });
193 let styled = StyledText::new(flat.text).with_runs(flat.runs);
194 let layout = styled.layout().clone();
195
196 let painted: AnyElement = if flat.links.is_empty() {
197 styled.into_any_element()
198 } else {
199 let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
200 InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
201 .on_click(ranges, move |clicked, _window, cx| {
202 if let Some(url) = urls.get(clicked) {
203 cx.open_url(url);
204 }
205 })
206 .into_any_element()
207 };
208
209 let wash = theme.code_wash;
213 let code_ranges = flat.code;
214 let chip_wash = theme.element_hover;
215 let chip_edge = theme.border;
216 let chip_ranges = flat.chips;
217 let caret_color = theme.caret;
218 let selection_color = theme.selection;
219 let annotated = overlay.annotated(len, theme);
220 let layouts = overlay.layouts.cloned();
221 let underlay = canvas(
222 |_, _, _| (),
223 move |_, _, window, _| {
224 if let Some(layouts) = &layouts {
225 layouts.record(ix, part, span.clone(), layout.clone());
226 }
227 for (range, wash) in &annotated {
230 for rect in range_rects(&layout, range, 0.0, 0.0) {
231 window.paint_quad(quad(
232 rect,
233 px(2.0),
234 *wash,
235 px(0.0),
236 gpui::transparent_black(),
237 BorderStyle::default(),
238 ));
239 }
240 }
241 if let Some(range) = &selected {
245 for rect in range_rects(&layout, range, 0.0, 0.0) {
246 window.paint_quad(quad(
247 rect,
248 px(2.0),
249 selection_color,
250 px(0.0),
251 gpui::transparent_black(),
252 BorderStyle::default(),
253 ));
254 }
255 }
256 if let Some(offset) = caret
257 && let Some(head) = layout.position_for_index(offset)
258 {
259 window.paint_quad(quad(
260 caret_quad(head, size, layout.line_height()),
261 px(0.0),
262 caret_color,
263 px(0.0),
264 gpui::transparent_black(),
265 BorderStyle::default(),
266 ));
267 }
268 for range in &code_ranges {
269 for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
270 window.paint_quad(quad(
271 rect,
272 px(INLINE_CODE_RADIUS),
273 wash,
274 px(0.0),
275 gpui::transparent_black(),
276 BorderStyle::default(),
277 ));
278 }
279 }
280 for range in &chip_ranges {
283 for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
284 window.paint_quad(quad(
285 rect,
286 px(Theme::control_radius()),
287 chip_wash,
288 px(1.0),
289 chip_edge,
290 BorderStyle::Solid,
291 ));
292 }
293 }
294 },
295 )
296 .absolute()
297 .size_full();
298
299 div()
300 .text_size(px(size))
301 .line_height(px(line_height))
302 .relative()
303 .child(underlay)
304 .children(hint)
305 .child(painted)
306 .into_any_element()
307}
308
309pub(super) fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
315 let inset = (line_height - px(size)) / 2.0;
316 Bounds::new(
317 head + point(px(0.0), inset),
318 gpui::size(px(CARET_WIDTH), px(size)),
319 )
320}
321
322pub(super) fn range_rects(
324 layout: &gpui::TextLayout,
325 range: &Range<usize>,
326 pad_x: f32,
327 inset_y: f32,
328) -> Vec<Bounds<Pixels>> {
329 let mut rects = Vec::new();
330 let line_height = layout.line_height();
331 let mut origin = layout.bounds().origin;
332 let mut line_start = 0;
333 for line in layout.line_layouts() {
334 let shaped = &line.unwrapped_layout;
335 let row_ends = line
338 .wrap_boundaries()
339 .iter()
340 .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
341 .chain([line.len()]);
342 let mut row_start = 0;
343 for (row, row_end) in row_ends.enumerate() {
344 let from = range
345 .start
346 .saturating_sub(line_start)
347 .clamp(row_start, row_end);
348 let to = range.end.saturating_sub(line_start).min(row_end);
349 let row_x = shaped.x_for_index(row_start);
350 let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
351 if from < to && right > left {
352 rects.push(Bounds::new(
353 origin
354 + point(
355 left - row_x - px(pad_x),
356 line_height * row as f32 + px(inset_y),
357 ),
358 size(
359 right - left + px(2.0 * pad_x),
360 line_height - px(2.0 * inset_y),
361 ),
362 ));
363 }
364 row_start = row_end;
365 }
366 origin.y += line.size(line_height).height;
367 line_start += line.len() + 1;
369 }
370 rects
371}