1use super::spatial::{Bounds, Placement, Resolved};
4use crate::{Node, ValidationError, Vector2};
5
6#[cfg(test)]
7mod tests;
8
9#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
11pub enum Alignment {
12 #[default]
14 Start,
15 Center,
17 End,
19 Baseline,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
26pub enum Layout {
27 Row {
29 gap: f64,
31 alignment: Alignment,
33 },
34 Column {
36 gap: f64,
38 alignment: Alignment,
40 },
41}
42
43#[derive(Clone, Copy, Debug, Default)]
80pub struct Row {
81 gap: f64,
82 alignment: Alignment,
83}
84
85impl Row {
86 pub fn new() -> Self {
88 Self::default()
89 }
90
91 pub fn gap(mut self, gap: f64) -> Self {
93 self.gap = gap;
94 self
95 }
96
97 pub fn alignment(mut self, alignment: Alignment) -> Self {
99 self.alignment = alignment;
100 self
101 }
102
103 pub fn children<Child: Into<Node>>(self, children: impl IntoIterator<Item = Child>) -> Node {
105 Node::layout(
106 Layout::Row {
107 gap: self.gap,
108 alignment: self.alignment,
109 },
110 children,
111 )
112 }
113}
114
115#[derive(Clone, Copy, Debug, Default)]
117pub struct Column {
118 gap: f64,
119 alignment: Alignment,
120}
121
122impl Column {
123 pub fn new() -> Self {
125 Self::default()
126 }
127
128 pub fn gap(mut self, gap: f64) -> Self {
130 self.gap = gap;
131 self
132 }
133
134 pub fn alignment(mut self, alignment: Alignment) -> Self {
136 self.alignment = alignment;
137 self
138 }
139
140 pub fn children<Child: Into<Node>>(self, children: impl IntoIterator<Item = Child>) -> Node {
142 Node::layout(
143 Layout::Column {
144 gap: self.gap,
145 alignment: self.alignment,
146 },
147 children,
148 )
149 }
150}
151
152impl Layout {
153 pub(super) fn baselines(
154 self,
155 children: &[Node],
156 context: &crate::DrawContext,
157 path: &str,
158 ) -> Result<Vec<Option<f64>>, ValidationError> {
159 if !matches!(
160 self,
161 Self::Row {
162 alignment: Alignment::Baseline,
163 ..
164 }
165 ) {
166 return Ok(vec![None; children.len()]);
167 }
168 children
169 .iter()
170 .enumerate()
171 .map(|(index, child)| child.baseline(context, &format!("{path}.children[{index}]")))
172 .collect()
173 }
174
175 pub(super) fn placements(
176 self,
177 children: &[(usize, Bounds)],
178 baselines: &[Option<f64>],
179 path: &str,
180 ) -> Result<Vec<Option<(Resolved, Bounds)>>, ValidationError> {
181 let (horizontal, gap, alignment) = match self {
182 Self::Row { gap, alignment } => (true, gap, alignment),
183 Self::Column { gap, alignment } => (false, gap, alignment),
184 };
185 if !gap.is_finite() {
186 return Err(ValidationError::invalid(
187 path,
188 "gap",
189 "Layout gap must be finite",
190 ));
191 }
192 if alignment == Alignment::Baseline && !horizontal {
193 return Err(ValidationError::unsupported(
194 path,
195 "alignment",
196 "Baseline alignment is supported only in horizontal rows",
197 ));
198 }
199 let mut cross_extent = 0.0_f64;
200 let mut common_baseline = 0.0_f64;
201 for (index, bounds) in children {
202 if matches!(bounds, Bounds::Empty) {
203 continue;
204 }
205 let rect = bounds.rect(&format!("{path}.children[{index}]"))?;
206 if alignment == Alignment::Baseline {
207 let baseline = baselines[*index].ok_or_else(|| {
208 ValidationError::unsupported(
209 path,
210 "alignment",
211 "Baseline requires text metrics for every nonempty child",
212 )
213 })?;
214 let offset = baseline - rect.top;
215 if !offset.is_finite() {
216 return Err(ValidationError::invalid(
217 path,
218 "baseline",
219 "Baseline offset must be finite",
220 ));
221 }
222 common_baseline = common_baseline.max(offset);
223 }
224 if !rect.width().is_finite() || !rect.height().is_finite() {
225 return Err(ValidationError::invalid(
226 path,
227 "layout_bounds",
228 "Layout spans must be finite",
229 ));
230 }
231 cross_extent = cross_extent.max(if horizontal {
232 rect.height()
233 } else {
234 rect.width()
235 });
236 }
237 let mut cursor = 0.0_f64;
238 let mut occupied = false;
239 let mut placements = Vec::with_capacity(children.len());
240 for (index, bounds) in children {
241 if matches!(bounds, Bounds::Empty) {
242 placements.push(None);
243 continue;
244 }
245 let child_path = format!("{path}.children[{index}]");
246 let rect = bounds.rect(&child_path)?;
247 if occupied {
248 cursor += gap;
249 }
250 let (main_size, cross_size) = if horizontal {
251 (rect.width(), rect.height())
252 } else {
253 (rect.height(), rect.width())
254 };
255 let cross = match alignment {
256 Alignment::Start => 0.0,
257 Alignment::Center => (cross_extent - cross_size) * 0.5,
258 Alignment::End => cross_extent - cross_size,
259 Alignment::Baseline => {
260 common_baseline - (baselines[*index].expect("validated baseline") - rect.top)
261 }
262 };
263 let offset = if horizontal {
264 Vector2::new(cursor - rect.left, cross - rect.top)
265 } else {
266 Vector2::new(cross - rect.left, cursor - rect.top)
267 };
268 let placement = Placement {
269 translation: offset,
270 ..Placement::default()
271 }
272 .resolve(*bounds, &child_path)?;
273 let placed_bounds = bounds.transformed(placement.transform, &child_path)?;
274 placements.push(Some((placement, placed_bounds)));
275 cursor += main_size;
276 if !cursor.is_finite() {
277 return Err(ValidationError::invalid(
278 path,
279 "layout_bounds",
280 "Layout extent must remain finite",
281 ));
282 }
283 occupied = true;
284 }
285 Ok(placements)
286 }
287}