ferroui_pretext_layout/
column_flow_layout.rs1use std::fmt;
2
3use ferroui_pretext::{LayoutCursor, PreparedTextWithSegments, PretextLayout};
4
5use crate::{
6 CircleObstacle, Interval, ObstacleLayoutHelper, PositionedLine, PreparedTextMetrics,
7 RectObstacle,
8};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub enum LayoutError {
12 InvalidLineHeight,
13 InvalidMaximumWidth,
14 InvalidGeometry,
15 InvalidMinimumSlotWidth,
16 CoordinateOverflow,
18}
19
20impl fmt::Display for LayoutError {
21 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
22 formatter.write_str(match self {
23 Self::InvalidLineHeight => "line height must be finite and positive",
24 Self::InvalidMaximumWidth => "maximum width must be positive and at most 2^53",
25 Self::InvalidGeometry => {
26 "geometry must have finite coordinates and nonnegative dimensions"
27 }
28 Self::InvalidMinimumSlotWidth => "minimum slot width must be finite and nonnegative",
29 Self::CoordinateOverflow => {
30 "advancing the line band does not produce a larger finite coordinate"
31 }
32 })
33 }
34}
35
36impl std::error::Error for LayoutError {}
37
38pub struct ColumnFlowLayout;
40
41impl ColumnFlowLayout {
42 pub fn layout_into_columns(
46 prepared: &PreparedTextWithSegments,
47 columns: &[RectObstacle],
48 rectangles: &[RectObstacle],
49 circles: &[CircleObstacle],
50 line_height: f64,
51 min_slot_width: f64,
52 ) -> Result<Vec<PositionedLine>, LayoutError> {
53 if !line_height.is_finite() || line_height <= 0.0 {
54 return Err(LayoutError::InvalidLineHeight);
55 }
56 if !min_slot_width.is_finite() || min_slot_width < 0.0 {
57 return Err(LayoutError::InvalidMinimumSlotWidth);
58 }
59 if columns.iter().chain(rectangles).any(|rect| {
60 rect.width < 0.0
61 || rect.height < 0.0
62 || ![
63 rect.x,
64 rect.y,
65 rect.width,
66 rect.height,
67 rect.right(),
68 rect.bottom(),
69 ]
70 .iter()
71 .all(|value| value.is_finite())
72 }) || circles.iter().any(|circle| {
73 circle.radius < 0.0
74 || ![circle.x, circle.y, circle.radius]
75 .iter()
76 .all(|value| value.is_finite())
77 }) {
78 return Err(LayoutError::InvalidGeometry);
79 }
80
81 let mut lines = Vec::new();
82 let mut cursor = LayoutCursor {
83 segment_index: 0,
84 grapheme_index: 0,
85 };
86 for &column in columns {
87 if column.width <= 0.0
91 || column.width < min_slot_width
92 || column.height < line_height
93 || rectangles.iter().any(|rect| {
94 rect.x <= column.x
95 && rect.right() >= column.right()
96 && rect.y <= column.y
97 && rect.bottom() >= column.bottom()
98 })
99 {
100 continue;
101 }
102 let mut y = column.y;
103 while !PreparedTextMetrics::is_end(prepared, cursor) {
104 let next_y = y + line_height;
105 if !next_y.is_finite() || next_y <= y {
106 return Err(LayoutError::CoordinateOverflow);
107 }
108 if next_y > column.bottom() {
109 break;
110 }
111 if let Some(slot) =
112 best_slot(column, y, next_y, rectangles, circles, min_slot_width)
113 {
114 let Some(next_line) =
115 PretextLayout::layout_next_line(prepared, cursor, slot.width())
116 else {
117 return Ok(lines);
118 };
119 lines.push(PositionedLine {
120 text: next_line.text,
121 x: slot.left,
122 y,
123 width: next_line.width,
124 });
125 cursor = next_line.end;
126 }
127 y = next_y;
128 }
129 }
130 Ok(lines)
131 }
132}
133
134fn best_slot(
135 column: RectObstacle,
136 band_top: f64,
137 band_bottom: f64,
138 rectangles: &[RectObstacle],
139 circles: &[CircleObstacle],
140 min_slot_width: f64,
141) -> Option<Interval> {
142 let rectangles = rectangles.iter().filter_map(|rect| {
143 (rect.height > 0.0 && band_bottom > rect.y && band_top < rect.bottom()).then_some(
144 Interval {
145 left: rect.x,
146 right: rect.right(),
147 },
148 )
149 });
150 let circles = circles.iter().filter_map(|circle| {
151 ObstacleLayoutHelper::circle_interval_for_band(
152 circle.x,
153 circle.y,
154 circle.radius,
155 band_top,
156 band_bottom,
157 0.0,
158 0.0,
159 )
160 });
161 let slots = ObstacleLayoutHelper::carve_text_line_slots(
162 Interval {
163 left: column.x,
164 right: column.right(),
165 },
166 rectangles.chain(circles),
167 min_slot_width,
168 );
169 ObstacleLayoutHelper::pick_slot(&slots, false)
170}