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