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