Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-ui-core/src/elements/flex/wrap.rs
1use crate::elements::AxisOrientation;
2use crate::event::DispatchedEvent;
3use crate::ClipBounds;
4 
5use super::cross_axis_size;
6use super::AppContext;
7use super::Axis;
8use super::CrossAxisAlignment;
9use super::Element;
10use super::EventContext;
11use super::LayoutContext;
12use super::MainAxisSize;
13use super::PaintContext;
14use super::Point;
15use super::SizeConstraint;
16use super::Vector2FExt;
17use crate::elements::flex::{main_axis_size, size_along_axis, LayoutState};
18use crate::elements::MainAxisAlignment;
19use ordered_float::OrderedFloat;
20use pathfinder_geometry::rect::RectF;
21use pathfinder_geometry::vector::{vec2f, Vector2F};
22 
23/// An element that positions its children in horizontal or vertical runs, leaving space in between
24/// each run.
25///
26/// This element can be thought of as a bare-bones version of a flex element with the `flex-wrap`
27/// property set in CSS. Children are laid out greedily until they can no longer fit on the current
28/// run, in which case a new run is created with the child as the first element. If a child exceeds
29/// the incoming size constraints it is clamped to the constraint max and clipped during painting.
30/// Children that can't fit in any run along the cross axis are not laid out or painted.
31pub struct Wrap {
32 axis: Axis,
33 orientation: AxisOrientation,
34 children: Vec<WrapChild>,
35 size: Option<Vector2F>,
36 origin: Option<Point>,
37 spacing: f32,
38 runs: Vec<Run>,
39 run_spacing: f32,
40 main_axis_alignment: MainAxisAlignment,
41 main_axis_size: MainAxisSize,
42 cross_axis_alignment: CrossAxisAlignment,
43}
44 
45impl Wrap {
46 pub fn new(axis: Axis) -> Self {
47 Self {
48 axis,
49 orientation: AxisOrientation::Normal,
50 children: vec![],
51 size: None,
52 origin: None,
53 spacing: 0.,
54 runs: vec![],
55 run_spacing: 0.,
56 main_axis_alignment: MainAxisAlignment::Start,
57 main_axis_size: MainAxisSize::Max,
58 cross_axis_alignment: CrossAxisAlignment::Start,
59 }
60 }
61 
62 pub fn row() -> Self {
63 Self::new(Axis::Horizontal)
64 }
65 
66 pub fn column() -> Self {
67 Self::new(Axis::Vertical)
68 }
69 
70 pub fn with_reverse_orientation(mut self) -> Self {
71 self.orientation = AxisOrientation::Reverse;
72 self
73 }
74 
75 pub fn with_spacing(mut self, spacing: f32) -> Self {
76 self.spacing = spacing;
77 self
78 }
79 
80 /// Use the specified amount of `spacing` between each run when positioning children.
81 pub fn with_run_spacing(mut self, spacing: f32) -> Self {
82 self.run_spacing = spacing;
83 self
84 }
85 
86 fn size_along_cross_axis(runs: &[Run], run_spacing: f32) -> f32 {
87 let run_height: f32 = runs.iter().map(|run| run.size_along_cross_axis).sum();
88 run_height + run_spacing * (runs.len().saturating_sub(1)) as f32
89 }
90 
91 /// Specifies the strategy to render children in each run when there is remaining space.
92 pub fn with_main_axis_alignment(mut self, alignment: MainAxisAlignment) -> Self {
93 self.main_axis_alignment = alignment;
94 self
95 }
96 
97 /// Specifies the strategy to size the overall element when there is remaining space after
98 /// runs.
99 pub fn with_main_axis_size(mut self, size: MainAxisSize) -> Self {
100 self.main_axis_size = size;
101 self
102 }
103 
104 pub fn with_cross_axis_alignment(mut self, alignment: CrossAxisAlignment) -> Self {
105 self.cross_axis_alignment = alignment;
106 self
107 }
108}
109 
110impl Extend<Box<dyn Element>> for Wrap {
111 fn extend<T: IntoIterator<Item = Box<dyn Element>>>(&mut self, iter: T) {
112 self.children.extend(iter.into_iter().map(WrapChild::new));
113 }
114}
115 
116impl Element for Wrap {
117 fn layout(
118 &mut self,
119 constraint: SizeConstraint,
120 ctx: &mut LayoutContext,
121 app: &AppContext,
122 ) -> Vector2F {
123 self.children.iter_mut().for_each(WrapChild::reset);
124 self.runs.clear();
125 
126 let max_constraint_along_cross_axis = constraint.max_along(self.axis.invert());
127 let max_constraint_along_main_axis = constraint.max_along(self.axis);
128 
129 let mut current_run = RunBuilder::default();
130 
131 for child in &mut self.children {
132 let child_constraint = match child.data() {
133 Some(child_data) if child_data.fill_run() => {
134 // If the child expands/shrinks based on the remaining space, then lay it out
135 // with _that_ as the max constraint along its main axis, rather than an
136 // infinite max constraint.
137 
138 let mut remaining_space_along_main_axis =
139 max_constraint_along_main_axis - current_run.size_along_main_axis;
140 
141 let should_create_new_run = match child_data {
142 WrapParentData::FillRemainingSpaceInRun { min_space, .. } => {
143 // If there's insufficient space along the main axis, start a new run rather
144 // than trying to lay out the child with the remaining space. This prevents
145 // calling child.layout() with a maximum constraint that's less than whatever
146 // minimum it might've set.
147 remaining_space_along_main_axis < min_space
148 }
149 WrapParentData::FillEntireRun => true,
150 };
151 
152 if should_create_new_run {
153 let mut new_run = RunBuilder::default();
154 std::mem::swap(&mut new_run, &mut current_run);
155 self.runs.push(new_run.build(
156 self.spacing,
157 max_constraint_along_main_axis,
158 self.main_axis_alignment,
159 self.axis,
160 ));
161 remaining_space_along_main_axis = max_constraint_along_main_axis;
162 }
163 
164 // Let the child expand along the cross axis as well.
165 let remaining_space_along_cross_axis = max_constraint_along_cross_axis
166 - Self::size_along_cross_axis(self.runs.as_slice(), self.run_spacing);
167 
168 match self.axis {
169 Axis::Horizontal => SizeConstraint::new(
170 vec2f(0., constraint.min.y()),
171 vec2f(
172 remaining_space_along_main_axis,
173 remaining_space_along_cross_axis,
174 ),
175 ),
176 Axis::Vertical => SizeConstraint::new(
177 vec2f(constraint.min.x(), 0.),
178 vec2f(
179 remaining_space_along_cross_axis,
180 remaining_space_along_main_axis,
181 ),
182 ),
183 }
184 }
185 // Lay out the child so that it has an infinite max constraint along its main axis. The
186 // incoming max size constraint is respected along the cross axis.
187 _ => SizeConstraint::child_constraint_along_axis(self.axis, constraint),
188 };
189 let size = child.layout(child_constraint, ctx, app);
190 
191 // If the child individually exceeds the incoming size constraints, clamp it
192 // to the constraint max so it doesn't overflow the container. We continue
193 // laying out subsequent children rather than stopping entirely.
194 let size = vec2f(
195 size.x().min(constraint.max.x()),
196 size.y().min(constraint.max.y()),
197 );
198 
199 let child_size_along_main_axis = size.along(self.axis);
200 let child_size_along_cross_axis = size.along(self.axis.invert());
201 
202 // The child doesn't fit in the current run--create a new run.
203 if child_size_along_main_axis + current_run.size_along_main_axis
204 > max_constraint_along_main_axis
205 {
206 let mut new_run = RunBuilder::default();
207 std::mem::swap(&mut new_run, &mut current_run);
208 self.runs.push(new_run.build(
209 self.spacing,
210 max_constraint_along_main_axis,
211 self.main_axis_alignment,
212 self.axis,
213 ));
214 }
215 
216 if child_size_along_cross_axis > current_run.size_along_cross_axis {
217 // If the new size would cause the element to exceed the max size along the
218 // cross axis--don't add the item to the run and immediately break.
219 let total_run_size_on_cross_axis = child_size_along_cross_axis
220 + Self::size_along_cross_axis(self.runs.as_slice(), self.run_spacing);
221 if total_run_size_on_cross_axis > max_constraint_along_cross_axis {
222 break;
223 }
224 current_run.size_along_cross_axis = child_size_along_cross_axis;
225 }
226 
227 current_run.num_children += 1;
228 current_run.size_along_main_axis += child_size_along_main_axis;
229 // Add the spacing between the child and the next child (were we to add one).
230 current_run.size_along_main_axis += self.spacing;
231 }
232 
233 if current_run.num_children > 0 {
234 self.runs.push(current_run.build(
235 self.spacing,
236 max_constraint_along_main_axis,
237 self.main_axis_alignment,
238 self.axis,
239 ))
240 }
241 
242 let size_along_cross_axis = Self::size_along_cross_axis(&self.runs, self.run_spacing);
243 let size_along_main_axis = match self.main_axis_size {
244 MainAxisSize::Min => {
245 // Use the largest run along the main axis as the overall element width.
246 self.runs
247 .iter()
248 .map(|run| OrderedFloat(run.size_along_main_axis))
249 .max()
250 .unwrap_or_default()
251 .0
252 }
253 MainAxisSize::Max => constraint.max_along(self.axis),
254 };
255 
256 let size = match self.axis {
257 Axis::Horizontal => vec2f(size_along_main_axis, size_along_cross_axis),
258 Axis::Vertical => vec2f(size_along_cross_axis, size_along_main_axis),
259 };
260 
261 self.size = Some(size);
262 size
263 }
264 
265 fn after_layout(&mut self, ctx: &mut crate::AfterLayoutContext, app: &crate::AppContext) {
266 for child in &mut self.children {
267 child.after_layout(ctx, app)
268 }
269 }
270 
271 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
272 let mut num_children_painted = 0;
273 let original_origin = origin;
274 
275 let wrap_size = self.size.expect("size should exist at paint time");
276 let clip_bounds = RectF::new(origin, wrap_size);
277 
278 // Clip children to the wrap's own bounds so oversized items don't overflow.
279 ctx.scene
280 .start_layer(ClipBounds::BoundedByActiveLayerAnd(clip_bounds));
281 
282 let mut origin = origin;
283 
284 // If the axis is reversed, offset the origin position by the length of the flex along its main axis,
285 if let AxisOrientation::Reverse = self.orientation {
286 let size_shift = size_along_axis(main_axis_size(wrap_size, self.axis), self.axis);
287 origin += size_shift;
288 };
289 
290 for run in &self.runs {
291 let mut run_origin = match self.orientation {
292 AxisOrientation::Normal => origin + run.layout_state.leading_space,
293 AxisOrientation::Reverse => origin - run.layout_state.leading_space,
294 };
295 
296 for child in self
297 .children
298 .iter_mut()
299 .skip(num_children_painted)
300 .take(run.num_children)
301 {
302 let child_size = child.size().expect("child size should exist at paint time");
303 let child_cross_size = cross_axis_size(child_size, self.axis);
304 
305 let child_cross_shift = match self.cross_axis_alignment {
306 CrossAxisAlignment::Center => {
307 run.size_along_cross_axis / 2. - child_cross_size / 2.
308 }
309 CrossAxisAlignment::Start => 0.,
310 CrossAxisAlignment::End => run.size_along_cross_axis - child_cross_size,
311 CrossAxisAlignment::Stretch => 0.,
312 };
313 
314 // Paint the child and offset the origin by the size of the child along the main
315 // axis.
316 match self.orientation {
317 AxisOrientation::Normal => {
318 child.paint(
319 run_origin + size_along_axis(child_cross_shift, self.axis.invert()),
320 ctx,
321 app,
322 );
323 if let Some(child_size) = child.size() {
324 run_origin +=
325 size_along_axis(main_axis_size(child_size, self.axis), self.axis);
326 }
327 run_origin += run.layout_state.between_space;
328 }
329 AxisOrientation::Reverse => {
330 if let Some(child_size) = child.size() {
331 run_origin -=
332 size_along_axis(main_axis_size(child_size, self.axis), self.axis);
333 }
334 child.paint(run_origin, ctx, app);
335 run_origin -= run.layout_state.between_space;
336 }
337 };
338 }
339 num_children_painted += run.num_children;
340 
341 // We're finished painting the run. Update the origin to be at the start of the new run.
342 origin += size_along_axis(
343 run.size_along_cross_axis + self.run_spacing,
344 self.axis.invert(),
345 );
346 }
347 
348 ctx.scene.stop_layer();
349 self.origin = Some(Point::from_vec2f(original_origin, ctx.scene.z_index()));
350 }
351 
352 fn size(&self) -> Option<Vector2F> {
353 self.size
354 }
355 
356 fn origin(&self) -> Option<Point> {
357 self.origin
358 }
359 
360 fn dispatch_event(
361 &mut self,
362 event: &DispatchedEvent,
363 ctx: &mut EventContext,
364 app: &AppContext,
365 ) -> bool {
366 let mut handled = false;
367 for child in &mut self.children {
368 let child_dispatch = child.dispatch_event(event, ctx, app);
369 handled |= child_dispatch;
370 }
371 handled
372 }
373}
374 
375#[derive(Clone, Copy)]
376enum WrapParentData {
377 FillRemainingSpaceInRun {
378 /// If `true`, the child element will be laid out with the run's remaining space, rather than
379 /// infinite width. This allows children to expand to fill runs.
380 fill_run: bool,
381 
382 /// The minimum space along the main axis that this child needs. Generally, child elements
383 /// should reserve required space in their [`Element::layout`] implementations instead.
384 /// However, for flexible children, we sometimes need a minimum here.
385 min_space: f32,
386 },
387 FillEntireRun,
388}
389 
390/// Convenience wrapper for a [`Wrap`] child that must consume the entire run.
391///
392/// When a child is wrapped in `WrapFillEntireRun`, the `Wrap` layout will place that child alone
393/// on its own run and treat it as occupying all remaining main-axis space for that run. This is
394/// useful for elements like wide cards or chips that should expand to the full width of the
395/// current row instead of sharing the row with other wrapped children.
396pub struct WrapFillEntireRun(WrapFill);
397 
398impl WrapFillEntireRun {
399 pub fn new(child: Box<dyn Element>) -> Self {
400 Self(WrapFill {
401 parent_data: WrapParentData::FillEntireRun,
402 child,
403 })
404 }
405 
406 pub fn finish(self) -> Box<dyn Element> {
407 self.0.finish()
408 }
409}
410 
411/// Marker for children of a [`Wrap`] element that preferentially expand to fill the current
412/// row/column before starting a new row/column.
413pub struct WrapFill {
414 parent_data: WrapParentData,
415 child: Box<dyn Element>,
416}
417 
418impl WrapFill {
419 pub fn new(min_space: f32, child: Box<dyn Element>) -> Self {
420 Self {
421 parent_data: WrapParentData::FillRemainingSpaceInRun {
422 fill_run: true,
423 min_space,
424 },
425 child,
426 }
427 }
428}
429 
430impl WrapParentData {
431 fn fill_run(&self) -> bool {
432 match self {
433 WrapParentData::FillRemainingSpaceInRun { fill_run, .. } => *fill_run,
434 WrapParentData::FillEntireRun => true,
435 }
436 }
437}
438 
439impl Element for WrapFill {
440 fn layout(
441 &mut self,
442 constraint: SizeConstraint,
443 ctx: &mut LayoutContext,
444 app: &AppContext,
445 ) -> Vector2F {
446 self.child.layout(constraint, ctx, app)
447 }
448 
449 fn after_layout(&mut self, ctx: &mut crate::AfterLayoutContext, app: &AppContext) {
450 self.child.after_layout(ctx, app);
451 }
452 
453 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
454 self.child.paint(origin, ctx, app);
455 }
456 
457 fn size(&self) -> Option<Vector2F> {
458 self.child.size()
459 }
460 
461 fn origin(&self) -> Option<Point> {
462 self.child.origin()
463 }
464 
465 fn dispatch_event(
466 &mut self,
467 event: &DispatchedEvent,
468 ctx: &mut EventContext,
469 app: &AppContext,
470 ) -> bool {
471 self.child.dispatch_event(event, ctx, app)
472 }
473 
474 fn parent_data(&self) -> Option<&dyn std::any::Any> {
475 Some(&self.parent_data)
476 }
477}
478 
479/// Helper struct to encapsulate a child of a `Wrap` element that may not be painted or laid out
480/// depending on the number of elements that fit into the `Wrap` given incoming size constraints.
481struct WrapChild {
482 element: Box<dyn Element>,
483 is_laid_out: bool,
484 is_painted: bool,
485}
486 
487impl WrapChild {
488 fn new(element: Box<dyn Element>) -> Self {
489 Self {
490 element,
491 is_laid_out: false,
492 is_painted: false,
493 }
494 }
495 
496 fn data(&self) -> Option<WrapParentData> {
497 self.element
498 .parent_data()
499 .and_then(|data| data.downcast_ref())
500 .copied()
501 }
502 
503 fn reset(&mut self) {
504 self.is_laid_out = false;
505 self.is_painted = false;
506 }
507}
508 
509impl Element for WrapChild {
510 fn layout(
511 &mut self,
512 constraint: SizeConstraint,
513 ctx: &mut LayoutContext,
514 app: &AppContext,
515 ) -> Vector2F {
516 self.is_laid_out = true;
517 self.element.layout(constraint, ctx, app)
518 }
519 
520 fn after_layout(&mut self, ctx: &mut crate::AfterLayoutContext, app: &crate::AppContext) {
521 if self.is_laid_out {
522 self.element.after_layout(ctx, app);
523 }
524 }
525 
526 fn paint(&mut self, origin: Vector2F, ctx: &mut PaintContext, app: &AppContext) {
527 if self.is_laid_out {
528 self.element.paint(origin, ctx, app);
529 self.is_painted = true;
530 }
531 }
532 
533 fn size(&self) -> Option<Vector2F> {
534 self.element.size()
535 }
536 
537 fn origin(&self) -> Option<Point> {
538 self.element.origin()
539 }
540 
541 fn dispatch_event(
542 &mut self,
543 event: &DispatchedEvent,
544 ctx: &mut EventContext,
545 app: &AppContext,
546 ) -> bool {
547 if self.is_painted {
548 self.element.dispatch_event(event, ctx, app)
549 } else {
550 false
551 }
552 }
553}
554 
555/// A given run of a `Wrap` element.
556#[derive(Debug)]
557struct Run {
558 /// The size along the cross axis of the run. This is functionally the max size on the cross
559 /// axis of the elements within the run.
560 size_along_cross_axis: f32,
561 /// The size along the main axis of the run. This is the sum of each element within the run's
562 /// size, plus the leading space and space between each child.
563 size_along_main_axis: f32,
564 /// The number of children of the parent `Wrap` element that are rendered within this run.
565 num_children: usize,
566 /// Metadata used to layout the run. This is used to properly respect `MainAxisAlignment` within
567 /// each run.
568 layout_state: LayoutState,
569}
570 
571/// Builder type to construct a `Run`.
572#[derive(Debug, Default)]
573struct RunBuilder {
574 /// The size along the cross axis of the run. This is functionally the max size on the cross
575 /// axis of the elements within the run.
576 size_along_cross_axis: f32,
577 /// The main axis size along the run.
578 size_along_main_axis: f32,
579 /// The number of children of the parent `Wrap` element that are rendered within this run.
580 num_children: usize,
581}
582 
583impl RunBuilder {
584 fn build(
585 self,
586 spacing: f32,
587 max_constraint_along_main_axis: f32,
588 main_axis_alignment: MainAxisAlignment,
589 axis: Axis,
590 ) -> Run {
591 // We added spacing after every child, but we only want spacing _between_ children,
592 // so subtract the (extra) spacing after the last child.
593 let size_along_main_axis = self.size_along_main_axis - spacing;
594 
595 let layout_state = LayoutState::compute(
596 self.num_children,
597 spacing,
598 max_constraint_along_main_axis - size_along_main_axis,
599 main_axis_alignment,
600 axis,
601 );
602 
603 let size_along_main_axis = size_along_main_axis
604 + layout_state.leading_space.along(axis)
605 + layout_state.between_space.along(axis) * (self.num_children as f32 - 1.);
606 
607 Run {
608 size_along_cross_axis: self.size_along_cross_axis,
609 size_along_main_axis,
610 num_children: self.num_children,
611 layout_state,
612 }
613 }
614}
615 
616#[cfg(test)]
617#[path = "wrap_test.rs"]
618mod tests;
619