1use crate::{
2 App, Bounds, DevicePixels, Half, Hsla, LineLayout, Pixels, Point, RenderGlyphParams, Result,
3 ShapedGlyph, ShapedRun, SharedString, StrikethroughStyle, TextAlign, UnderlineStyle, Window,
4 WrapBoundary, WrappedLineLayout, black, fill, point, px, size,
5};
6use derive_more::{Deref, DerefMut};
7use smallvec::SmallVec;
8use std::sync::Arc;
9
10/// Pre-computed glyph data for efficient painting without per-glyph cache lookups.
11///
12/// This is produced by `ShapedLine::compute_glyph_raster_data` during prepaint
13/// and consumed by `ShapedLine::paint_with_raster_data` during paint.
14#[derive(Clone, Debug)]
15pub struct GlyphRasterData {
16 /// The raster bounds for each glyph, in paint order.
17 pub bounds: Vec<Bounds<DevicePixels>>,
18 /// The render params for each glyph (needed for sprite atlas lookup).
19 pub params: Vec<RenderGlyphParams>,
20}
21
22/// Set the text decoration for a run of text.
23#[derive(Debug, Clone)]
24pub struct DecorationRun {
25 /// The length of the run in utf-8 bytes.
26 pub len: u32,
27
28 /// The color for this run
29 pub color: Hsla,
30
31 /// The background color for this run
32 pub background_color: Option<Hsla>,
33
34 /// The underline style for this run
35 pub underline: Option<UnderlineStyle>,
36
37 /// The strikethrough style for this run
38 pub strikethrough: Option<StrikethroughStyle>,
39}
40
41/// A line of text that has been shaped and decorated.
42#[derive(Clone, Default, Debug, Deref, DerefMut)]
43pub struct ShapedLine {
44 #[deref]
45 #[deref_mut]
46 pub(crate) layout: Arc<LineLayout>,
47 /// The text that was shaped for this line.
48 pub text: SharedString,
49 pub(crate) decoration_runs: SmallVec<[DecorationRun; 32]>,
50}
51
52impl ShapedLine {
53 /// The length of the line in utf-8 bytes.
54 #[allow(clippy::len_without_is_empty)]
55 pub fn len(&self) -> usize {
56 self.layout.len
57 }
58
59 /// The width of the shaped line in pixels.
60 ///
61 /// This is the glyph advance width computed by the text shaping system and is useful for
62 /// incrementally advancing a "pen" when painting multiple fragments on the same row.
63 pub fn width(&self) -> Pixels {
64 self.layout.width
65 }
66
67 /// Override the len, useful if you're rendering text a
68 /// as text b (e.g. rendering invisibles).
69 pub fn with_len(mut self, len: usize) -> Self {
70 let layout = self.layout.as_ref();
71 self.layout = Arc::new(LineLayout {
72 font_size: layout.font_size,
73 width: layout.width,
74 ascent: layout.ascent,
75 descent: layout.descent,
76 runs: layout.runs.clone(),
77 len,
78 });
79 self
80 }
81
82 /// Paint the line of text to the window.
83 pub fn paint(
84 &self,
85 origin: Point<Pixels>,
86 line_height: Pixels,
87 align: TextAlign,
88 align_width: Option<Pixels>,
89 window: &mut Window,
90 cx: &mut App,
91 ) -> Result<()> {
92 paint_line(
93 origin,
94 &self.layout,
95 line_height,
96 align,
97 align_width,
98 &self.decoration_runs,
99 &[],
100 window,
101 cx,
102 )?;
103
104 Ok(())
105 }
106
107 /// Paint the background of the line to the window.
108 pub fn paint_background(
109 &self,
110 origin: Point<Pixels>,
111 line_height: Pixels,
112 align: TextAlign,
113 align_width: Option<Pixels>,
114 window: &mut Window,
115 cx: &mut App,
116 ) -> Result<()> {
117 paint_line_background(
118 origin,
119 &self.layout,
120 line_height,
121 align,
122 align_width,
123 &self.decoration_runs,
124 &[],
125 window,
126 cx,
127 )?;
128
129 Ok(())
130 }
131
132 /// Split this shaped line at a byte index, returning `(prefix, suffix)`.
133 ///
134 /// - `prefix` contains glyphs for bytes `[0, byte_index)` with original positions.
135 /// Its width equals the x-advance up to the split point.
136 /// - `suffix` contains glyphs for bytes `[byte_index, len)` with positions
137 /// shifted left so the first glyph starts at x=0, and byte indices rebased to 0.
138 /// - Decoration runs are partitioned at the boundary; a run that straddles it is
139 /// split into two with adjusted lengths.
140 /// - `font_size`, `ascent`, and `descent` are copied to both halves.
141 pub fn split_at(&self, byte_index: usize) -> (ShapedLine, ShapedLine) {
142 let x_offset = self.layout.x_for_index(byte_index);
143
144 // Partition glyph runs. A single run may contribute glyphs to both halves.
145 let mut left_runs = Vec::new();
146 let mut right_runs = Vec::new();
147
148 for run in &self.layout.runs {
149 let split_pos = run.glyphs.partition_point(|g| g.index < byte_index);
150
151 if split_pos > 0 {
152 left_runs.push(ShapedRun {
153 font_id: run.font_id,
154 glyphs: run.glyphs[..split_pos].to_vec(),
155 });
156 }
157
158 if split_pos < run.glyphs.len() {
159 let right_glyphs = run.glyphs[split_pos..]
160 .iter()
161 .map(|g| ShapedGlyph {
162 id: g.id,
163 position: point(g.position.x - x_offset, g.position.y),
164 index: g.index - byte_index,
165 is_emoji: g.is_emoji,
166 })
167 .collect();
168 right_runs.push(ShapedRun {
169 font_id: run.font_id,
170 glyphs: right_glyphs,
171 });
172 }
173 }
174
175 // Partition decoration runs. A run straddling the boundary is split into two.
176 let mut left_decorations = SmallVec::new();
177 let mut right_decorations = SmallVec::new();
178 let mut decoration_offset = 0u32;
179 let split_point = byte_index as u32;
180
181 for decoration in &self.decoration_runs {
182 let run_end = decoration_offset + decoration.len;
183
184 if run_end <= split_point {
185 left_decorations.push(decoration.clone());
186 } else if decoration_offset >= split_point {
187 right_decorations.push(decoration.clone());
188 } else {
189 let left_len = split_point - decoration_offset;
190 let right_len = run_end - split_point;
191 left_decorations.push(DecorationRun {
192 len: left_len,
193 color: decoration.color,
194 background_color: decoration.background_color,
195 underline: decoration.underline,
196 strikethrough: decoration.strikethrough,
197 });
198 right_decorations.push(DecorationRun {
199 len: right_len,
200 color: decoration.color,
201 background_color: decoration.background_color,
202 underline: decoration.underline,
203 strikethrough: decoration.strikethrough,
204 });
205 }
206
207 decoration_offset = run_end;
208 }
209
210 // Split text
211 let left_text = SharedString::new(self.text[..byte_index].to_string());
212 let right_text = SharedString::new(self.text[byte_index..].to_string());
213
214 let left_width = x_offset;
215 let right_width = self.layout.width - left_width;
216
217 let left = ShapedLine {
218 layout: Arc::new(LineLayout {
219 font_size: self.layout.font_size,
220 width: left_width,
221 ascent: self.layout.ascent,
222 descent: self.layout.descent,
223 runs: left_runs,
224 len: byte_index,
225 }),
226 text: left_text,
227 decoration_runs: left_decorations,
228 };
229
230 let right = ShapedLine {
231 layout: Arc::new(LineLayout {
232 font_size: self.layout.font_size,
233 width: right_width,
234 ascent: self.layout.ascent,
235 descent: self.layout.descent,
236 runs: right_runs,
237 len: self.layout.len - byte_index,
238 }),
239 text: right_text,
240 decoration_runs: right_decorations,
241 };
242
243 (left, right)
244 }
245}
246
247/// A line of text that has been shaped, decorated, and wrapped by the text layout system.
248#[derive(Default, Debug, Deref, DerefMut)]
249pub struct WrappedLine {
250 #[deref]
251 #[deref_mut]
252 pub(crate) layout: Arc<WrappedLineLayout>,
253 /// The text that was shaped for this line.
254 pub text: SharedString,
255 pub(crate) decoration_runs: Vec<DecorationRun>,
256}
257
258impl WrappedLine {
259 /// The length of the underlying, unwrapped layout, in utf-8 bytes.
260 #[allow(clippy::len_without_is_empty)]
261 pub fn len(&self) -> usize {
262 self.layout.len()
263 }
264
265 /// Paint this line of text to the window.
266 pub fn paint(
267 &self,
268 origin: Point<Pixels>,
269 line_height: Pixels,
270 align: TextAlign,
271 bounds: Option<Bounds<Pixels>>,
272 window: &mut Window,
273 cx: &mut App,
274 ) -> Result<()> {
275 let align_width = match bounds {
276 Some(bounds) => Some(bounds.size.width),
277 None => self.layout.wrap_width,
278 };
279
280 paint_line(
281 origin,
282 &self.layout.unwrapped_layout,
283 line_height,
284 align,
285 align_width,
286 &self.decoration_runs,
287 &self.wrap_boundaries,
288 window,
289 cx,
290 )?;
291
292 Ok(())
293 }
294
295 /// Paint the background of line of text to the window.
296 pub fn paint_background(
297 &self,
298 origin: Point<Pixels>,
299 line_height: Pixels,
300 align: TextAlign,
301 bounds: Option<Bounds<Pixels>>,
302 window: &mut Window,
303 cx: &mut App,
304 ) -> Result<()> {
305 let align_width = match bounds {
306 Some(bounds) => Some(bounds.size.width),
307 None => self.layout.wrap_width,
308 };
309
310 paint_line_background(
311 origin,
312 &self.layout.unwrapped_layout,
313 line_height,
314 align,
315 align_width,
316 &self.decoration_runs,
317 &self.wrap_boundaries,
318 window,
319 cx,
320 )?;
321
322 Ok(())
323 }
324}
325
326fn paint_line(
327 origin: Point<Pixels>,
328 layout: &LineLayout,
329 line_height: Pixels,
330 align: TextAlign,
331 align_width: Option<Pixels>,
332 decoration_runs: &[DecorationRun],
333 wrap_boundaries: &[WrapBoundary],
334 window: &mut Window,
335 cx: &mut App,
336) -> Result<()> {
337 let line_bounds = Bounds::new(
338 origin,
339 size(
340 layout.width,
341 line_height * (wrap_boundaries.len() as f32 + 1.),
342 ),
343 );
344 window.paint_layer(line_bounds, |window| {
345 let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
346 let baseline_offset = point(px(0.), padding_top + layout.ascent);
347 let mut decoration_runs = decoration_runs.iter();
348 let mut wraps = wrap_boundaries.iter().peekable();
349 let mut run_end = 0;
350 let mut color = black();
351 let mut current_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
352 let mut current_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
353 let text_system = cx.text_system().clone();
354 let mut glyph_origin = point(
355 aligned_origin_x(
356 origin,
357 align_width.unwrap_or(layout.width),
358 px(0.0),
359 &align,
360 layout,
361 wraps.peek(),
362 ),
363 origin.y,
364 );
365 let mut prev_glyph_position = Point::default();
366 let mut max_glyph_size = size(px(0.), px(0.));
367 let mut first_glyph_x = origin.x;
368 for (run_ix, run) in layout.runs.iter().enumerate() {
369 max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
370
371 for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
372 glyph_origin.x += glyph.position.x - prev_glyph_position.x;
373 if glyph_ix == 0 && run_ix == 0 {
374 first_glyph_x = glyph_origin.x;
375 }
376
377 if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
378 wraps.next();
379 if let Some((underline_origin, underline_style)) = current_underline.as_mut() {
380 if glyph_origin.x == underline_origin.x {
381 underline_origin.x -= max_glyph_size.width.half();
382 };
383 window.paint_underline(
384 *underline_origin,
385 glyph_origin.x - underline_origin.x,
386 underline_style,
387 );
388 if glyph.index < run_end {
389 underline_origin.x = origin.x;
390 underline_origin.y += line_height;
391 } else {
392 current_underline = None;
393 }
394 }
395 if let Some((strikethrough_origin, strikethrough_style)) =
396 current_strikethrough.as_mut()
397 {
398 if glyph_origin.x == strikethrough_origin.x {
399 strikethrough_origin.x -= max_glyph_size.width.half();
400 };
401 window.paint_strikethrough(
402 *strikethrough_origin,
403 glyph_origin.x - strikethrough_origin.x,
404 strikethrough_style,
405 );
406 if glyph.index < run_end {
407 strikethrough_origin.x = origin.x;
408 strikethrough_origin.y += line_height;
409 } else {
410 current_strikethrough = None;
411 }
412 }
413
414 glyph_origin.x = aligned_origin_x(
415 origin,
416 align_width.unwrap_or(layout.width),
417 glyph.position.x,
418 &align,
419 layout,
420 wraps.peek(),
421 );
422 glyph_origin.y += line_height;
423 }
424 prev_glyph_position = glyph.position;
425
426 let mut finished_underline: Option<(Point<Pixels>, UnderlineStyle)> = None;
427 let mut finished_strikethrough: Option<(Point<Pixels>, StrikethroughStyle)> = None;
428 if glyph.index >= run_end {
429 let mut style_run = decoration_runs.next();
430
431 // ignore style runs that apply to a partial glyph
432 while let Some(run) = style_run {
433 if glyph.index < run_end + (run.len as usize) {
434 break;
435 }
436 run_end += run.len as usize;
437 style_run = decoration_runs.next();
438 }
439
440 if let Some(style_run) = style_run {
441 if let Some((_, underline_style)) = &mut current_underline
442 && style_run.underline.as_ref() != Some(underline_style)
443 {
444 finished_underline = current_underline.take();
445 }
446 if let Some(run_underline) = style_run.underline.as_ref() {
447 current_underline.get_or_insert((
448 point(
449 glyph_origin.x,
450 glyph_origin.y + baseline_offset.y + (layout.descent * 0.618),
451 ),
452 UnderlineStyle {
453 color: Some(run_underline.color.unwrap_or(style_run.color)),
454 thickness: run_underline.thickness,
455 wavy: run_underline.wavy,
456 },
457 ));
458 }
459 if let Some((_, strikethrough_style)) = &mut current_strikethrough
460 && style_run.strikethrough.as_ref() != Some(strikethrough_style)
461 {
462 finished_strikethrough = current_strikethrough.take();
463 }
464 if let Some(run_strikethrough) = style_run.strikethrough.as_ref() {
465 current_strikethrough.get_or_insert((
466 point(
467 glyph_origin.x,
468 glyph_origin.y
469 + (((layout.ascent * 0.5) + baseline_offset.y) * 0.5),
470 ),
471 StrikethroughStyle {
472 color: Some(run_strikethrough.color.unwrap_or(style_run.color)),
473 thickness: run_strikethrough.thickness,
474 },
475 ));
476 }
477
478 run_end += style_run.len as usize;
479 color = style_run.color;
480 } else {
481 run_end = layout.len;
482 finished_underline = current_underline.take();
483 finished_strikethrough = current_strikethrough.take();
484 }
485 }
486
487 if let Some((mut underline_origin, underline_style)) = finished_underline {
488 if underline_origin.x == glyph_origin.x {
489 underline_origin.x -= max_glyph_size.width.half();
490 };
491 window.paint_underline(
492 underline_origin,
493 glyph_origin.x - underline_origin.x,
494 &underline_style,
495 );
496 }
497
498 if let Some((mut strikethrough_origin, strikethrough_style)) =
499 finished_strikethrough
500 {
501 if strikethrough_origin.x == glyph_origin.x {
502 strikethrough_origin.x -= max_glyph_size.width.half();
503 };
504 window.paint_strikethrough(
505 strikethrough_origin,
506 glyph_origin.x - strikethrough_origin.x,
507 &strikethrough_style,
508 );
509 }
510
511 let max_glyph_bounds = Bounds {
512 origin: glyph_origin,
513 size: max_glyph_size,
514 };
515
516 let content_mask = window.content_mask();
517 if max_glyph_bounds.intersects(&content_mask.bounds) {
518 let vertical_offset = point(px(0.0), glyph.position.y);
519 if glyph.is_emoji {
520 window.paint_emoji(
521 glyph_origin + baseline_offset + vertical_offset,
522 run.font_id,
523 glyph.id,
524 layout.font_size,
525 )?;
526 } else {
527 window.paint_glyph(
528 glyph_origin + baseline_offset + vertical_offset,
529 run.font_id,
530 glyph.id,
531 layout.font_size,
532 color,
533 )?;
534 }
535 }
536 }
537 }
538
539 let mut last_line_end_x = first_glyph_x + layout.width;
540 if let Some(boundary) = wrap_boundaries.last() {
541 let run = &layout.runs[boundary.run_ix];
542 let glyph = &run.glyphs[boundary.glyph_ix];
543 last_line_end_x -= glyph.position.x;
544 }
545
546 if let Some((mut underline_start, underline_style)) = current_underline.take() {
547 if last_line_end_x == underline_start.x {
548 underline_start.x -= max_glyph_size.width.half()
549 };
550 window.paint_underline(
551 underline_start,
552 last_line_end_x - underline_start.x,
553 &underline_style,
554 );
555 }
556
557 if let Some((mut strikethrough_start, strikethrough_style)) = current_strikethrough.take() {
558 if last_line_end_x == strikethrough_start.x {
559 strikethrough_start.x -= max_glyph_size.width.half()
560 };
561 window.paint_strikethrough(
562 strikethrough_start,
563 last_line_end_x - strikethrough_start.x,
564 &strikethrough_style,
565 );
566 }
567
568 Ok(())
569 })
570}
571
572fn paint_line_background(
573 origin: Point<Pixels>,
574 layout: &LineLayout,
575 line_height: Pixels,
576 align: TextAlign,
577 align_width: Option<Pixels>,
578 decoration_runs: &[DecorationRun],
579 wrap_boundaries: &[WrapBoundary],
580 window: &mut Window,
581 cx: &mut App,
582) -> Result<()> {
583 let line_bounds = Bounds::new(
584 origin,
585 size(
586 layout.width,
587 line_height * (wrap_boundaries.len() as f32 + 1.),
588 ),
589 );
590 window.paint_layer(line_bounds, |window| {
591 let mut decoration_runs = decoration_runs.iter();
592 let mut wraps = wrap_boundaries.iter().peekable();
593 let mut run_end = 0;
594 let mut current_background: Option<(Point<Pixels>, Hsla)> = None;
595 let text_system = cx.text_system().clone();
596 let mut glyph_origin = point(
597 aligned_origin_x(
598 origin,
599 align_width.unwrap_or(layout.width),
600 px(0.0),
601 &align,
602 layout,
603 wraps.peek(),
604 ),
605 origin.y,
606 );
607 let mut prev_glyph_position = Point::default();
608 let mut max_glyph_size = size(px(0.), px(0.));
609 for (run_ix, run) in layout.runs.iter().enumerate() {
610 max_glyph_size = text_system.bounding_box(run.font_id, layout.font_size).size;
611
612 for (glyph_ix, glyph) in run.glyphs.iter().enumerate() {
613 glyph_origin.x += glyph.position.x - prev_glyph_position.x;
614
615 if wraps.peek() == Some(&&WrapBoundary { run_ix, glyph_ix }) {
616 wraps.next();
617 if let Some((background_origin, background_color)) = current_background.as_mut()
618 {
619 if glyph_origin.x == background_origin.x {
620 background_origin.x -= max_glyph_size.width.half()
621 }
622 window.paint_quad(fill(
623 Bounds {
624 origin: *background_origin,
625 size: size(glyph_origin.x - background_origin.x, line_height),
626 },
627 *background_color,
628 ));
629 if glyph.index < run_end {
630 background_origin.x = origin.x;
631 background_origin.y += line_height;
632 } else {
633 current_background = None;
634 }
635 }
636
637 glyph_origin.x = aligned_origin_x(
638 origin,
639 align_width.unwrap_or(layout.width),
640 glyph.position.x,
641 &align,
642 layout,
643 wraps.peek(),
644 );
645 glyph_origin.y += line_height;
646 }
647 prev_glyph_position = glyph.position;
648
649 let mut finished_background: Option<(Point<Pixels>, Hsla)> = None;
650 if glyph.index >= run_end {
651 let mut style_run = decoration_runs.next();
652
653 // ignore style runs that apply to a partial glyph
654 while let Some(run) = style_run {
655 if glyph.index < run_end + (run.len as usize) {
656 break;
657 }
658 run_end += run.len as usize;
659 style_run = decoration_runs.next();
660 }
661
662 if let Some(style_run) = style_run {
663 if let Some((_, background_color)) = &mut current_background
664 && style_run.background_color.as_ref() != Some(background_color)
665 {
666 finished_background = current_background.take();
667 }
668 if let Some(run_background) = style_run.background_color {
669 current_background.get_or_insert((
670 point(glyph_origin.x, glyph_origin.y),
671 run_background,
672 ));
673 }
674 run_end += style_run.len as usize;
675 } else {
676 run_end = layout.len;
677 finished_background = current_background.take();
678 }
679 }
680
681 if let Some((mut background_origin, background_color)) = finished_background {
682 let mut width = glyph_origin.x - background_origin.x;
683 if background_origin.x == glyph_origin.x {
684 background_origin.x -= max_glyph_size.width.half();
685 };
686 window.paint_quad(fill(
687 Bounds {
688 origin: background_origin,
689 size: size(width, line_height),
690 },
691 background_color,
692 ));
693 }
694 }
695 }
696
697 let mut last_line_end_x = origin.x + layout.width;
698 if let Some(boundary) = wrap_boundaries.last() {
699 let run = &layout.runs[boundary.run_ix];
700 let glyph = &run.glyphs[boundary.glyph_ix];
701 last_line_end_x -= glyph.position.x;
702 }
703
704 if let Some((mut background_origin, background_color)) = current_background.take() {
705 if last_line_end_x == background_origin.x {
706 background_origin.x -= max_glyph_size.width.half()
707 };
708 window.paint_quad(fill(
709 Bounds {
710 origin: background_origin,
711 size: size(last_line_end_x - background_origin.x, line_height),
712 },
713 background_color,
714 ));
715 }
716
717 Ok(())
718 })
719}
720
721fn aligned_origin_x(
722 origin: Point<Pixels>,
723 align_width: Pixels,
724 last_glyph_x: Pixels,
725 align: &TextAlign,
726 layout: &LineLayout,
727 wrap_boundary: Option<&&WrapBoundary>,
728) -> Pixels {
729 let end_of_line = if let Some(WrapBoundary { run_ix, glyph_ix }) = wrap_boundary {
730 layout.runs[*run_ix].glyphs[*glyph_ix].position.x
731 } else {
732 layout.width
733 };
734
735 let line_width = end_of_line - last_glyph_x;
736
737 match align {
738 TextAlign::Left => origin.x,
739 TextAlign::Center => (origin.x * 2.0 + align_width - line_width) / 2.0,
740 TextAlign::Right => origin.x + align_width - line_width,
741 }
742}
743
744#[cfg(test)]
745mod tests {
746 use super::*;
747 use crate::{FontId, GlyphId};
748
749 /// Helper: build a ShapedLine from glyph descriptors without the platform text system.
750 /// Each glyph is described as (byte_index, x_position).
751 fn make_shaped_line(
752 text: &str,
753 glyphs: &[(usize, f32)],
754 width: f32,
755 decorations: &[DecorationRun],
756 ) -> ShapedLine {
757 let shaped_glyphs: Vec<ShapedGlyph> = glyphs
758 .iter()
759 .map(|&(index, x)| ShapedGlyph {
760 id: GlyphId(0),
761 position: point(px(x), px(0.0)),
762 index,
763 is_emoji: false,
764 })
765 .collect();
766
767 ShapedLine {
768 layout: Arc::new(LineLayout {
769 font_size: px(16.0),
770 width: px(width),
771 ascent: px(12.0),
772 descent: px(4.0),
773 runs: vec![ShapedRun {
774 font_id: FontId(0),
775 glyphs: shaped_glyphs,
776 }],
777 len: text.len(),
778 }),
779 text: SharedString::new(text.to_string()),
780 decoration_runs: SmallVec::from(decorations.to_vec()),
781 }
782 }
783
784 #[test]
785 fn test_split_at_invariants() {
786 // Split "abcdef" at every possible byte index and verify structural invariants.
787 let line = make_shaped_line(
788 "abcdef",
789 &[
790 (0, 0.0),
791 (1, 10.0),
792 (2, 20.0),
793 (3, 30.0),
794 (4, 40.0),
795 (5, 50.0),
796 ],
797 60.0,
798 &[],
799 );
800
801 for i in 0..=6 {
802 let (left, right) = line.split_at(i);
803
804 assert_eq!(
805 left.width() + right.width(),
806 line.width(),
807 "widths must sum at split={i}"
808 );
809 assert_eq!(
810 left.len() + right.len(),
811 line.len(),
812 "lengths must sum at split={i}"
813 );
814 assert_eq!(
815 format!("{}{}", left.text.as_ref(), right.text.as_ref()),
816 "abcdef",
817 "text must concatenate at split={i}"
818 );
819 assert_eq!(left.font_size, line.font_size, "font_size at split={i}");
820 assert_eq!(right.ascent, line.ascent, "ascent at split={i}");
821 assert_eq!(right.descent, line.descent, "descent at split={i}");
822 }
823
824 // Edge: split at 0 produces no left runs, full content on right
825 let (left, right) = line.split_at(0);
826 assert_eq!(left.runs.len(), 0);
827 assert_eq!(right.runs[0].glyphs.len(), 6);
828
829 // Edge: split at end produces full content on left, no right runs
830 let (left, right) = line.split_at(6);
831 assert_eq!(left.runs[0].glyphs.len(), 6);
832 assert_eq!(right.runs.len(), 0);
833 }
834
835 #[test]
836 fn test_split_at_glyph_rebasing() {
837 // Two font runs (simulating a font fallback boundary at byte 3):
838 // run A (FontId 0): glyphs at bytes 0,1,2 positions 0,10,20
839 // run B (FontId 1): glyphs at bytes 3,4,5 positions 30,40,50
840 // Successive splits simulate the incremental splitting done during wrap.
841 let line = ShapedLine {
842 layout: Arc::new(LineLayout {
843 font_size: px(16.0),
844 width: px(60.0),
845 ascent: px(12.0),
846 descent: px(4.0),
847 runs: vec![
848 ShapedRun {
849 font_id: FontId(0),
850 glyphs: vec![
851 ShapedGlyph {
852 id: GlyphId(0),
853 position: point(px(0.0), px(0.0)),
854 index: 0,
855 is_emoji: false,
856 },
857 ShapedGlyph {
858 id: GlyphId(0),
859 position: point(px(10.0), px(0.0)),
860 index: 1,
861 is_emoji: false,
862 },
863 ShapedGlyph {
864 id: GlyphId(0),
865 position: point(px(20.0), px(0.0)),
866 index: 2,
867 is_emoji: false,
868 },
869 ],
870 },
871 ShapedRun {
872 font_id: FontId(1),
873 glyphs: vec![
874 ShapedGlyph {
875 id: GlyphId(0),
876 position: point(px(30.0), px(0.0)),
877 index: 3,
878 is_emoji: false,
879 },
880 ShapedGlyph {
881 id: GlyphId(0),
882 position: point(px(40.0), px(0.0)),
883 index: 4,
884 is_emoji: false,
885 },
886 ShapedGlyph {
887 id: GlyphId(0),
888 position: point(px(50.0), px(0.0)),
889 index: 5,
890 is_emoji: false,
891 },
892 ],
893 },
894 ],
895 len: 6,
896 }),
897 text: "abcdef".into(),
898 decoration_runs: SmallVec::new(),
899 };
900
901 // First split at byte 2 — mid-run in run A
902 let (first, remainder) = line.split_at(2);
903 assert_eq!(first.text.as_ref(), "ab");
904 assert_eq!(first.runs.len(), 1);
905 assert_eq!(first.runs[0].font_id, FontId(0));
906
907 // Remainder "cdef" should have two runs: tail of A (1 glyph) + all of B (3 glyphs)
908 assert_eq!(remainder.text.as_ref(), "cdef");
909 assert_eq!(remainder.runs.len(), 2);
910 assert_eq!(remainder.runs[0].font_id, FontId(0));
911 assert_eq!(remainder.runs[0].glyphs.len(), 1);
912 assert_eq!(remainder.runs[0].glyphs[0].index, 0);
913 assert_eq!(remainder.runs[0].glyphs[0].position.x, px(0.0));
914 assert_eq!(remainder.runs[1].font_id, FontId(1));
915 assert_eq!(remainder.runs[1].glyphs[0].index, 1);
916 assert_eq!(remainder.runs[1].glyphs[0].position.x, px(10.0));
917
918 // Second split at byte 2 within remainder — crosses the run boundary
919 let (second, final_part) = remainder.split_at(2);
920 assert_eq!(second.text.as_ref(), "cd");
921 assert_eq!(final_part.text.as_ref(), "ef");
922 assert_eq!(final_part.runs[0].glyphs[0].index, 0);
923 assert_eq!(final_part.runs[0].glyphs[0].position.x, px(0.0));
924
925 // Widths must sum across all three pieces
926 assert_eq!(
927 first.width() + second.width() + final_part.width(),
928 line.width()
929 );
930 }
931
932 #[test]
933 fn test_split_at_decorations() {
934 // Three decoration runs: red [0..2), green [2..5), blue [5..6).
935 // Split at byte 3 — red goes entirely left, green straddles, blue goes entirely right.
936 let red = Hsla {
937 h: 0.0,
938 s: 1.0,
939 l: 0.5,
940 a: 1.0,
941 };
942 let green = Hsla {
943 h: 0.3,
944 s: 1.0,
945 l: 0.5,
946 a: 1.0,
947 };
948 let blue = Hsla {
949 h: 0.6,
950 s: 1.0,
951 l: 0.5,
952 a: 1.0,
953 };
954
955 let line = make_shaped_line(
956 "abcdef",
957 &[
958 (0, 0.0),
959 (1, 10.0),
960 (2, 20.0),
961 (3, 30.0),
962 (4, 40.0),
963 (5, 50.0),
964 ],
965 60.0,
966 &[
967 DecorationRun {
968 len: 2,
969 color: red,
970 background_color: None,
971 underline: None,
972 strikethrough: None,
973 },
974 DecorationRun {
975 len: 3,
976 color: green,
977 background_color: None,
978 underline: None,
979 strikethrough: None,
980 },
981 DecorationRun {
982 len: 1,
983 color: blue,
984 background_color: None,
985 underline: None,
986 strikethrough: None,
987 },
988 ],
989 );
990
991 let (left, right) = line.split_at(3);
992
993 // Left: red(2) + green(1) — green straddled, left portion has len 1
994 assert_eq!(left.decoration_runs.len(), 2);
995 assert_eq!(left.decoration_runs[0].len, 2);
996 assert_eq!(left.decoration_runs[0].color, red);
997 assert_eq!(left.decoration_runs[1].len, 1);
998 assert_eq!(left.decoration_runs[1].color, green);
999
1000 // Right: green(2) + blue(1) — green straddled, right portion has len 2
1001 assert_eq!(right.decoration_runs.len(), 2);
1002 assert_eq!(right.decoration_runs[0].len, 2);
1003 assert_eq!(right.decoration_runs[0].color, green);
1004 assert_eq!(right.decoration_runs[1].len, 1);
1005 assert_eq!(right.decoration_runs[1].color, blue);
1006 }
1007}