1use crate::{
11 engine::{LayoutEngine, LayoutResult, Line, LineMetrics, ParagraphMetrics},
12 linebreak::{LineBreak, LineBreaker},
13 options::LayoutOptions,
14};
15use oxitext_core::{FontVerticalMetrics, PositionedGlyph, Rgba8, ShapedGlyph, VerticalPosition};
16use std::sync::Arc;
17
18pub struct StyledRun {
24 pub glyphs: Vec<ShapedGlyph>,
26 pub metrics: FontVerticalMetrics,
28 pub px_size: f32,
30 pub color: Rgba8,
32 pub font_data: Arc<[u8]>,
34 pub vertical_position: VerticalPosition,
37}
38
39struct FlatEntry<'a> {
41 glyph: &'a ShapedGlyph,
42 font_data: &'a Arc<[u8]>,
43 run_ascent: f32,
44 run_descent: f32,
45 px_size: f32,
46 vertical_position: VerticalPosition,
47}
48
49impl LayoutEngine {
50 pub fn layout_styled_runs(
82 &mut self,
83 runs: &[StyledRun],
84 source_text: &str,
85 max_width: f32,
86 options: &LayoutOptions,
87 ) -> Result<LayoutResult, oxitext_core::OxiTextError> {
88 let mut flat: Vec<FlatEntry<'_>> = Vec::new();
92 for run in runs {
93 let run_ascent = run.metrics.ascent_px(run.px_size);
94 let run_descent = run.metrics.descent_px(run.px_size);
95 for g in &run.glyphs {
96 flat.push(FlatEntry {
97 glyph: g,
98 font_data: &run.font_data,
99 run_ascent,
100 run_descent,
101 px_size: run.px_size,
102 vertical_position: run.vertical_position,
103 });
104 }
105 }
106
107 if source_text != self.break_cache_text {
112 let b = LineBreaker::new(source_text).breaks().to_vec();
113 self.break_cache_text = source_text.to_owned();
114 self.break_cache_ops = b;
115 }
116 let breaks = &self.break_cache_ops;
117
118 let break_at = |off: usize| -> Option<LineBreak> {
119 breaks
120 .iter()
121 .find(|(pos, _)| *pos == off)
122 .map(|(_, kind)| kind.clone())
123 };
124
125 let wrap = max_width > 0.0;
129 let mut line_ranges: Vec<(usize, usize)> = Vec::new();
130 let mut line_start = 0usize;
131 let mut cursor_x = 0.0f32;
132 let mut last_safe: Option<usize> = None;
133 let mut width_at_break = 0.0f32;
134
135 let mut i = 0usize;
136 while i < flat.len() {
137 let adv = flat[i].glyph.x_advance;
138 let cluster_off = flat[i].glyph.cluster as usize;
139
140 if i > line_start {
141 let current_char = source_text
143 .get(cluster_off..)
144 .and_then(|s| s.chars().next());
145 let preceding_char = if cluster_off == 0 {
146 None
147 } else {
148 (1..=4usize).find_map(|back| {
149 let start = cluster_off.checked_sub(back)?;
150 if source_text.is_char_boundary(start) {
151 source_text[start..cluster_off].chars().next_back()
152 } else {
153 None
154 }
155 })
156 };
157 let zwj_precedes = preceding_char == Some('\u{200D}');
158 let is_zwnj = current_char == Some('\u{200C}');
159
160 let effective_break: Option<LineBreak> = if zwj_precedes {
161 None
162 } else if is_zwnj {
163 Some(LineBreak::Allowed)
164 } else {
165 break_at(cluster_off)
166 };
167
168 if let Some(kind) = effective_break {
169 if kind == LineBreak::Mandatory {
170 line_ranges.push((line_start, i));
171 line_start = i;
172 cursor_x = 0.0;
173 last_safe = None;
174 width_at_break = 0.0;
175 continue;
176 } else {
177 last_safe = Some(i);
178 width_at_break = cursor_x;
179 }
180 }
181 }
182
183 if wrap && cursor_x + adv > max_width && i > line_start {
184 if let Some(brk) = last_safe {
185 if brk > line_start {
186 line_ranges.push((line_start, brk));
187 line_start = brk;
188 cursor_x -= width_at_break;
189 last_safe = None;
190 width_at_break = 0.0;
191 continue;
192 }
193 }
194 line_ranges.push((line_start, i));
196 line_start = i;
197 cursor_x = 0.0;
198 last_safe = None;
199 width_at_break = 0.0;
200 continue;
201 }
202
203 cursor_x += adv;
204 i += 1;
205 }
206 if line_start < flat.len() {
207 line_ranges.push((line_start, flat.len()));
208 } else if line_ranges.is_empty() {
209 line_ranges.push((0, 0));
210 }
211 if line_ranges.is_empty() {
212 line_ranges.push((0, 0));
213 }
214
215 let mut positioned: Vec<PositionedGlyph> = Vec::with_capacity(flat.len());
219 let mut lines: Vec<Line> = Vec::with_capacity(line_ranges.len());
220 let mut cursor_y = 0.0f32;
221 let mut total_width = 0.0f32;
222
223 for &(start, end) in &line_ranges {
224 let line_slice = &flat[start..end];
225
226 let line_ascender = line_slice
228 .iter()
229 .map(|fe| fe.run_ascent)
230 .fold(0.0f32, f32::max);
231 let line_descender = line_slice
232 .iter()
233 .map(|fe| fe.run_descent)
234 .fold(0.0f32, f32::max);
235 let line_gap = if line_slice.is_empty() {
236 0.0
237 } else {
238 runs.iter()
240 .map(|r| r.metrics.line_gap_px(r.px_size))
241 .fold(0.0f32, f32::max)
242 };
243
244 let baseline_y = cursor_y + line_ascender;
245 let glyph_start = positioned.len();
246 let mut pen_x = 0.0f32;
247 let mut line_width = 0.0f32;
248
249 for fe in line_slice {
250 let y_shift = line_ascender - fe.run_ascent;
252 let vp_adjust = fe.vertical_position.baseline_adjustment(fe.px_size);
254 let glyph_y = baseline_y + y_shift + fe.glyph.y_offset - vp_adjust;
255 let effective_font_size = fe.vertical_position.effective_size(fe.px_size);
256
257 positioned.push(PositionedGlyph {
258 gid: fe.glyph.gid,
259 font_data: Arc::clone(fe.font_data),
260 pos: (pen_x + fe.glyph.x_offset, glyph_y),
261 font_size: effective_font_size,
262 advance_x: fe.glyph.x_advance,
263 cluster: fe.glyph.cluster,
264 });
265
266 pen_x += fe.glyph.x_advance;
267 if !fe.glyph.is_whitespace {
268 line_width = pen_x;
269 }
270 }
271
272 total_width = total_width.max(line_width);
273
274 lines.push(Line {
275 glyph_start,
276 glyph_end: positioned.len(),
277 metrics: LineMetrics {
278 ascent: line_ascender,
279 descent: line_descender,
280 leading: line_gap,
281 baseline_y,
282 width: line_width,
283 },
284 });
285
286 cursor_y += line_ascender + line_descender + line_gap + options.paragraph_spacing;
287 }
288
289 let total_height = cursor_y;
290
291 Ok(LayoutResult {
292 glyphs: positioned,
293 lines,
294 metrics: ParagraphMetrics {
295 total_height,
296 total_width,
297 line_count: line_ranges.len(),
298 overflow: false,
299 truncated: false,
300 },
301 decorations: Vec::new(),
302 inline_objects: Vec::new(),
303 })
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use oxitext_core::{FontVerticalMetrics, Rgba8, ShapedGlyph, VerticalPosition};
311 use std::sync::Arc;
312
313 fn make_shaped_glyph(cluster: u32, x_advance: f32, is_whitespace: bool) -> ShapedGlyph {
314 ShapedGlyph {
315 gid: 1,
316 x_advance,
317 y_advance: 0.0,
318 x_offset: 0.0,
319 y_offset: 0.0,
320 cluster,
321 is_whitespace,
322 unsafe_to_break: false,
323 }
324 }
325
326 fn make_metrics(ascender: i16, descender: i16) -> FontVerticalMetrics {
328 FontVerticalMetrics {
329 units_per_em: 1000,
330 ascender,
331 descender,
332 line_gap: 0,
333 }
334 }
335
336 #[test]
337 fn test_multistyle_baseline_alignment() {
338 let big_metrics = make_metrics(800, -200);
351 let small_metrics = make_metrics(800, -200);
352
353 let big_run = StyledRun {
354 glyphs: vec![
355 make_shaped_glyph(0, 20.0, false),
356 make_shaped_glyph(1, 20.0, false),
357 ],
358 metrics: big_metrics,
359 px_size: 20.0,
360 color: Rgba8::BLACK,
361 font_data: Arc::from(&[][..]),
362 vertical_position: VerticalPosition::Normal,
363 };
364 let small_run = StyledRun {
365 glyphs: vec![
366 make_shaped_glyph(2, 10.0, false),
367 make_shaped_glyph(3, 10.0, false),
368 ],
369 metrics: small_metrics,
370 px_size: 10.0,
371 color: Rgba8::new(255, 0, 0, 255),
372 font_data: Arc::from(&[][..]),
373 vertical_position: VerticalPosition::Normal,
374 };
375
376 let runs = [big_run, small_run];
377 let source_text = "abcd";
378 let mut engine = LayoutEngine::new();
379 let opts = LayoutOptions::default();
380 let result = engine
381 .layout_styled_runs(&runs, source_text, 1000.0, &opts)
382 .expect("layout_styled_runs");
383
384 assert_eq!(result.glyphs.len(), 4);
386 assert_eq!(result.lines.len(), 1);
387
388 let line = &result.lines[0];
390 assert!(
391 (line.metrics.ascent - 16.0).abs() < 1e-3,
392 "line ascent should be 16.0, got {}",
393 line.metrics.ascent
394 );
395
396 let baseline_y = line.metrics.baseline_y;
398
399 for gi in 0..2 {
401 assert!(
402 (result.glyphs[gi].pos.1 - baseline_y).abs() < 1e-3,
403 "big-run glyph {} y should equal baseline_y={}, got {}",
404 gi,
405 baseline_y,
406 result.glyphs[gi].pos.1
407 );
408 }
409
410 let expected_small_y = baseline_y + 8.0;
412 for gi in 2..4 {
413 assert!(
414 (result.glyphs[gi].pos.1 - expected_small_y).abs() < 1e-3,
415 "small-run glyph {} y should be baseline_y+8={}, got {}",
416 gi,
417 expected_small_y,
418 result.glyphs[gi].pos.1
419 );
420 }
421 }
422
423 #[test]
424 fn test_multistyle_single_run_no_shift() {
425 let m = make_metrics(800, -200);
427 let run = StyledRun {
428 glyphs: vec![
429 make_shaped_glyph(0, 10.0, false),
430 make_shaped_glyph(1, 10.0, false),
431 make_shaped_glyph(2, 10.0, false),
432 ],
433 metrics: m,
434 px_size: 16.0,
435 color: Rgba8::BLACK,
436 font_data: Arc::from(&[][..]),
437 vertical_position: VerticalPosition::Normal,
438 };
439
440 let source = "abc";
441 let mut engine = LayoutEngine::new();
442 let opts = LayoutOptions::default();
443 let result = engine
444 .layout_styled_runs(&[run], source, 1000.0, &opts)
445 .expect("layout_styled_runs single");
446
447 let baseline_y = result.lines[0].metrics.baseline_y;
448 for g in &result.glyphs {
449 assert!(
450 (g.pos.1 - baseline_y).abs() < 1e-3,
451 "single-run glyph y should equal baseline_y={}, got {}",
452 baseline_y,
453 g.pos.1
454 );
455 }
456 }
457
458 #[test]
459 fn test_multistyle_wraps_at_max_width() {
460 let m = make_metrics(800, -200);
462 let run = StyledRun {
463 glyphs: (0..4)
464 .map(|i| make_shaped_glyph(i as u32, 10.0, false))
465 .collect(),
466 metrics: m,
467 px_size: 12.0,
468 color: Rgba8::BLACK,
469 font_data: Arc::from(&[][..]),
470 vertical_position: VerticalPosition::Normal,
471 };
472
473 let source = "abcd";
474 let mut engine = LayoutEngine::new();
475 let opts = LayoutOptions::default();
476 let result = engine
477 .layout_styled_runs(&[run], source, 25.0, &opts)
478 .expect("layout_styled_runs wrap");
479
480 assert!(result.lines.len() >= 2, "expected wrapping");
481 assert_eq!(result.glyphs.len(), 4);
482 }
483
484 #[test]
485 fn test_multistyle_empty_runs() {
486 let mut engine = LayoutEngine::new();
488 let opts = LayoutOptions::default();
489 let result = engine
490 .layout_styled_runs(&[], "", 400.0, &opts)
491 .expect("layout_styled_runs empty");
492 assert_eq!(result.glyphs.len(), 0);
493 assert_eq!(result.lines.len(), 1);
494 }
495}