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