1use crate::{
2 color::Color,
3 fonts::{HighlightStyle, TextStyle},
4 geometry::{
5 rect::RectF,
6 vector::{vec2f, Vector2F},
7 },
8 json::{ToJson, Value},
9 text_layout::{Invisible, Line, RunStyle, ShapedBoundary},
10 AppContext, Element, FontCache, LayoutContext, SceneBuilder, SizeConstraint, TextLayoutCache,
11 View, ViewContext,
12};
13use log::warn;
14use serde_json::json;
15use std::{borrow::Cow, ops::Range, sync::Arc};
16
17pub struct Text {
18 text: Cow<'static, str>,
19 style: TextStyle,
20 soft_wrap: bool,
21 highlights: Option<Box<[(Range<usize>, HighlightStyle)]>>,
22 custom_runs: Option<(
23 Box<[Range<usize>]>,
24 Box<dyn FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext)>,
25 )>,
26}
27
28pub struct LayoutState {
29 shaped_lines: Vec<Line>,
30 wrap_boundaries: Vec<Vec<ShapedBoundary>>,
31 line_height: f32,
32}
33
34impl Text {
35 pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
36 Self {
37 text: text.into(),
38 style,
39 soft_wrap: true,
40 highlights: None,
41 custom_runs: None,
42 }
43 }
44
45 pub fn with_default_color(mut self, color: Color) -> Self {
46 self.style.color = color;
47 self
48 }
49
50 pub fn with_highlights(
51 mut self,
52 runs: impl Into<Box<[(Range<usize>, HighlightStyle)]>>,
53 ) -> Self {
54 self.highlights = Some(runs.into());
55 self
56 }
57
58 pub fn with_custom_runs(
59 mut self,
60 runs: impl Into<Box<[Range<usize>]>>,
61 callback: impl 'static + FnMut(usize, RectF, &mut SceneBuilder, &mut AppContext),
62 ) -> Self {
63 self.custom_runs = Some((runs.into(), Box::new(callback)));
64 self
65 }
66
67 pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
68 self.soft_wrap = soft_wrap;
69 self
70 }
71}
72
73impl<V: View> Element<V> for Text {
74 type LayoutState = LayoutState;
75 type PaintState = ();
76
77 fn layout(
78 &mut self,
79 constraint: SizeConstraint,
80 _: &mut V,
81 cx: &mut LayoutContext<V>,
82 ) -> (Vector2F, Self::LayoutState) {
83 // Convert the string and highlight ranges into an iterator of highlighted chunks.
84
85 let mut offset = 0;
86 let mut highlight_ranges = self
87 .highlights
88 .as_ref()
89 .map_or(Default::default(), AsRef::as_ref)
90 .iter()
91 .peekable();
92 let chunks = std::iter::from_fn(|| {
93 let result;
94 if let Some((range, highlight_style)) = highlight_ranges.peek() {
95 if offset < range.start {
96 result = Some((&self.text[offset..range.start], None));
97 offset = range.start;
98 } else if range.end <= self.text.len() {
99 result = Some((&self.text[range.clone()], Some(*highlight_style)));
100 highlight_ranges.next();
101 offset = range.end;
102 } else {
103 warn!(
104 "Highlight out of text range. Text len: {}, Highlight range: {}..{}",
105 self.text.len(),
106 range.start,
107 range.end
108 );
109 result = None;
110 }
111 } else if offset < self.text.len() {
112 result = Some((&self.text[offset..], None));
113 offset = self.text.len();
114 } else {
115 result = None;
116 }
117 result.map(|(chunk, style)| HighlightedChunk {
118 chunk,
119 style,
120 is_tab: false,
121 })
122 });
123
124 // Perform shaping on these highlighted chunks
125 let shaped_lines = layout_highlighted_chunks(
126 chunks,
127 &self.style,
128 cx.text_layout_cache(),
129 &cx.font_cache,
130 usize::MAX,
131 self.text.matches('\n').count() + 1,
132 false,
133 );
134
135 // If line wrapping is enabled, wrap each of the shaped lines.
136 let font_id = self.style.font_id;
137 let mut line_count = 0;
138 let mut max_line_width = 0_f32;
139 let mut wrap_boundaries = Vec::new();
140 let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
141 for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
142 if self.soft_wrap {
143 let boundaries = wrapper
144 .wrap_shaped_line(line, shaped_line, constraint.max.x())
145 .collect::<Vec<_>>();
146 line_count += boundaries.len() + 1;
147 wrap_boundaries.push(boundaries);
148 } else {
149 line_count += 1;
150 }
151 max_line_width = max_line_width.max(shaped_line.width());
152 }
153
154 let line_height = cx.font_cache.line_height(self.style.font_size);
155 let size = vec2f(
156 max_line_width
157 .ceil()
158 .max(constraint.min.x())
159 .min(constraint.max.x()),
160 (line_height * line_count as f32).ceil(),
161 );
162 (
163 size,
164 LayoutState {
165 shaped_lines,
166 wrap_boundaries,
167 line_height,
168 },
169 )
170 }
171
172 fn paint(
173 &mut self,
174 scene: &mut SceneBuilder,
175 bounds: RectF,
176 visible_bounds: RectF,
177 layout: &mut Self::LayoutState,
178 _: &mut V,
179 cx: &mut ViewContext<V>,
180 ) -> Self::PaintState {
181 let mut origin = bounds.origin();
182 let empty = Vec::new();
183 let mut callback = |_, _, _: &mut SceneBuilder, _: &mut AppContext| {};
184
185 let mouse_runs;
186 let custom_run_callback;
187 if let Some((runs, build_region)) = &mut self.custom_runs {
188 mouse_runs = runs.iter();
189 custom_run_callback = build_region.as_mut();
190 } else {
191 mouse_runs = [].iter();
192 custom_run_callback = &mut callback;
193 }
194 let mut custom_runs = mouse_runs.enumerate().peekable();
195
196 let mut offset = 0;
197 for (ix, line) in layout.shaped_lines.iter().enumerate() {
198 let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
199 let boundaries = RectF::new(
200 origin,
201 vec2f(
202 bounds.width(),
203 (wrap_boundaries.len() + 1) as f32 * layout.line_height,
204 ),
205 );
206
207 if boundaries.intersects(visible_bounds) {
208 if self.soft_wrap {
209 line.paint_wrapped(
210 scene,
211 origin,
212 visible_bounds,
213 layout.line_height,
214 wrap_boundaries,
215 cx,
216 );
217 } else {
218 line.paint(scene, origin, visible_bounds, layout.line_height, cx);
219 }
220 }
221
222 // Paint any custom runs that intersect this line.
223 let end_offset = offset + line.len();
224 if let Some((custom_run_ix, custom_run_range)) = custom_runs.peek().cloned() {
225 if custom_run_range.start < end_offset {
226 let mut current_custom_run = None;
227 if custom_run_range.start <= offset {
228 current_custom_run = Some((custom_run_ix, custom_run_range.end, origin));
229 }
230
231 let mut glyph_origin = origin;
232 let mut prev_position = 0.;
233 let mut wrap_boundaries = wrap_boundaries.iter().copied().peekable();
234 for (run_ix, glyph_ix, glyph) in
235 line.runs().iter().enumerate().flat_map(|(run_ix, run)| {
236 run.glyphs()
237 .iter()
238 .enumerate()
239 .map(move |(ix, glyph)| (run_ix, ix, glyph))
240 })
241 {
242 glyph_origin.set_x(glyph_origin.x() + glyph.position.x() - prev_position);
243 prev_position = glyph.position.x();
244
245 // If we've reached a soft wrap position, move down one line. If there
246 // is a custom run in-progress, paint it.
247 if wrap_boundaries
248 .peek()
249 .map_or(false, |b| b.run_ix == run_ix && b.glyph_ix == glyph_ix)
250 {
251 if let Some((run_ix, _, run_origin)) = &mut current_custom_run {
252 let bounds = RectF::from_points(
253 *run_origin,
254 glyph_origin + vec2f(0., layout.line_height),
255 );
256 custom_run_callback(*run_ix, bounds, scene, cx);
257 *run_origin =
258 vec2f(origin.x(), glyph_origin.y() + layout.line_height);
259 }
260 wrap_boundaries.next();
261 glyph_origin = vec2f(origin.x(), glyph_origin.y() + layout.line_height);
262 }
263
264 // If we've reached the end of the current custom run, paint it.
265 if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
266 if offset + glyph.index == run_end_offset {
267 current_custom_run.take();
268 let bounds = RectF::from_points(
269 run_origin,
270 glyph_origin + vec2f(0., layout.line_height),
271 );
272 custom_run_callback(run_ix, bounds, scene, cx);
273 custom_runs.next();
274 }
275
276 if let Some((_, run_range)) = custom_runs.peek() {
277 if run_range.start >= end_offset {
278 break;
279 }
280 if run_range.start == offset + glyph.index {
281 current_custom_run =
282 Some((run_ix, run_range.end, glyph_origin));
283 }
284 }
285 }
286
287 // If we've reached the start of a new custom run, start tracking it.
288 if let Some((run_ix, run_range)) = custom_runs.peek() {
289 if offset + glyph.index == run_range.start {
290 current_custom_run = Some((*run_ix, run_range.end, glyph_origin));
291 }
292 }
293 }
294
295 // If a custom run extends beyond the end of the line, paint it.
296 if let Some((run_ix, run_end_offset, run_origin)) = current_custom_run {
297 let line_end = glyph_origin + vec2f(line.width() - prev_position, 0.);
298 let bounds = RectF::from_points(
299 run_origin,
300 line_end + vec2f(0., layout.line_height),
301 );
302 custom_run_callback(run_ix, bounds, scene, cx);
303 if end_offset == run_end_offset {
304 custom_runs.next();
305 }
306 }
307 }
308 }
309
310 offset = end_offset + 1;
311 origin.set_y(boundaries.max_y());
312 }
313 }
314
315 fn rect_for_text_range(
316 &self,
317 _: Range<usize>,
318 _: RectF,
319 _: RectF,
320 _: &Self::LayoutState,
321 _: &Self::PaintState,
322 _: &V,
323 _: &ViewContext<V>,
324 ) -> Option<RectF> {
325 None
326 }
327
328 fn debug(
329 &self,
330 bounds: RectF,
331 _: &Self::LayoutState,
332 _: &Self::PaintState,
333 _: &V,
334 _: &ViewContext<V>,
335 ) -> Value {
336 json!({
337 "type": "Text",
338 "bounds": bounds.to_json(),
339 "text": &self.text,
340 "style": self.style.to_json(),
341 })
342 }
343}
344
345pub struct HighlightedChunk<'a> {
346 pub chunk: &'a str,
347 pub style: Option<HighlightStyle>,
348 pub is_tab: bool,
349}
350
351impl<'a> HighlightedChunk<'a> {
352 fn plain_str(str_symbols: &'a str) -> Self {
353 Self {
354 chunk: str_symbols,
355 style: None,
356 is_tab: str_symbols == "\t",
357 }
358 }
359}
360
361/// Perform text layout on a series of highlighted chunks of text.
362pub fn layout_highlighted_chunks<'a>(
363 chunks: impl Iterator<Item = HighlightedChunk<'a>>,
364 text_style: &TextStyle,
365 text_layout_cache: &TextLayoutCache,
366 font_cache: &Arc<FontCache>,
367 max_line_len: usize,
368 max_line_count: usize,
369 show_invisibles: bool,
370) -> Vec<Line> {
371 let mut layouts = Vec::with_capacity(max_line_count);
372 let mut line = String::new();
373 let mut invisibles = Vec::new();
374 let mut styles = Vec::new();
375 let mut row = 0;
376 let mut line_exceeded_max_len = false;
377 for highlighted_chunk in chunks.chain(std::iter::once(HighlightedChunk::plain_str("\n"))) {
378 for (ix, mut line_chunk) in highlighted_chunk.chunk.split('\n').enumerate() {
379 if ix > 0 {
380 let mut laid_out_line =
381 text_layout_cache.layout_str(&line, text_style.font_size, &styles);
382 laid_out_line.invisibles.extend(invisibles.drain(..));
383 layouts.push(laid_out_line);
384 line.clear();
385 styles.clear();
386 row += 1;
387 line_exceeded_max_len = false;
388 if row == max_line_count {
389 return layouts;
390 }
391 }
392
393 if !line_chunk.is_empty() && !line_exceeded_max_len {
394 let text_style = if let Some(style) = highlighted_chunk.style {
395 text_style
396 .clone()
397 .highlight(style, font_cache)
398 .map(Cow::Owned)
399 .unwrap_or_else(|_| Cow::Borrowed(text_style))
400 } else {
401 Cow::Borrowed(text_style)
402 };
403
404 if line.len() + line_chunk.len() > max_line_len {
405 let mut chunk_len = max_line_len - line.len();
406 while !line_chunk.is_char_boundary(chunk_len) {
407 chunk_len -= 1;
408 }
409 line_chunk = &line_chunk[..chunk_len];
410 line_exceeded_max_len = true;
411 }
412
413 styles.push((
414 line_chunk.len(),
415 RunStyle {
416 font_id: text_style.font_id,
417 color: text_style.color,
418 underline: text_style.underline,
419 },
420 ));
421 if show_invisibles && highlighted_chunk.is_tab {
422 invisibles.push(Invisible::Tab {
423 range: line.len()..line.len() + line_chunk.len(),
424 });
425 }
426 line.push_str(line_chunk);
427 }
428 }
429 }
430
431 layouts
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::{elements::Empty, fonts, AnyElement, AppContext, Entity, View, ViewContext};
438
439 #[crate::test(self)]
440 fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
441 cx.add_window(Default::default(), |cx| {
442 let mut view = TestView;
443 fonts::with_font_cache(cx.font_cache().clone(), || {
444 let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
445 let mut new_parents = Default::default();
446 let mut notify_views_if_parents_change = Default::default();
447 let mut layout_cx = LayoutContext::new(
448 cx,
449 &mut new_parents,
450 &mut notify_views_if_parents_change,
451 false,
452 );
453 let (_, state) = text.layout(
454 SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
455 &mut view,
456 &mut layout_cx,
457 );
458 assert_eq!(state.shaped_lines.len(), 2);
459 assert_eq!(state.wrap_boundaries.len(), 2);
460 });
461 view
462 });
463 }
464
465 struct TestView;
466
467 impl Entity for TestView {
468 type Event = ();
469 }
470
471 impl View for TestView {
472 fn ui_name() -> &'static str {
473 "TestView"
474 }
475
476 fn render(&mut self, _: &mut ViewContext<Self>) -> AnyElement<Self> {
477 Empty::new().into_any()
478 }
479 }
480}