1use std::{ops::Range, sync::Arc};
2
3use crate::{
4 color::Color,
5 fonts::{HighlightStyle, TextStyle},
6 geometry::{
7 rect::RectF,
8 vector::{vec2f, Vector2F},
9 },
10 json::{ToJson, Value},
11 text_layout::{Line, RunStyle, ShapedBoundary},
12 DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
13 SizeConstraint, TextLayoutCache,
14};
15use serde_json::json;
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 let mut offset = 0;
67 let mut highlight_ranges = self.highlights.iter().peekable();
68 let chunks = std::iter::from_fn(|| {
69 let result;
70 if let Some((range, highlight_style)) = highlight_ranges.peek() {
71 if offset < range.start {
72 result = Some((
73 &self.text[offset..range.start],
74 HighlightStyle::from(&self.style),
75 ));
76 offset = range.start;
77 } else {
78 result = Some((&self.text[range.clone()], *highlight_style));
79 highlight_ranges.next();
80 offset = range.end;
81 }
82 } else if offset < self.text.len() {
83 result = Some((&self.text[offset..], HighlightStyle::from(&self.style)));
84 offset = self.text.len();
85 } else {
86 result = None;
87 }
88 result
89 });
90
91 // Perform shaping on these highlighted chunks
92 let shaped_lines = layout_highlighted_chunks(
93 chunks,
94 &self.style,
95 cx.text_layout_cache,
96 &cx.font_cache,
97 usize::MAX,
98 self.text.matches('\n').count() + 1,
99 );
100
101 // If line wrapping is enabled, wrap each of the shaped lines.
102 let font_id = self.style.font_id;
103 let mut line_count = 0;
104 let mut max_line_width = 0_f32;
105 let mut wrap_boundaries = Vec::new();
106 let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
107 for (line, shaped_line) in self.text.lines().zip(&shaped_lines) {
108 if self.soft_wrap {
109 let boundaries = wrapper
110 .wrap_shaped_line(line, shaped_line, constraint.max.x())
111 .collect::<Vec<_>>();
112 line_count += boundaries.len() + 1;
113 wrap_boundaries.push(boundaries);
114 } else {
115 line_count += 1;
116 }
117 max_line_width = max_line_width.max(shaped_line.width());
118 }
119
120 let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
121 let size = vec2f(
122 max_line_width
123 .ceil()
124 .max(constraint.min.x())
125 .min(constraint.max.x()),
126 (line_height * line_count as f32).ceil(),
127 );
128 (
129 size,
130 LayoutState {
131 shaped_lines,
132 wrap_boundaries,
133 line_height,
134 },
135 )
136 }
137
138 fn paint(
139 &mut self,
140 bounds: RectF,
141 visible_bounds: RectF,
142 layout: &mut Self::LayoutState,
143 cx: &mut PaintContext,
144 ) -> Self::PaintState {
145 let mut origin = bounds.origin();
146 let empty = Vec::new();
147 for (ix, line) in layout.shaped_lines.iter().enumerate() {
148 let wrap_boundaries = layout.wrap_boundaries.get(ix).unwrap_or(&empty);
149 let boundaries = RectF::new(
150 origin,
151 vec2f(
152 bounds.width(),
153 (wrap_boundaries.len() + 1) as f32 * layout.line_height,
154 ),
155 );
156
157 if boundaries.intersects(visible_bounds) {
158 if self.soft_wrap {
159 line.paint_wrapped(
160 origin,
161 visible_bounds,
162 layout.line_height,
163 wrap_boundaries.iter().copied(),
164 cx,
165 );
166 } else {
167 line.paint(origin, visible_bounds, layout.line_height, cx);
168 }
169 }
170 origin.set_y(boundaries.max_y());
171 }
172 }
173
174 fn dispatch_event(
175 &mut self,
176 _: &Event,
177 _: RectF,
178 _: &mut Self::LayoutState,
179 _: &mut Self::PaintState,
180 _: &mut EventContext,
181 ) -> bool {
182 false
183 }
184
185 fn debug(
186 &self,
187 bounds: RectF,
188 _: &Self::LayoutState,
189 _: &Self::PaintState,
190 _: &DebugContext,
191 ) -> Value {
192 json!({
193 "type": "Text",
194 "bounds": bounds.to_json(),
195 "text": &self.text,
196 "style": self.style.to_json(),
197 })
198 }
199}
200
201/// Perform text layout on a series of highlighted chunks of text.
202pub fn layout_highlighted_chunks<'a>(
203 chunks: impl Iterator<Item = (&'a str, HighlightStyle)>,
204 text_style: &'a TextStyle,
205 text_layout_cache: &'a TextLayoutCache,
206 font_cache: &'a Arc<FontCache>,
207 max_line_len: usize,
208 max_line_count: usize,
209) -> Vec<Line> {
210 let mut layouts = Vec::with_capacity(max_line_count);
211 let mut prev_font_properties = text_style.font_properties.clone();
212 let mut prev_font_id = text_style.font_id;
213 let mut line = String::new();
214 let mut styles = Vec::new();
215 let mut row = 0;
216 let mut line_exceeded_max_len = false;
217 for (chunk, highlight_style) in chunks.chain([("\n", Default::default())]) {
218 for (ix, mut line_chunk) in chunk.split('\n').enumerate() {
219 if ix > 0 {
220 layouts.push(text_layout_cache.layout_str(&line, text_style.font_size, &styles));
221 line.clear();
222 styles.clear();
223 row += 1;
224 line_exceeded_max_len = false;
225 if row == max_line_count {
226 return layouts;
227 }
228 }
229
230 if !line_chunk.is_empty() && !line_exceeded_max_len {
231 // Avoid a lookup if the font properties match the previous ones.
232 let font_id = if highlight_style.font_properties == prev_font_properties {
233 prev_font_id
234 } else {
235 font_cache
236 .select_font(text_style.font_family_id, &highlight_style.font_properties)
237 .unwrap_or(text_style.font_id)
238 };
239
240 if line.len() + line_chunk.len() > max_line_len {
241 let mut chunk_len = max_line_len - line.len();
242 while !line_chunk.is_char_boundary(chunk_len) {
243 chunk_len -= 1;
244 }
245 line_chunk = &line_chunk[..chunk_len];
246 line_exceeded_max_len = true;
247 }
248
249 line.push_str(line_chunk);
250 styles.push((
251 line_chunk.len(),
252 RunStyle {
253 font_id,
254 color: highlight_style.color,
255 underline: highlight_style.underline,
256 },
257 ));
258 prev_font_id = font_id;
259 prev_font_properties = highlight_style.font_properties;
260 }
261 }
262 }
263
264 layouts
265}