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