Skip to main content

ferroui_pretext_layout/
column_flow_layout.rs

1use 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    /// The next row cannot be represented as a strictly larger finite `f64`.
17    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
38/// Flow text through an ordered set of rectangular columns, avoiding obstacles.
39pub struct ColumnFlowLayout;
40
41impl ColumnFlowLayout {
42    /// Text that does not fit the supplied columns is left unmaterialized. Fully
43    /// blocked rows advance vertically without consuming source text. Exactly
44    /// one (the widest) available horizontal slot is used on each row.
45    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            // No row can acquire a usable slot in these cases. In particular,
88            // do not iterate height / line_height times for a zero-width or
89            // completely excluded column with a very small positive line height.
90            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}