1use std::fmt;
9
10use ravenrs_core::{Insets, Point, Rect, Size};
11use taffy::prelude::{
12 AvailableSpace, Dimension, FlexDirection, LengthPercentage, LengthPercentageAuto, TaffyTree,
13};
14
15pub use taffy::NodeId as LayoutId;
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
19#[non_exhaustive]
20#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
21pub enum Axis {
22 #[default]
24 Vertical,
25 Horizontal,
27}
28
29#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
31#[non_exhaustive]
32#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
33pub enum Align {
34 #[default]
36 Start,
37 Center,
39 End,
41 Stretch,
43 Fit,
47}
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51#[non_exhaustive]
52#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
53pub enum Justify {
54 #[default]
56 Start,
57 Center,
59 End,
61 SpaceBetween,
63}
64
65#[derive(Clone, Copy, Debug, Default, PartialEq)]
67#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
68pub struct LayoutStyle {
69 pub axis: Option<Axis>,
71 pub spacing: f32,
73 pub padding: Insets,
75 pub align: Align,
77 pub justify: Justify,
79 pub width: Option<f32>,
81 pub height: Option<f32>,
83 pub min_width: Option<f32>,
85 pub min_height: Option<f32>,
87 pub max_width: Option<f32>,
89 pub grow: f32,
91 pub align_self: Option<Align>,
93 pub overflow_hidden: bool,
96 pub grid_columns: Option<u16>,
98 pub wrap: bool,
100 pub aspect_ratio: Option<f32>,
102 pub absolute: Option<(f32, f32)>,
105}
106
107#[cfg_attr(feature = "live", ravenrs_live::expose)]
108impl LayoutStyle {
109 pub fn stack(axis: Axis) -> Self {
111 Self { axis: Some(axis), ..Self::default() }
112 }
113
114 pub fn leaf() -> Self {
116 Self::default()
117 }
118
119 fn to_taffy(self) -> taffy::Style {
120 let len = LengthPercentage::length;
121 let dim = |v: Option<f32>| v.map_or(Dimension::auto(), Dimension::length);
122 let lpa = |v: Option<f32>| v.map_or(LengthPercentageAuto::auto(), LengthPercentageAuto::length);
123 let align = |a: Align| match a {
124 Align::Start | Align::Fit => taffy::AlignItems::FLEX_START,
125 Align::Center => taffy::AlignItems::CENTER,
126 Align::End => taffy::AlignItems::FLEX_END,
127 Align::Stretch => taffy::AlignItems::STRETCH,
128 };
129 let mut style = taffy::Style {
130 display: taffy::Display::Flex,
131 flex_direction: match self.axis {
132 Some(Axis::Horizontal) => FlexDirection::Row,
133 _ => FlexDirection::Column,
134 },
135 gap: taffy::Size { width: len(self.spacing), height: len(self.spacing) },
136 padding: taffy::Rect {
137 left: len(self.padding.left),
138 right: len(self.padding.right),
139 top: len(self.padding.top),
140 bottom: len(self.padding.bottom),
141 },
142 align_items: align(self.align),
143 align_self: self.align_self.map(align),
144 justify_content: match self.justify {
145 Justify::Start => taffy::JustifyContent::FLEX_START,
146 Justify::Center => taffy::JustifyContent::CENTER,
147 Justify::End => taffy::JustifyContent::FLEX_END,
148 Justify::SpaceBetween => taffy::JustifyContent::SPACE_BETWEEN,
149 },
150 size: taffy::Size { width: dim(self.width), height: dim(self.height) },
151 min_size: taffy::Size { width: lpa(self.min_width), height: lpa(self.min_height) },
152 max_size: taffy::Size { width: lpa(self.max_width), height: LengthPercentageAuto::auto() },
153 flex_grow: self.grow,
154 flex_shrink: 1.0,
155 ..taffy::Style::default()
156 };
157 style.aspect_ratio = self.aspect_ratio;
158 if self.width.is_some() || self.height.is_some() {
160 style.flex_shrink = 0.0;
161 }
162 if self.wrap {
163 style.flex_wrap = taffy::FlexWrap::Wrap;
164 }
165 if let Some((x, y)) = self.absolute {
166 style.position = taffy::Position::Absolute;
167 style.inset = taffy::Rect {
168 left: LengthPercentageAuto::length(x),
169 top: LengthPercentageAuto::length(y),
170 right: LengthPercentageAuto::auto(),
171 bottom: LengthPercentageAuto::auto(),
172 };
173 }
174 if let Some(columns) = self.grid_columns {
175 style.display = taffy::Display::Grid;
176 style.grid_template_columns = taffy::style_helpers::evenly_sized_tracks(columns);
177 style.align_items = taffy::AlignItems::STRETCH;
178 }
179 if self.overflow_hidden {
180 style.overflow = taffy::Point { x: taffy::Overflow::Scroll, y: taffy::Overflow::Scroll };
181 style.scrollbar_width = 0.0;
182 }
183 if self.axis.is_none() {
186 style.min_size.width = LengthPercentageAuto::length(self.min_width.unwrap_or(0.0));
187 style.margin = taffy::Rect {
188 left: LengthPercentageAuto::length(0.0),
189 right: LengthPercentageAuto::length(0.0),
190 top: LengthPercentageAuto::length(0.0),
191 bottom: LengthPercentageAuto::length(0.0),
192 };
193 }
194 style
195 }
196}
197
198#[derive(Clone, Copy, Debug, PartialEq)]
200#[non_exhaustive]
201#[cfg_attr(feature = "live", ravenrs_live::expose(part = "variants"))]
202pub enum Available {
203 Definite(f32),
205 MinContent,
207 MaxContent,
209}
210
211#[cfg_attr(feature = "live", ravenrs_live::expose)]
212impl Available {
213 pub fn limit(self) -> Option<f32> {
215 match self {
216 Self::Definite(v) => Some(v),
217 Self::MinContent => Some(0.0),
218 Self::MaxContent => None,
219 }
220 }
221}
222
223#[derive(Clone, Copy, Debug, PartialEq)]
225#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
226pub struct MeasureInput {
227 pub known_width: Option<f32>,
229 pub known_height: Option<f32>,
231 pub available_width: Available,
233 pub available_height: Available,
235}
236
237#[derive(Debug)]
239#[cfg_attr(feature = "live", ravenrs_live::expose(part = "fields"))]
240pub struct LayoutError(taffy::TaffyError);
241
242impl fmt::Display for LayoutError {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 write!(f, "layout error: {}", self.0)
245 }
246}
247
248impl std::error::Error for LayoutError {}
249
250impl From<taffy::TaffyError> for LayoutError {
251 fn from(e: taffy::TaffyError) -> Self {
252 Self(e)
253 }
254}
255
256pub struct LayoutTree<C> {
258 taffy: TaffyTree<C>,
259 fit: std::collections::HashSet<LayoutId>,
261}
262
263impl<C> Default for LayoutTree<C> {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl<C> fmt::Debug for LayoutTree<C> {
270 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
271 f.debug_struct("LayoutTree").field("nodes", &self.taffy.total_node_count()).finish()
272 }
273}
274
275fn to_available(a: AvailableSpace) -> Available {
276 match a {
277 AvailableSpace::Definite(v) => Available::Definite(v),
278 AvailableSpace::MinContent => Available::MinContent,
279 AvailableSpace::MaxContent => Available::MaxContent,
280 }
281}
282
283#[cfg_attr(feature = "live", ravenrs_live::expose)]
284impl<C> LayoutTree<C> {
285 pub fn new() -> Self {
298 Self { taffy: TaffyTree::new(), fit: std::collections::HashSet::new() }
299 }
300
301 pub fn clear(&mut self) {
303 self.taffy.clear();
304 self.fit.clear();
305 }
306
307 pub fn node(&mut self, style: LayoutStyle, children: &[LayoutId]) -> Result<LayoutId, LayoutError> {
309 if matches!(style.align, Align::Center | Align::End) {
310 for &child in children {
311 if self.fit.contains(&child) {
312 let mut own = self.taffy.style(child)?.clone();
313 own.align_self = None;
314 self.taffy.set_style(child, own)?;
315 }
316 }
317 }
318 let id = self.taffy.new_with_children(style.to_taffy(), children)?;
319 if style.align_self == Some(Align::Fit) {
320 self.fit.insert(id);
321 }
322 Ok(id)
323 }
324
325 pub fn measured_leaf(&mut self, style: LayoutStyle, context: C) -> Result<LayoutId, LayoutError> {
327 Ok(self.taffy.new_leaf_with_context(style.to_taffy(), context)?)
328 }
329
330 pub fn compute(
332 &mut self,
333 root: LayoutId,
334 available: Size,
335 mut measure: impl FnMut(&mut C, MeasureInput) -> Size,
336 ) -> Result<(), LayoutError> {
337 let space = taffy::Size {
338 width: AvailableSpace::Definite(available.width),
339 height: AvailableSpace::Definite(available.height),
340 };
341 self.taffy.compute_layout_with_measure(root, space, |inputs, _id, context, style| {
342 taffy::compute_leaf_layout(
343 inputs,
344 style,
345 |_, _| 0.0,
346 |known, avail| {
347 let Some(ctx) = context else {
348 return taffy::Size::ZERO;
349 };
350 let size = measure(
351 ctx,
352 MeasureInput {
353 known_width: known.width,
354 known_height: known.height,
355 available_width: to_available(avail.width),
356 available_height: to_available(avail.height),
357 },
358 );
359 taffy::Size { width: size.width, height: size.height }
360 },
361 )
362 })?;
363 Ok(())
364 }
365
366 pub fn frame(&self, node: LayoutId) -> Result<Rect, LayoutError> {
368 let l = self.taffy.layout(node)?;
369 Ok(Rect::from_origin_size(
370 Point::new(l.location.x, l.location.y),
371 Size::new(l.size.width, l.size.height),
372 ))
373 }
374
375 pub fn content_box(&self, node: LayoutId) -> Result<Rect, LayoutError> {
377 let l = self.taffy.layout(node)?;
378 Ok(Rect::new(
379 l.padding.left,
380 l.padding.top,
381 (l.size.width - l.padding.left - l.padding.right).max(0.0),
382 (l.size.height - l.padding.top - l.padding.bottom).max(0.0),
383 ))
384 }
385
386 pub fn content_size(&self, node: LayoutId) -> Result<Size, LayoutError> {
388 let l = self.taffy.layout(node)?;
389 let r = l.scrollable_overflow_rect;
390 Ok(Size::new(r.right.max(0.0), r.bottom.max(0.0)))
391 }
392
393 pub fn context_mut(&mut self, node: LayoutId) -> Option<&mut C> {
395 self.taffy.get_node_context_mut(node)
396 }
397
398 pub fn node_count(&self) -> usize {
400 self.taffy.total_node_count()
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407
408 fn fixed(tree: &mut LayoutTree<()>, w: f32, h: f32) -> LayoutId {
409 let style = LayoutStyle { width: Some(w), height: Some(h), ..LayoutStyle::leaf() };
410 tree.node(style, &[]).unwrap()
411 }
412
413 #[test]
414 fn vstack_applies_padding_and_spacing() {
415 let mut tree = LayoutTree::<()>::new();
416 let a = fixed(&mut tree, 100.0, 20.0);
417 let b = fixed(&mut tree, 50.0, 30.0);
418 let style = LayoutStyle {
419 spacing: 24.0,
420 padding: Insets::uniform(32.0),
421 width: Some(800.0),
422 height: Some(600.0),
423 ..LayoutStyle::stack(Axis::Vertical)
424 };
425 let root = tree.node(style, &[a, b]).unwrap();
426 tree.compute(root, Size::new(800.0, 600.0), |_, _| Size::ZERO).unwrap();
427 assert_eq!(tree.frame(a).unwrap(), Rect::new(32.0, 32.0, 100.0, 20.0));
428 assert_eq!(tree.frame(b).unwrap(), Rect::new(32.0, 76.0, 50.0, 30.0));
429 assert_eq!(tree.frame(root).unwrap().size, Size::new(800.0, 600.0));
430 }
431
432 #[test]
433 fn absolute_children_sit_at_their_offset_outside_the_flow() {
434 let mut tree = LayoutTree::<()>::new();
435 let flow = fixed(&mut tree, 40.0, 10.0);
436 let pinned = tree
437 .node(
438 LayoutStyle {
439 width: Some(30.0),
440 height: Some(12.0),
441 absolute: Some((50.0, 70.0)),
442 ..LayoutStyle::leaf()
443 },
444 &[],
445 )
446 .unwrap();
447 let style = LayoutStyle {
448 padding: Insets::uniform(8.0),
449 width: Some(200.0),
450 height: Some(100.0),
451 ..LayoutStyle::stack(Axis::Vertical)
452 };
453 let root = tree.node(style, &[pinned, flow]).unwrap();
454 tree.compute(root, Size::new(200.0, 100.0), |_, _| Size::ZERO).unwrap();
455 assert_eq!(tree.frame(pinned).unwrap(), Rect::new(50.0, 70.0, 30.0, 12.0));
456 assert_eq!(tree.frame(flow).unwrap().origin, Point::new(8.0, 8.0), "the flow ignores absolute boxes");
457 }
458
459 #[test]
460 fn hstack_centers_on_cross_axis() {
461 let mut tree = LayoutTree::<()>::new();
462 let a = fixed(&mut tree, 10.0, 10.0);
463 let b = fixed(&mut tree, 10.0, 30.0);
464 let style = LayoutStyle {
465 spacing: 5.0,
466 align: Align::Center,
467 height: Some(100.0),
468 ..LayoutStyle::stack(Axis::Horizontal)
469 };
470 let root = tree.node(style, &[a, b]).unwrap();
471 tree.compute(root, Size::new(100.0, 100.0), |_, _| Size::ZERO).unwrap();
472 assert_eq!(tree.frame(b).unwrap().origin, Point::new(15.0, 35.0));
473 assert_eq!(tree.frame(a).unwrap().origin, Point::new(0.0, 45.0));
474 }
475
476 #[test]
477 fn explicit_sizes_do_not_shrink_next_to_overflowing_content() {
478 let mut tree = LayoutTree::<()>::new();
479 let header = tree
480 .node(LayoutStyle { height: Some(40.0), overflow_hidden: true, ..LayoutStyle::leaf() }, &[])
481 .unwrap();
482 let tall = fixed(&mut tree, 10.0, 5000.0);
483 let body = tree
484 .node(
485 LayoutStyle {
486 grow: 1.0,
487 min_height: Some(0.0),
488 overflow_hidden: true,
489 ..LayoutStyle::stack(Axis::Vertical)
490 },
491 &[tall],
492 )
493 .unwrap();
494 let root = tree
495 .node(LayoutStyle { height: Some(300.0), ..LayoutStyle::stack(Axis::Vertical) }, &[header, body])
496 .unwrap();
497 tree.compute(root, Size::new(100.0, 300.0), |_, _| Size::ZERO).unwrap();
498 assert_eq!(tree.frame(header).unwrap().size.height, 40.0);
499 let body = tree.frame(body).unwrap();
500 assert_eq!((body.y(), body.height()), (40.0, 260.0), "the overflowing body takes the rest");
501 }
502
503 #[test]
504 fn grid_splits_width_into_equal_columns() {
505 let mut tree = LayoutTree::<()>::new();
506 let cells: Vec<_> = (0..4).map(|_| fixed(&mut tree, 10.0, 10.0)).collect();
507 let style = LayoutStyle {
508 grid_columns: Some(2),
509 spacing: 10.0,
510 width: Some(210.0),
511 ..LayoutStyle::stack(Axis::Vertical)
512 };
513 let root = tree.node(style, &cells).unwrap();
514 tree.compute(root, Size::new(500.0, 500.0), |_, _| Size::ZERO).unwrap();
515 assert_eq!(tree.frame(cells[1]).unwrap().origin, Point::new(110.0, 0.0));
516 assert_eq!(tree.frame(cells[3]).unwrap().origin, Point::new(110.0, 20.0));
517 }
518
519 #[test]
520 fn measured_leaf_wraps_to_available_width() {
521 let mut tree = LayoutTree::<f32>::new();
523 let text = tree.measured_leaf(LayoutStyle::leaf(), 300.0).unwrap();
524 let style = LayoutStyle { align: Align::Stretch, ..LayoutStyle::stack(Axis::Vertical) };
525 let root = tree.node(LayoutStyle { width: Some(120.0), ..style }, &[text]).unwrap();
526 tree.compute(root, Size::new(500.0, 500.0), |natural, input| {
527 let limit = input.known_width.or(input.available_width.limit()).unwrap_or(*natural);
528 let width = natural.min(limit.max(1.0));
529 Size::new(width, (*natural / width).ceil() * 10.0)
530 })
531 .unwrap();
532 assert_eq!(tree.frame(text).unwrap().size, Size::new(120.0, 30.0));
533 assert_eq!(*tree.context_mut(text).unwrap(), 300.0);
534 }
535}