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