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 Drawable, FontCache, SceneBuilder, SizeConstraint, TextLayoutCache, View, ViewContext,
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: Vec<(Range<usize>, HighlightStyle)>,
21}
22
23pub struct LayoutState {
24 shaped_lines: Vec<Line>,
25 wrap_boundaries: Vec<Vec<ShapedBoundary>>,
26 line_height: f32,
27}
28
29impl Text {
30 pub fn new<I: Into<Cow<'static, str>>>(text: I, style: TextStyle) -> Self {
31 Self {
32 text: text.into(),
33 style,
34 soft_wrap: true,
35 highlights: Vec::new(),
36 }
37 }
38
39 pub fn with_default_color(mut self, color: Color) -> Self {
40 self.style.color = color;
41 self
42 }
43
44 pub fn with_highlights(mut self, runs: Vec<(Range<usize>, HighlightStyle)>) -> Self {
45 self.highlights = runs;
46 self
47 }
48
49 pub fn with_soft_wrap(mut self, soft_wrap: bool) -> Self {
50 self.soft_wrap = soft_wrap;
51 self
52 }
53}
54
55impl<V: View> Drawable<V> for Text {
56 type LayoutState = LayoutState;
57 type PaintState = ();
58
59 fn layout(
60 &mut self,
61 constraint: SizeConstraint,
62 _: &mut V,
63 cx: &mut ViewContext<V>,
64 ) -> (Vector2F, Self::LayoutState) {
65 // Convert the string and highlight ranges into an iterator of highlighted chunks.
66
67 let mut offset = 0;
68 let mut highlight_ranges = self.highlights.iter().peekable();
69 let chunks = std::iter::from_fn(|| {
70 let result;
71 if let Some((range, highlight_style)) = highlight_ranges.peek() {
72 if offset < range.start {
73 result = Some((&self.text[offset..range.start], None));
74 offset = range.start;
75 } else if range.end <= self.text.len() {
76 result = Some((&self.text[range.clone()], Some(*highlight_style)));
77 highlight_ranges.next();
78 offset = range.end;
79 } else {
80 warn!(
81 "Highlight out of text range. Text len: {}, Highlight range: {}..{}",
82 self.text.len(),
83 range.start,
84 range.end
85 );
86 result = None;
87 }
88 } else if offset < self.text.len() {
89 result = Some((&self.text[offset..], None));
90 offset = self.text.len();
91 } else {
92 result = None;
93 }
94 result
95 });
96
97 // Perform shaping on these highlighted chunks
98 let shaped_lines = layout_highlighted_chunks(
99 chunks,
100 &self.style,
101 cx.text_layout_cache(),
102 &cx.font_cache,
103 usize::MAX,
104 self.text.matches('\n').count() + 1,
105 );
106
107 // If line wrapping is enabled, wrap each of the shaped lines.
108 let font_id = self.style.font_id;
109 let mut line_count = 0;
110 let mut max_line_width = 0_f32;
111 let mut wrap_boundaries = Vec::new();
112 let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
113 for (line, shaped_line) in self.text.split('\n').zip(&shaped_lines) {
114 if self.soft_wrap {
115 let boundaries = wrapper
116 .wrap_shaped_line(line, shaped_line, constraint.max.x())
117 .collect::<Vec<_>>();
118 line_count += boundaries.len() + 1;
119 wrap_boundaries.push(boundaries);
120 } else {
121 line_count += 1;
122 }
123 max_line_width = max_line_width.max(shaped_line.width());
124 }
125
126 let line_height = cx.font_cache.line_height(self.style.font_size);
127 let size = vec2f(
128 max_line_width
129 .ceil()
130 .max(constraint.min.x())
131 .min(constraint.max.x()),
132 (line_height * line_count as f32).ceil(),
133 );
134 (
135 size,
136 LayoutState {
137 shaped_lines,
138 wrap_boundaries,
139 line_height,
140 },
141 )
142 }
143
144 fn paint(
145 &mut self,
146 scene: &mut SceneBuilder,
147 bounds: RectF,
148 visible_bounds: RectF,
149 layout: &mut Self::LayoutState,
150 _: &mut V,
151 cx: &mut ViewContext<V>,
152 ) -> Self::PaintState {
153 let mut origin = bounds.origin();
154 let empty = Vec::new();
155 for (ix, line) in layout.shaped_lines.iter().enumerate() {
156 let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
157 let boundaries = RectF::new(
158 origin,
159 vec2f(
160 bounds.width(),
161 (wrap_boundaries.len() + 1) as f32 * layout.line_height,
162 ),
163 );
164
165 if boundaries.intersects(visible_bounds) {
166 if self.soft_wrap {
167 line.paint_wrapped(
168 scene,
169 origin,
170 visible_bounds,
171 layout.line_height,
172 wrap_boundaries.iter().copied(),
173 cx,
174 );
175 } else {
176 line.paint(scene, origin, visible_bounds, layout.line_height, cx);
177 }
178 }
179 origin.set_y(boundaries.max_y());
180 }
181 }
182
183 fn rect_for_text_range(
184 &self,
185 _: Range<usize>,
186 _: RectF,
187 _: RectF,
188 _: &Self::LayoutState,
189 _: &Self::PaintState,
190 _: &V,
191 _: &ViewContext<V>,
192 ) -> Option<RectF> {
193 None
194 }
195
196 fn debug(
197 &self,
198 bounds: RectF,
199 _: &Self::LayoutState,
200 _: &Self::PaintState,
201 _: &V,
202 _: &ViewContext<V>,
203 ) -> Value {
204 json!({
205 "type": "Text",
206 "bounds": bounds.to_json(),
207 "text": &self.text,
208 "style": self.style.to_json(),
209 })
210 }
211}
212
213/// Perform text layout on a series of highlighted chunks of text.
214pub fn layout_highlighted_chunks<'a>(
215 chunks: impl Iterator<Item = (&'a str, Option<HighlightStyle>)>,
216 text_style: &TextStyle,
217 text_layout_cache: &TextLayoutCache,
218 font_cache: &Arc<FontCache>,
219 max_line_len: usize,
220 max_line_count: usize,
221) -> Vec<Line> {
222 let mut layouts = Vec::with_capacity(max_line_count);
223 let mut line = String::new();
224 let mut styles = Vec::new();
225 let mut row = 0;
226 let mut line_exceeded_max_len = false;
227 for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
228 for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
229 if ix > 0 {
230 layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
231 line.clear();
232 styles.clear();
233 row += 1;
234 line_exceeded_max_len = false;
235 if row == max_line_count {
236 return layouts;
237 }
238 }
239
240 if !line_chunk.is_empty() && !line_exceeded_max_len {
241 let text_style = if let Some(style) = highlight_style {
242 text_style
243 .clone()
244 .highlight(style, font_cache)
245 .map(Cow::Owned)
246 .unwrap_or_else(|_| Cow::Borrowed(text_style))
247 } else {
248 Cow::Borrowed(text_style)
249 };
250
251 if line.len() + line_chunk.len() > max_line_len {
252 let mut chunk_len = max_line_len - line.len();
253 while !line_chunk.is_char_boundary(chunk_len) {
254 chunk_len -= 1;
255 }
256 line_chunk = &line_chunk[..chunk_len];
257 line_exceeded_max_len = true;
258 }
259
260 line.push_str(line_chunk);
261 styles.push((
262 line_chunk.len(),
263 RunStyle {
264 font_id: text_style.font_id,
265 color: text_style.color,
266 underline: text_style.underline,
267 },
268 ));
269 }
270 }
271 }
272
273 layouts
274}
275
276#[cfg(test)]
277mod tests {
278 use super::*;
279 use crate::{elements::Empty, fonts, AppContext, Element, Entity, View, ViewContext};
280
281 #[crate::test(self)]
282 fn test_soft_wrapping_with_carriage_returns(cx: &mut AppContext) {
283 let (window_id, root_view) = cx.add_window(Default::default(), |_| TestView);
284 fonts::with_font_cache(cx.font_cache().clone(), || {
285 root_view.update(cx, |view, cx| {
286 let mut text = Text::new("Hello\r\n", Default::default()).with_soft_wrap(true);
287 let (_, state) = text.layout(
288 SizeConstraint::new(Default::default(), vec2f(f32::INFINITY, f32::INFINITY)),
289 view,
290 cx,
291 );
292 assert_eq!(state.shaped_lines.len(), 2);
293 assert_eq!(state.wrap_boundaries.len(), 2);
294 });
295 });
296 }
297
298 struct TestView;
299
300 impl Entity for TestView {
301 type Event = ();
302 }
303
304 impl View for TestView {
305 fn ui_name() -> &'static str {
306 "TestView"
307 }
308
309 fn render(&mut self, _: &mut ViewContext<Self>) -> Element<Self> {
310 Empty::new().boxed()
311 }
312 }
313}