1use kozan_primitives::geometry::{Point, Size};
19use smallvec::SmallVec;
20use std::sync::Arc;
21
22use super::item::InlineItem;
23use super::measurer::{FontHeight, TextMeasurer};
24use crate::layout::fragment::{
25 BoxFragmentData, ChildFragment, Fragment, LineFragmentData, TextFragmentData,
26};
27use style::properties::longhands::text_wrap_mode::computed_value::T as TextWrapMode;
28use style::values::computed::box_::AlignmentBaseline;
29
30#[derive(Debug)]
32pub struct Line {
33 pub fragments: Vec<ChildFragment>,
34 pub width: f32,
35 pub height: f32,
36 pub baseline: f32,
37}
38
39pub fn break_into_lines(
48 items: &[InlineItem],
49 available_width: f32,
50 text_wrap_mode: TextWrapMode,
51 strut: &FontHeight,
52 measurer: &dyn TextMeasurer,
53) -> Vec<Line> {
54 let allow_wrap = text_wrap_mode == TextWrapMode::Wrap;
55
56 let mut lines: Vec<Line> = Vec::new();
57 let mut current_line = LineBuilder::new(strut);
58
59 for item in items {
60 match item {
61 InlineItem::ForcedBreak => {
62 lines.push(current_line.finish());
63 current_line = LineBuilder::new(strut);
64 }
65
66 InlineItem::Text {
67 content,
68 style,
69 measured_width,
70 measured_height,
71 baseline,
72 ..
73 } => {
74 let remaining_width = available_width - current_line.width;
75 let valign = style.clone_alignment_baseline();
76
77 if !allow_wrap || *measured_width <= remaining_width {
78 current_line.add_text(
80 content.clone(),
81 *measured_width,
82 *measured_height,
83 *baseline,
84 valign,
85 );
86 } else {
87 let font_size = style.clone_font_size().computed_size().px();
89 let mut text: &str = content;
90 let height = *measured_height;
91 let bl = *baseline;
92
93 loop {
94 let space_left = (available_width - current_line.width).max(0.0);
95
96 if let Some(split) = find_break_point(text, font_size, space_left, measurer)
97 {
98 let first = &text[..split];
100 let first_metrics = measurer.measure(first, font_size);
101 if !first.is_empty() {
102 current_line.add_text(
103 Arc::from(first),
104 first_metrics.width,
105 height,
106 bl,
107 valign,
108 );
109 }
110 lines.push(current_line.finish());
111 current_line = LineBuilder::new(strut);
112
113 text = text[split..].trim_start();
115 if text.is_empty() {
116 break;
117 }
118 } else {
120 if current_line.width > 0.0 {
122 lines.push(current_line.finish());
124 current_line = LineBuilder::new(strut);
125 } else {
127 let full_metrics = measurer.measure(text, font_size);
130 current_line.add_text(
131 Arc::from(text),
132 full_metrics.width,
133 height,
134 bl,
135 valign,
136 );
137 break;
138 }
139 }
140 }
141 }
142 }
143
144 InlineItem::AtomicInline {
145 width,
146 height,
147 baseline,
148 layout_id,
149 style,
150 } => {
151 if allow_wrap
152 && current_line.width + width > available_width
153 && current_line.width > 0.0
154 {
155 lines.push(current_line.finish());
156 current_line = LineBuilder::new(strut);
157 }
158
159 current_line.add_atomic(
160 *width,
161 *height,
162 *baseline,
163 *layout_id,
164 style.clone_alignment_baseline(),
165 );
166 }
167
168 InlineItem::OpenTag {
169 margin_inline_start,
170 border_inline_start,
171 padding_inline_start,
172 ..
173 } => {
174 current_line.width +=
175 margin_inline_start + border_inline_start + padding_inline_start;
176 }
177
178 InlineItem::CloseTag {
179 margin_inline_end,
180 border_inline_end,
181 padding_inline_end,
182 } => {
183 current_line.width += margin_inline_end + border_inline_end + padding_inline_end;
184 }
185 }
186 }
187
188 if current_line.width > 0.0 || current_line.items.is_empty() {
190 lines.push(current_line.finish());
191 }
192
193 lines
194}
195
196fn find_break_point(
206 text: &str,
207 font_size: f32,
208 available_width: f32,
209 measurer: &dyn TextMeasurer,
210) -> Option<usize> {
211 let mut last_break: Option<usize> = None;
215 let mut segment_start = 0;
216 let mut running_width = 0.0_f32;
217
218 for (i, ch) in text.char_indices() {
219 if ch.is_whitespace() {
220 let segment = &text[segment_start..i];
222 running_width += measurer.measure(segment, font_size).width;
223 if running_width <= available_width {
224 last_break = Some(i);
225 segment_start = i;
226 } else {
227 break;
228 }
229 }
230 }
231
232 last_break
233}
234
235#[must_use]
237pub fn lines_to_fragments(lines: Vec<Line>, available_width: f32) -> Vec<ChildFragment> {
238 let mut result = Vec::with_capacity(lines.len());
239 let mut block_offset: f32 = 0.0;
240
241 for line in lines {
242 let line_fragment = Fragment::new_line(
243 Size::new(available_width, line.height),
244 LineFragmentData {
245 children: line.fragments,
246 baseline: line.baseline,
247 },
248 );
249
250 result.push(ChildFragment {
251 offset: Point::new(0.0, block_offset),
252 fragment: line_fragment,
253 });
254
255 block_offset += line.height;
256 }
257
258 result
259}
260
261enum LineItemKind {
265 Text(Arc<str>),
267 Atomic(u32),
269}
270
271struct LineItem {
275 x: f32,
276 width: f32,
277 height: f32,
278 baseline: f32,
279 alignment_baseline: AlignmentBaseline,
280 kind: LineItemKind,
281}
282
283struct LineBuilder {
287 items: SmallVec<[LineItem; 8]>,
288 width: f32,
289 max_ascent: f32,
290 max_descent: f32,
291}
292
293impl LineBuilder {
294 fn new(strut: &FontHeight) -> Self {
295 Self {
296 items: SmallVec::new(),
297 width: 0.0,
298 max_ascent: strut.ascent,
299 max_descent: strut.descent,
300 }
301 }
302
303 fn add_text(
304 &mut self,
305 content: Arc<str>,
306 width: f32,
307 height: f32,
308 baseline: f32,
309 alignment_baseline: AlignmentBaseline,
310 ) {
311 self.items.push(LineItem {
312 x: self.width,
313 width,
314 height,
315 baseline,
316 alignment_baseline,
317 kind: LineItemKind::Text(content),
318 });
319 self.width += width;
320 self.update_line_metrics(height, baseline, alignment_baseline);
321 }
322
323 fn add_atomic(
324 &mut self,
325 width: f32,
326 height: f32,
327 baseline: f32,
328 layout_id: u32,
329 alignment_baseline: AlignmentBaseline,
330 ) {
331 self.items.push(LineItem {
332 x: self.width,
333 width,
334 height,
335 baseline,
336 alignment_baseline,
337 kind: LineItemKind::Atomic(layout_id),
338 });
339 self.width += width;
340 self.update_line_metrics(height, baseline, alignment_baseline);
341 }
342
343 fn update_line_metrics(&mut self, height: f32, baseline: f32, align: AlignmentBaseline) {
352 match align {
353 AlignmentBaseline::TextTop | AlignmentBaseline::TextBottom => {
358 let total = self.max_ascent + self.max_descent;
360 if height > total {
361 self.max_descent = self.max_descent.max(height - self.max_ascent);
362 }
363 }
364 _ => {
367 let descent = height - baseline;
368 self.max_ascent = self.max_ascent.max(baseline);
369 self.max_descent = self.max_descent.max(descent);
370 }
371 }
372 }
373
374 fn finish(self) -> Line {
375 let height = self.max_ascent + self.max_descent;
376 let baseline = self.max_ascent;
377
378 let mut fragments = Vec::with_capacity(self.items.len());
379
380 for item in &self.items {
381 let offset_y = match item.alignment_baseline {
382 AlignmentBaseline::Baseline => (baseline - item.baseline).max(0.0),
383 AlignmentBaseline::Middle => ((height - item.height) / 2.0).max(0.0),
384 AlignmentBaseline::TextTop => {
385 0.0
387 }
388 AlignmentBaseline::TextBottom => {
389 (height - item.height).max(0.0)
391 }
392 _ => (baseline - item.baseline).max(0.0),
395 };
396
397 let fragment = match &item.kind {
398 LineItemKind::Text(content) => Fragment::new_text(
399 Size::new(item.width, item.height),
400 TextFragmentData {
401 text_range: 0..content.len(),
402 baseline: item.baseline,
403 text: Some(content.clone()),
404 shaped_runs: Vec::new(),
405 },
406 ),
407 LineItemKind::Atomic(_layout_id) => Fragment::new_box(
408 Size::new(item.width, item.height),
409 BoxFragmentData {
410 scrollable_overflow: Size::new(item.width, item.height),
411 ..Default::default()
412 },
413 ),
414 };
415
416 fragments.push(ChildFragment {
417 offset: Point::new(item.x, offset_y),
418 fragment,
419 });
420 }
421
422 Line {
423 fragments,
424 width: self.width,
425 height,
426 baseline,
427 }
428 }
429}
430
431#[cfg(test)]
432mod tests {
433 use super::*;
434 use crate::layout::inline::FontSystem;
435 use crate::layout::inline::measurer::resolve_line_height;
436 use style::properties::ComputedValues;
437
438 fn initial_style() -> servo_arc::Arc<ComputedValues> {
439 crate::styling::initial_values_arc().clone()
440 }
441
442 fn default_strut() -> FontHeight {
443 let m = FontSystem::new();
444 let fm = m.font_metrics(16.0);
445 let style = initial_style();
446 let lh = resolve_line_height(&style.clone_line_height(), 16.0, &fm);
447 FontHeight::from_metrics_and_line_height(&fm, lh)
448 }
449
450 fn text_item(text: &str, width: f32) -> InlineItem {
451 let m = FontSystem::new();
452 let fm = m.font_metrics(16.0);
453 let style = initial_style();
454 let lh = resolve_line_height(&style.clone_line_height(), 16.0, &fm);
455 let fh = FontHeight::from_metrics_and_line_height(&fm, lh);
456
457 InlineItem::Text {
458 content: Arc::from(text),
459 style,
460 measured_width: width,
461 measured_height: fh.height(),
462 baseline: fh.ascent,
463 }
464 }
465
466 fn measured_text_item(text: &str) -> InlineItem {
468 let m = FontSystem::new();
469 let fm = m.font_metrics(16.0);
470 let style = initial_style();
471 let lh = resolve_line_height(&style.clone_line_height(), 16.0, &fm);
472 let fh = FontHeight::from_metrics_and_line_height(&fm, lh);
473 let width = m.measure(text, 16.0).width;
474
475 InlineItem::Text {
476 content: Arc::from(text),
477 style,
478 measured_width: width,
479 measured_height: fh.height(),
480 baseline: fh.ascent,
481 }
482 }
483
484 fn m() -> FontSystem {
485 FontSystem::new()
486 }
487
488 #[test]
489 fn single_line_fits() {
490 let strut = default_strut();
491 let items = vec![text_item("Hello", 50.0), text_item("World", 50.0)];
492 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
493 assert_eq!(lines.len(), 1);
494 assert_eq!(lines[0].width, 100.0);
495 }
496
497 #[test]
498 fn multiple_items_overflow() {
499 let strut = default_strut();
500 let items = vec![
504 measured_text_item("Hello"),
505 measured_text_item("World"),
506 measured_text_item("Foo"),
507 ];
508 let lines = break_into_lines(&items, 50.0, TextWrapMode::Wrap, &strut, &m());
509 assert_eq!(lines.len(), 3);
511 }
512
513 #[test]
514 fn no_wrap_single_line() {
515 let strut = default_strut();
516 let items = vec![text_item("Hello", 200.0), text_item("World", 200.0)];
517 let lines = break_into_lines(&items, 100.0, TextWrapMode::Nowrap, &strut, &m());
518 assert_eq!(lines.len(), 1);
519 assert_eq!(lines[0].width, 400.0);
520 }
521
522 #[test]
523 fn forced_break() {
524 let strut = default_strut();
525 let items = vec![
526 text_item("Line1", 50.0),
527 InlineItem::ForcedBreak,
528 text_item("Line2", 50.0),
529 ];
530 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
531 assert_eq!(lines.len(), 2);
532 }
533
534 #[test]
535 fn baseline_alignment() {
536 let strut = default_strut();
537 let items = vec![
538 InlineItem::Text {
539 content: Arc::from("Big"),
540 style: initial_style(),
541 measured_width: 50.0,
542 measured_height: 24.0,
543 baseline: 20.0,
544 },
545 InlineItem::Text {
546 content: Arc::from("Small"),
547 style: initial_style(),
548 measured_width: 30.0,
549 measured_height: 12.0,
550 baseline: 10.0,
551 },
552 ];
553 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
554 assert_eq!(lines.len(), 1);
555 let expected_ascent = strut.ascent.max(20.0).max(10.0);
558 let expected_descent = strut.descent.max(4.0).max(2.0);
559 assert_eq!(lines[0].height, expected_ascent + expected_descent);
560 assert_eq!(lines[0].baseline, expected_ascent);
561 }
562
563 #[test]
564 fn empty_line_uses_strut() {
565 let strut = FontHeight {
566 ascent: 14.0,
567 descent: 6.0,
568 };
569 let items = vec![InlineItem::ForcedBreak];
570 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
571 assert_eq!(lines.len(), 2);
572 assert_eq!(lines[0].height, 20.0);
573 assert_eq!(lines[0].baseline, 14.0);
574 }
575
576 #[test]
577 fn strut_sets_minimum_line_height() {
578 let strut = FontHeight {
579 ascent: 20.0,
580 descent: 10.0,
581 };
582 let items = vec![InlineItem::Text {
583 content: Arc::from("tiny"),
584 style: initial_style(),
585 measured_width: 30.0,
586 measured_height: 12.0,
587 baseline: 10.0,
588 }];
589 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
590 assert_eq!(lines.len(), 1);
591 assert_eq!(lines[0].height, 30.0);
592 assert_eq!(lines[0].baseline, 20.0);
593 }
594
595 #[test]
596 fn lines_to_fragments_stacks_vertically() {
597 let strut = default_strut();
598 let items = vec![
599 text_item("Line1", 50.0),
600 InlineItem::ForcedBreak,
601 text_item("Line2", 50.0),
602 ];
603 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
604 let line_height = lines[0].height;
605 let frags = lines_to_fragments(lines, 200.0);
606 assert_eq!(frags.len(), 2);
607 assert_eq!(frags[0].offset.y, 0.0);
608 assert_eq!(frags[1].offset.y, line_height);
609 }
610
611 #[test]
614 fn word_break_splits_at_space() {
615 let strut = default_strut();
617 let items = vec![measured_text_item("Hello World")];
618 let lines = break_into_lines(&items, 50.0, TextWrapMode::Wrap, &strut, &m());
621 assert_eq!(lines.len(), 2, "should split at space");
622 assert!(lines[0].width > 0.0);
624 assert!(lines[1].width > 0.0);
625 }
626
627 #[test]
628 fn word_break_no_space_overflows() {
629 let strut = default_strut();
631 let items = vec![measured_text_item("Superlongword")];
632 let lines = break_into_lines(&items, 30.0, TextWrapMode::Wrap, &strut, &m());
633 assert_eq!(lines.len(), 1, "no break point → overflow on single line");
634 assert!(lines[0].width > 30.0);
635 }
636
637 #[test]
638 fn word_break_multiple_words() {
639 let strut = default_strut();
641 let measurer = m();
642 let items = vec![measured_text_item("The quick brown fox")];
643 let one_word_w = measurer.measure("quick", 16.0).width;
645 let lines = break_into_lines(
646 &items,
647 one_word_w * 1.5,
648 TextWrapMode::Wrap,
649 &strut,
650 &measurer,
651 );
652 assert!(
653 lines.len() >= 2,
654 "should break into at least 2 lines, got {}",
655 lines.len()
656 );
657 }
658
659 #[test]
660 fn word_break_exact_fit() {
661 let strut = default_strut();
663 let measurer = m();
664 let items = vec![measured_text_item("AB CD")];
665 let ab_width = measurer.measure("AB", 16.0).width;
667 let lines = break_into_lines(&items, ab_width, TextWrapMode::Wrap, &strut, &measurer);
668 assert_eq!(lines.len(), 2);
669 }
670
671 #[test]
674 fn nowrap_no_wrapping() {
675 let strut = default_strut();
677 let items = vec![measured_text_item(
678 "This is a long line that exceeds the width",
679 )];
680 let lines = break_into_lines(&items, 50.0, TextWrapMode::Nowrap, &strut, &m());
681 assert_eq!(
682 lines.len(),
683 1,
684 "nowrap should not wrap, got {} lines",
685 lines.len()
686 );
687 }
688
689 #[test]
690 fn wrap_wraps_at_space() {
691 let strut = default_strut();
693 let items = vec![measured_text_item("Hello World")];
694 let lines = break_into_lines(&items, 50.0, TextWrapMode::Wrap, &strut, &m());
695 assert_eq!(
696 lines.len(),
697 2,
698 "wrap should wrap at space, got {} lines",
699 lines.len()
700 );
701 }
702
703 #[test]
704 fn wrap_preserves_forced_breaks() {
705 let strut = default_strut();
707 let items = vec![
708 measured_text_item("First"),
709 InlineItem::ForcedBreak,
710 measured_text_item("Second"),
711 ];
712 let lines = break_into_lines(&items, 800.0, TextWrapMode::Wrap, &strut, &m());
713 assert_eq!(
714 lines.len(),
715 2,
716 "wrap should preserve forced break, got {} lines",
717 lines.len()
718 );
719 }
720
721 #[test]
724 fn vertical_align_baseline_default() {
725 let strut = default_strut();
728 let items = vec![text_item("x", strut.height())];
729 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
730 assert_eq!(lines.len(), 1);
731 let frag = &lines[0].fragments[0];
733 assert!(
734 frag.offset.y.abs() < 0.5,
735 "baseline-aligned item should have offset_y ≈ 0, got {}",
736 frag.offset.y,
737 );
738 }
739
740 #[test]
741 fn taller_item_increases_line_height() {
742 let strut = default_strut();
744 let strut_height = strut.height();
745 let tall_height = strut_height * 2.0;
747 let tall_baseline = strut.ascent * 2.0;
748 let items = vec![InlineItem::Text {
749 content: Arc::from("tall"),
750 style: initial_style(),
751 measured_width: 50.0,
752 measured_height: tall_height,
753 baseline: tall_baseline,
754 }];
755 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
756 assert_eq!(lines.len(), 1);
757 assert!(
758 lines[0].height >= tall_height,
759 "line height must accommodate tall item: line={}, item={}",
760 lines[0].height,
761 tall_height,
762 );
763 }
764
765 #[test]
766 fn mixed_height_items_share_line() {
767 let strut = default_strut();
769 let small_baseline = strut.ascent * 0.5;
770 let large_baseline = strut.ascent * 1.5;
771 let items = vec![
772 InlineItem::Text {
773 content: Arc::from("small"),
774 style: initial_style(),
775 measured_width: 30.0,
776 measured_height: small_baseline + 5.0,
777 baseline: small_baseline,
778 },
779 InlineItem::Text {
780 content: Arc::from("large"),
781 style: initial_style(),
782 measured_width: 30.0,
783 measured_height: large_baseline + 8.0,
784 baseline: large_baseline,
785 },
786 ];
787 let lines = break_into_lines(&items, 200.0, TextWrapMode::Wrap, &strut, &m());
788 assert_eq!(lines.len(), 1, "both items should fit on one line");
789 assert!(
791 lines[0].baseline >= large_baseline,
792 "line baseline must accommodate the tallest item: {}",
793 lines[0].baseline,
794 );
795 }
796
797 #[test]
798 #[ignore] fn vertical_align_middle() {}
800
801 #[test]
802 #[ignore] fn vertical_align_super_shifts_up() {}
804
805 #[test]
806 #[ignore] fn vertical_align_sub_shifts_down() {}
808}