1use rdocx_oxml::borders::CT_TabStop;
6use rdocx_oxml::shared::{ST_Jc, ST_TabJc, ST_Underline};
7use rdocx_oxml::units::Twips;
8
9use crate::error::Result;
10use crate::font::FontManager;
11use crate::output::{Color, FieldKind, FontId};
12
13#[derive(Debug, Clone)]
15pub enum InlineItem {
16 Text(TextSegment),
18 Tab,
20 LineBreak,
22 PageBreak,
24 ColumnBreak,
26 Image {
28 width: f64,
29 height: f64,
30 embed_id: String,
31 },
32 Marker(TextSegment),
34}
35
36#[derive(Debug, Clone)]
38pub struct TextSegment {
39 pub text: String,
40 pub font_id: FontId,
41 pub font_size: f64,
42 pub glyph_ids: Vec<u16>,
43 pub advances: Vec<f64>,
44 pub width: f64,
45 pub ascent: f64,
46 pub descent: f64,
47 pub color: Color,
48 pub bold: bool,
49 pub italic: bool,
50 pub underline: Option<ST_Underline>,
52 pub strike: bool,
54 pub dstrike: bool,
56 pub highlight: Option<Color>,
58 pub baseline_offset: f64,
60 pub hyperlink_url: Option<String>,
62 pub field_kind: Option<FieldKind>,
64 pub footnote_id: Option<i32>,
66}
67
68#[derive(Debug, Clone)]
70pub enum LineItem {
71 Text(TextSegment),
72 Tab {
73 width: f64,
74 leader: Option<TextSegment>,
76 },
77 Image {
78 width: f64,
79 height: f64,
80 embed_id: String,
81 },
82 Marker(TextSegment),
83}
84
85impl LineItem {
86 pub fn width(&self) -> f64 {
87 match self {
88 LineItem::Text(seg) => seg.width,
89 LineItem::Tab { width, .. } => *width,
90 LineItem::Image { width, .. } => *width,
91 LineItem::Marker(seg) => seg.width,
92 }
93 }
94}
95
96#[derive(Debug, Clone)]
98pub struct LayoutLine {
99 pub items: Vec<LineItem>,
100 pub width: f64,
102 pub ascent: f64,
104 pub descent: f64,
106 pub height: f64,
108 pub indent_left: f64,
110 pub available_width: f64,
112 pub is_last: bool,
114}
115
116pub struct LineBreakParams {
118 pub available_width: f64,
120 pub ind_left: f64,
122 pub ind_right: f64,
124 pub ind_first_line: f64,
126 pub ind_hanging: f64,
128 pub tab_stops: Vec<CT_TabStop>,
130 pub line_spacing: Option<Twips>,
132 pub line_rule: Option<String>,
134 pub jc: Option<ST_Jc>,
136}
137
138impl Default for LineBreakParams {
139 fn default() -> Self {
140 LineBreakParams {
141 available_width: 468.0, ind_left: 0.0,
143 ind_right: 0.0,
144 ind_first_line: 0.0,
145 ind_hanging: 0.0,
146 tab_stops: Vec::new(),
147 line_spacing: None,
148 line_rule: None,
149 jc: None,
150 }
151 }
152}
153
154pub fn break_into_lines(
156 items: &[InlineItem],
157 params: &LineBreakParams,
158 fm: &FontManager,
159) -> Result<Vec<LayoutLine>> {
160 if items.is_empty() {
161 return Ok(vec![LayoutLine {
163 items: Vec::new(),
164 width: 0.0,
165 ascent: 0.0,
166 descent: 0.0,
167 height: compute_line_height(0.0, 0.0, params),
168 indent_left: params.ind_left + params.ind_first_line,
169 available_width: params.available_width,
170 is_last: true,
171 }]);
172 }
173
174 let mut lines: Vec<LayoutLine> = Vec::new();
175 let mut current_items: Vec<LineItem> = Vec::new();
176 let mut current_width: f64 = 0.0;
177 let mut current_ascent: f64 = 0.0;
178 let mut current_descent: f64 = 0.0;
179 let mut is_first_line = true;
180
181 let first_line_width = compute_first_line_width(params);
182 let subsequent_line_width = compute_subsequent_line_width(params);
183
184 let mut line_avail = first_line_width;
185
186 let mut font_ctx: Option<(FontId, f64)> = None;
188 for item in items {
190 if let InlineItem::Text(seg) | InlineItem::Marker(seg) = item {
191 font_ctx = Some((seg.font_id, seg.font_size));
192 break;
193 }
194 }
195
196 let segments = build_breakable_segments(items);
198
199 for seg in &segments {
200 match seg {
201 BreakableSegment::Items(seg_items) => {
202 let seg_width: f64 = seg_items.iter().map(inline_item_width).sum();
203
204 if !current_items.is_empty() && current_width + seg_width > line_avail + 0.01 {
205 let indent = if is_first_line {
207 first_line_indent(params)
208 } else {
209 subsequent_line_indent(params)
210 };
211 lines.push(LayoutLine {
212 items: std::mem::take(&mut current_items),
213 width: current_width,
214 ascent: current_ascent,
215 descent: current_descent,
216 height: compute_line_height(current_ascent, current_descent, params),
217 indent_left: indent,
218 available_width: line_avail,
219 is_last: false,
220 });
221 current_width = 0.0;
222 current_ascent = 0.0;
223 current_descent = 0.0;
224 is_first_line = false;
225 line_avail = subsequent_line_width;
226 }
227
228 for item in seg_items {
230 let (w, a, d) = item_metrics(item);
231 current_width += w;
232 if a > current_ascent {
233 current_ascent = a;
234 }
235 if d > current_descent {
236 current_descent = d;
237 }
238 if let InlineItem::Text(seg) | InlineItem::Marker(seg) = item {
240 font_ctx = Some((seg.font_id, seg.font_size));
241 }
242 current_items.push(inline_to_line_item(
243 item,
244 current_width,
245 ¶ms.tab_stops,
246 fm,
247 font_ctx,
248 ));
249 }
250 }
251 BreakableSegment::ForcedBreak(break_type) => {
252 let indent = if is_first_line {
253 first_line_indent(params)
254 } else {
255 subsequent_line_indent(params)
256 };
257 lines.push(LayoutLine {
258 items: std::mem::take(&mut current_items),
259 width: current_width,
260 ascent: current_ascent,
261 descent: current_descent,
262 height: compute_line_height(current_ascent, current_descent, params),
263 indent_left: indent,
264 available_width: line_avail,
265 is_last: matches!(break_type, ForcedBreakType::Page | ForcedBreakType::Column),
266 });
267 current_width = 0.0;
268 current_ascent = 0.0;
269 current_descent = 0.0;
270 is_first_line = false;
271 line_avail = subsequent_line_width;
272 }
273 }
274 }
275
276 let indent = if is_first_line {
278 first_line_indent(params)
279 } else {
280 subsequent_line_indent(params)
281 };
282 lines.push(LayoutLine {
283 items: current_items,
284 width: current_width,
285 ascent: current_ascent,
286 descent: current_descent,
287 height: compute_line_height(current_ascent, current_descent, params),
288 indent_left: indent,
289 available_width: line_avail,
290 is_last: true,
291 });
292
293 Ok(lines)
294}
295
296#[derive(Debug)]
299enum BreakableSegment {
300 Items(Vec<InlineItem>),
302 ForcedBreak(ForcedBreakType),
304}
305
306#[derive(Debug)]
307enum ForcedBreakType {
308 Line,
309 Page,
310 Column,
311}
312
313fn build_breakable_segments(items: &[InlineItem]) -> Vec<BreakableSegment> {
319 let mut segments = Vec::new();
320 let mut current_group: Vec<InlineItem> = Vec::new();
321
322 for item in items {
323 match item {
324 InlineItem::LineBreak => {
325 if !current_group.is_empty() {
326 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
327 }
328 segments.push(BreakableSegment::ForcedBreak(ForcedBreakType::Line));
329 }
330 InlineItem::PageBreak => {
331 if !current_group.is_empty() {
332 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
333 }
334 segments.push(BreakableSegment::ForcedBreak(ForcedBreakType::Page));
335 }
336 InlineItem::ColumnBreak => {
337 if !current_group.is_empty() {
338 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
339 }
340 segments.push(BreakableSegment::ForcedBreak(ForcedBreakType::Column));
341 }
342 InlineItem::Tab => {
343 if !current_group.is_empty() {
345 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
346 }
347 segments.push(BreakableSegment::Items(vec![item.clone()]));
348 }
349 InlineItem::Text(seg) => {
350 let breaks = split_text_at_break_opportunities(seg);
352
353 for tb in &breaks {
354 let chunk = &seg.text[tb.start..tb.end];
355 if chunk.is_empty() {
356 continue;
357 }
358
359 if !current_group.is_empty() && chunk.starts_with(|c: char| c.is_whitespace()) {
361 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
362 }
363
364 let sub_item = split_text_subsegment(seg, tb.start, tb.end);
366 current_group.push(sub_item);
367
368 if tb.is_break {
369 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
370 }
371 }
372
373 if !current_group.is_empty() {
375 segments.push(BreakableSegment::Items(std::mem::take(&mut current_group)));
376 }
377 }
378 InlineItem::Marker(_) | InlineItem::Image { .. } => {
379 current_group.push(item.clone());
380 }
381 }
382 }
383
384 if !current_group.is_empty() {
385 segments.push(BreakableSegment::Items(current_group));
386 }
387
388 segments
389}
390
391fn split_text_subsegment(seg: &TextSegment, byte_start: usize, byte_end: usize) -> InlineItem {
396 if byte_start == 0 && byte_end == seg.text.len() {
398 return InlineItem::Text(seg.clone());
399 }
400
401 let sub_text = seg.text[byte_start..byte_end].to_string();
402 let total_chars = seg.text.chars().count();
403 let char_start = seg.text[..byte_start].chars().count();
404 let char_count = sub_text.chars().count();
405
406 let (sub_glyphs, sub_advances, sub_width) = if seg.glyph_ids.len() == total_chars {
407 let end = (char_start + char_count).min(seg.glyph_ids.len());
409 let glyphs = seg.glyph_ids[char_start..end].to_vec();
410 let advances = seg.advances[char_start..end].to_vec();
411 let width: f64 = advances.iter().sum();
412 (glyphs, advances, width)
413 } else if seg.glyph_ids.is_empty() {
414 (vec![], vec![], 0.0)
416 } else {
417 let byte_frac = (byte_end - byte_start) as f64 / seg.text.len() as f64;
419 let est_glyphs = (seg.glyph_ids.len() as f64 * byte_frac).round() as usize;
420 let glyph_start = (seg.glyph_ids.len() as f64 * byte_start as f64 / seg.text.len() as f64)
421 .round() as usize;
422 let glyph_end = (glyph_start + est_glyphs).min(seg.glyph_ids.len());
423 let glyphs = seg.glyph_ids[glyph_start..glyph_end].to_vec();
424 let advances = seg.advances[glyph_start..glyph_end].to_vec();
425 let width: f64 = advances.iter().sum();
426 (glyphs, advances, width)
427 };
428
429 InlineItem::Text(TextSegment {
430 text: sub_text,
431 font_id: seg.font_id,
432 font_size: seg.font_size,
433 glyph_ids: sub_glyphs,
434 advances: sub_advances,
435 width: sub_width,
436 ascent: seg.ascent,
437 descent: seg.descent,
438 color: seg.color,
439 bold: seg.bold,
440 italic: seg.italic,
441 underline: seg.underline,
442 strike: seg.strike,
443 dstrike: seg.dstrike,
444 highlight: seg.highlight,
445 baseline_offset: seg.baseline_offset,
446 hyperlink_url: seg.hyperlink_url.clone(),
447 field_kind: seg.field_kind,
448 footnote_id: seg.footnote_id,
449 })
450}
451
452struct TextBreakInfo {
453 start: usize,
455 end: usize,
456 is_break: bool,
458}
459
460fn split_text_at_break_opportunities(seg: &TextSegment) -> Vec<TextBreakInfo> {
461 use unicode_linebreak::{BreakOpportunity, linebreaks};
462
463 let text = &seg.text;
464 if text.is_empty() {
465 return vec![];
466 }
467
468 let mut breaks = Vec::new();
469 let mut last_start = 0;
470
471 for (byte_pos, opportunity) in linebreaks(text) {
472 if byte_pos == 0 {
473 continue;
474 }
475
476 let is_break = matches!(
477 opportunity,
478 BreakOpportunity::Allowed | BreakOpportunity::Mandatory
479 );
480
481 breaks.push(TextBreakInfo {
482 start: last_start,
483 end: byte_pos,
484 is_break,
485 });
486 last_start = byte_pos;
487 }
488
489 if breaks.is_empty() {
491 breaks.push(TextBreakInfo {
492 start: 0,
493 end: text.len(),
494 is_break: true,
495 });
496 }
497
498 breaks
499}
500
501fn inline_item_width(item: &InlineItem) -> f64 {
502 match item {
503 InlineItem::Text(seg) => seg.width,
504 InlineItem::Tab => 36.0, InlineItem::Image { width, .. } => *width,
506 InlineItem::Marker(seg) => seg.width,
507 InlineItem::LineBreak | InlineItem::PageBreak | InlineItem::ColumnBreak => 0.0,
508 }
509}
510
511fn item_metrics(item: &InlineItem) -> (f64, f64, f64) {
512 match item {
514 InlineItem::Text(seg) => (seg.width, seg.ascent, seg.descent),
515 InlineItem::Marker(seg) => (seg.width, seg.ascent, seg.descent),
516 InlineItem::Tab => (36.0, 0.0, 0.0),
517 InlineItem::Image { width, height, .. } => (*width, *height, 0.0),
518 InlineItem::LineBreak | InlineItem::PageBreak | InlineItem::ColumnBreak => (0.0, 0.0, 0.0),
519 }
520}
521
522fn inline_to_line_item(
523 item: &InlineItem,
524 current_x: f64,
525 tab_stops: &[CT_TabStop],
526 fm: &FontManager,
527 font_ctx: Option<(FontId, f64)>,
528) -> LineItem {
529 match item {
530 InlineItem::Text(seg) => LineItem::Text(seg.clone()),
531 InlineItem::Marker(seg) => LineItem::Marker(seg.clone()),
532 InlineItem::Tab => {
533 let (tab_width, leader_char) = resolve_tab_width(current_x, tab_stops);
534 let leader = leader_char.and_then(|ch| shape_leader(fm, font_ctx, ch, tab_width));
535 LineItem::Tab {
536 width: tab_width,
537 leader,
538 }
539 }
540 InlineItem::Image {
541 width,
542 height,
543 embed_id,
544 } => LineItem::Image {
545 width: *width,
546 height: *height,
547 embed_id: embed_id.clone(),
548 },
549 InlineItem::LineBreak | InlineItem::PageBreak | InlineItem::ColumnBreak => LineItem::Tab {
550 width: 0.0,
551 leader: None,
552 },
553 }
554}
555
556fn shape_leader(
558 fm: &FontManager,
559 font_ctx: Option<(FontId, f64)>,
560 leader_char: char,
561 tab_width: f64,
562) -> Option<TextSegment> {
563 let (font_id, font_size) = font_ctx?;
564 if tab_width < 1.0 {
565 return None;
566 }
567
568 let single = String::from(leader_char);
570 let shaped = fm.shape_text(font_id, &single, font_size).ok()?;
571 if shaped.glyph_ids.is_empty() {
572 return None;
573 }
574 let char_advance = shaped.advances[0];
575 if char_advance < 0.5 {
576 return None;
577 }
578
579 let spacing = match leader_char {
581 '.' | '\u{00B7}' => char_advance * 0.5,
582 _ => char_advance * 0.15,
583 };
584 let step = char_advance + spacing;
585 let count = ((tab_width - spacing) / step).floor() as usize;
586 if count == 0 {
587 return None;
588 }
589
590 let leader_text: String = std::iter::repeat_n(leader_char, count).collect();
592 let mut glyph_ids = Vec::with_capacity(count);
593 let mut advances = Vec::with_capacity(count);
594 for i in 0..count {
595 glyph_ids.push(shaped.glyph_ids[0]);
596 if i + 1 < count {
597 advances.push(char_advance + spacing);
598 } else {
599 advances.push(char_advance);
600 }
601 }
602
603 let metrics = fm.metrics(font_id, font_size).ok()?;
604
605 Some(TextSegment {
606 text: leader_text,
607 font_id,
608 font_size,
609 glyph_ids,
610 advances,
611 width: tab_width, ascent: metrics.ascent,
613 descent: metrics.descent,
614 color: Color::BLACK,
615 bold: false,
616 italic: false,
617 underline: None,
618 strike: false,
619 dstrike: false,
620 highlight: None,
621 baseline_offset: 0.0,
622 hyperlink_url: None,
623 field_kind: None,
624 footnote_id: None,
625 })
626}
627
628fn resolve_tab_width(current_x: f64, tab_stops: &[CT_TabStop]) -> (f64, Option<char>) {
630 use rdocx_oxml::shared::ST_TabLeader;
631
632 for stop in tab_stops {
634 let stop_pos = stop.pos.to_pt();
635 if stop_pos > current_x {
636 let width = match stop.val {
637 ST_TabJc::Left => stop_pos - current_x,
638 ST_TabJc::Center => (stop_pos - current_x).max(0.0),
639 ST_TabJc::Right => (stop_pos - current_x).max(0.0),
640 _ => stop_pos - current_x,
641 };
642 let leader = stop.leader.and_then(|l| match l {
643 ST_TabLeader::Dot => Some('.'),
644 ST_TabLeader::Hyphen => Some('-'),
645 ST_TabLeader::Underscore => Some('_'),
646 ST_TabLeader::MiddleDot => Some('\u{00B7}'),
647 ST_TabLeader::Heavy => Some('_'),
648 ST_TabLeader::None => None,
649 });
650 return (width, leader);
651 }
652 }
653 let default_interval = 36.0;
655 let next_stop = ((current_x / default_interval).floor() + 1.0) * default_interval;
656 (next_stop - current_x, None)
657}
658
659fn compute_first_line_width(params: &LineBreakParams) -> f64 {
660 if params.ind_hanging > 0.0 {
661 params.available_width - params.ind_left - params.ind_right + params.ind_hanging
663 } else {
664 params.available_width - params.ind_left - params.ind_right - params.ind_first_line
665 }
666}
667
668fn compute_subsequent_line_width(params: &LineBreakParams) -> f64 {
669 params.available_width - params.ind_left - params.ind_right
670}
671
672fn first_line_indent(params: &LineBreakParams) -> f64 {
673 if params.ind_hanging > 0.0 {
674 params.ind_left - params.ind_hanging
675 } else {
676 params.ind_left + params.ind_first_line
677 }
678}
679
680fn subsequent_line_indent(params: &LineBreakParams) -> f64 {
681 params.ind_left
682}
683
684fn compute_line_height(ascent: f64, descent: f64, params: &LineBreakParams) -> f64 {
686 let natural = ascent + descent;
687 let natural = if natural < 1.0 { 12.0 } else { natural }; match (params.line_spacing, params.line_rule.as_deref()) {
690 (Some(spacing), Some("exact")) => {
691 spacing.to_pt()
693 }
694 (Some(spacing), Some("atLeast")) => {
695 natural.max(spacing.to_pt())
697 }
698 (Some(spacing), _) => {
699 let factor = spacing.0 as f64 / 240.0;
702 natural * factor
703 }
704 (None, _) => {
705 natural
707 }
708 }
709}
710
711#[cfg(test)]
712mod tests {
713 use super::*;
714
715 fn make_text_segment(text: &str, width: f64) -> TextSegment {
716 TextSegment {
717 text: text.to_string(),
718 font_id: FontId(0),
719 font_size: 12.0,
720 glyph_ids: vec![],
721 advances: vec![],
722 width,
723 ascent: 10.0,
724 descent: 3.0,
725 color: Color::BLACK,
726 bold: false,
727 italic: false,
728 underline: None,
729 strike: false,
730 dstrike: false,
731 highlight: None,
732 baseline_offset: 0.0,
733 hyperlink_url: None,
734 field_kind: None,
735 footnote_id: None,
736 }
737 }
738
739 #[test]
740 fn empty_paragraph_gets_one_line() {
741 let fm = FontManager::new();
742 let lines = break_into_lines(&[], &LineBreakParams::default(), &fm).unwrap();
743 assert_eq!(lines.len(), 1);
744 assert!(lines[0].is_last);
745 assert!(lines[0].items.is_empty());
746 }
747
748 #[test]
749 fn single_word_fits_one_line() {
750 let fm = FontManager::new();
751 let items = vec![InlineItem::Text(make_text_segment("Hello", 50.0))];
752 let lines = break_into_lines(&items, &LineBreakParams::default(), &fm).unwrap();
753 assert_eq!(lines.len(), 1);
754 assert!(lines[0].is_last);
755 }
756
757 #[test]
758 fn words_wrap_to_multiple_lines() {
759 let fm = FontManager::new();
760 let mut items = Vec::new();
761 items.push(InlineItem::Text(make_text_segment("Word1", 200.0)));
763 items.push(InlineItem::Text(make_text_segment("Word2", 200.0)));
764 items.push(InlineItem::Text(make_text_segment("Word3", 200.0)));
765
766 let lines = break_into_lines(&items, &LineBreakParams::default(), &fm).unwrap();
767 assert!(lines.len() >= 2);
768 }
769
770 #[test]
771 fn forced_line_break() {
772 let fm = FontManager::new();
773 let items = vec![
774 InlineItem::Text(make_text_segment("Before", 50.0)),
775 InlineItem::LineBreak,
776 InlineItem::Text(make_text_segment("After", 50.0)),
777 ];
778 let lines = break_into_lines(&items, &LineBreakParams::default(), &fm).unwrap();
779 assert!(lines.len() >= 2);
780 }
781
782 #[test]
783 fn line_height_exact() {
784 let params = LineBreakParams {
785 line_spacing: Some(Twips::from_pt(24.0)),
786 line_rule: Some("exact".to_string()),
787 ..Default::default()
788 };
789 let h = compute_line_height(10.0, 3.0, ¶ms);
790 assert!((h - 24.0).abs() < 0.01);
791 }
792
793 #[test]
794 fn line_height_auto() {
795 let params = LineBreakParams {
796 line_spacing: Some(Twips(480)), line_rule: Some("auto".to_string()),
798 ..Default::default()
799 };
800 let h = compute_line_height(10.0, 3.0, ¶ms);
801 assert!((h - 26.0).abs() < 0.01); }
803
804 #[test]
805 fn first_line_indent() {
806 let params = LineBreakParams {
807 ind_first_line: 36.0,
808 ..Default::default()
809 };
810 let first_w = compute_first_line_width(¶ms);
811 let subseq_w = compute_subsequent_line_width(¶ms);
812 assert!(first_w < subseq_w);
813 }
814
815 #[test]
816 fn hanging_indent() {
817 let params = LineBreakParams {
818 ind_left: 36.0,
819 ind_hanging: 36.0,
820 ..Default::default()
821 };
822 let first_indent = super::first_line_indent(¶ms);
823 let subseq_indent = super::subsequent_line_indent(¶ms);
824 assert!(first_indent < subseq_indent);
825 }
826
827 #[test]
828 fn tab_stop_resolution() {
829 let stops = vec![CT_TabStop::new(ST_TabJc::Left, Twips::from_pt(72.0))];
830 let (w, leader) = resolve_tab_width(36.0, &stops);
831 assert!((w - 36.0).abs() < 0.01);
832 assert!(leader.is_none());
833 }
834
835 #[test]
836 fn default_tab_stops() {
837 let (w, _) = resolve_tab_width(10.0, &[]);
838 assert!((w - 26.0).abs() < 0.01); }
840
841 #[test]
842 fn tab_stop_with_dot_leader() {
843 use rdocx_oxml::shared::ST_TabLeader;
844 let stops = vec![CT_TabStop {
845 val: ST_TabJc::Right,
846 pos: Twips::from_pt(400.0),
847 leader: Some(ST_TabLeader::Dot),
848 }];
849 let (w, leader) = resolve_tab_width(100.0, &stops);
850 assert!((w - 300.0).abs() < 0.01);
851 assert_eq!(leader, Some('.'));
852 }
853}