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/stack/offset_positioning.rs
StratoSDK / crates / strato-ui-core / src / elements / stack / offset_positioning.rs
1use pathfinder_geometry::{
2 rect::RectF,
3 vector::{vec2f, Vector2F},
4};
5 
6use crate::{presenter::PositionCache, SizeConstraint};
7 
8/// Defines the positioning for an element in a stack on both X and Y axes relative to the parent of
9/// the stack or another child element within the stack. The child element is anchored to
10/// the other element by `Anchor` and then offset on both axes by the specified offset.
11#[derive(Default, Clone)]
12pub struct OffsetPositioning {
13 pub(super) x_axis: PositioningAxis<XAxisAnchor>,
14 pub(super) y_axis: PositioningAxis<YAxisAnchor>,
15}
16 
17impl OffsetPositioning {
18 pub fn from_axes(
19 x_axis: PositioningAxis<XAxisAnchor>,
20 y_axis: PositioningAxis<YAxisAnchor>,
21 ) -> Self {
22 OffsetPositioning { x_axis, y_axis }
23 }
24 
25 /// Returns an `OffsetPositioning` that may be used to position a stack child element relative
26 /// to the stack parent's bounding rectangle.
27 ///
28 /// `bounds` specifies bounding behavior, if any, that should be used when calculating the stack
29 /// child's final size and position. See docs on [`Bound`] for more detail.
30 ///
31 /// `parent_anchor` is used to determine the exact position on the parent's bounding rectangle
32 /// relative to which the child will be positioned, using the child_anchor-determined position
33 /// on the child's bounding rect.
34 ///
35 /// For example, to position the bottom right of the child offset from the top-left corner of
36 /// the parent, pass `parent_anchor`: [`ParentAnchor::TopLeft`] and
37 /// `child_anchor`: [`ChildAnchor::BottomRight`].
38 pub fn offset_from_parent(
39 offset: Vector2F,
40 bound: ParentOffsetBounds,
41 parent_anchor: ParentAnchor,
42 child_anchor: ChildAnchor,
43 ) -> Self {
44 let parent_anchor: Anchor = parent_anchor.into();
45 let child_anchor: Anchor = child_anchor.into();
46 Self::from_axes(
47 PositioningAxis::relative_to_parent(
48 bound,
49 OffsetType::Pixel(offset.x()),
50 AnchorPair::new(parent_anchor.x(), child_anchor.x()),
51 ),
52 PositioningAxis::relative_to_parent(
53 bound,
54 OffsetType::Pixel(offset.y()),
55 AnchorPair::new(parent_anchor.y(), child_anchor.y()),
56 ),
57 )
58 }
59 
60 /// Returns an `OffsetPositioning` that may be used to position a stack child element relative
61 /// to an arbitrary 'anchor' element that is wrapped and rendered within a [`SavePosition`].
62 ///
63 /// `bound` specifies bounding behavior, if any, that should be used when calculating the stack
64 /// child's final size and position. See docs on [`Bound`] for more detail.
65 ///
66 /// `save_position_element_anchor` is used to determine the exact position on the anchor
67 /// element's bounding rectangle relative to which the child will be positioned, using the
68 /// child_anchor-determined position on the child's bounding rect.
69 ///
70 /// For example, to position the bottom right of the child offset from the top-left corner of
71 /// the anchor, pass `save_position_element_anchor`: [`PositionedElementAnchor::TopLeft`] and
72 /// `child_anchor`: [`ChildAnchor::BottomRight`].
73 pub fn offset_from_save_position_element(
74 saved_position_id: impl Into<String>,
75 offset: Vector2F,
76 bounds: PositionedElementOffsetBounds,
77 save_position_element_anchor: PositionedElementAnchor,
78 child_anchor: ChildAnchor,
79 ) -> Self {
80 let position_id = saved_position_id.into();
81 let child_anchor: Anchor = child_anchor.into();
82 let save_position_element_anchor: Anchor = save_position_element_anchor.into();
83 Self::from_axes(
84 PositioningAxis::relative_to_stack_child(
85 position_id.clone(),
86 bounds,
87 OffsetType::Pixel(offset.x()),
88 AnchorPair::new(save_position_element_anchor.x(), child_anchor.x()),
89 ),
90 PositioningAxis::relative_to_stack_child(
91 position_id,
92 bounds,
93 OffsetType::Pixel(offset.y()),
94 AnchorPair::new(save_position_element_anchor.y(), child_anchor.y()),
95 ),
96 )
97 }
98 
99 /// Returns the size constraint to be used for the stack child, according to the
100 /// anchors and bounding behaviors specified in the [`PositionAxis`]'s.
101 pub fn size_constraint(
102 &self,
103 parent_size: Vector2F,
104 window_size: Vector2F,
105 default_constraint: SizeConstraint,
106 position_cache: &PositionCache,
107 ) -> SizeConstraint {
108 let default_max_width = default_constraint.max.x();
109 let size_constraint_max_x = self
110 .x_axis
111 .compute_max_child_width(parent_size.x(), window_size.x(), position_cache)
112 .unwrap_or_else(|err| {
113 // In production, or if the element is conditionally-rendered by
114 // its x-axis anchor, use the default max width instead of panicking.
115 debug_assert!(
116 self.x_axis.anchor.is_conditional(),
117 "Couldn't compute max child width: {err}"
118 );
119 None
120 })
121 .unwrap_or(default_max_width);
122 
123 let default_max_height = default_constraint.max.y();
124 let size_constraint_max_y = self
125 .y_axis
126 .compute_max_child_height(parent_size.y(), window_size.y(), position_cache)
127 .unwrap_or_else(|err| {
128 debug_assert!(
129 self.y_axis.anchor.is_conditional(),
130 "Couldn't compute max child height: {err}"
131 );
132 None
133 })
134 .unwrap_or(default_max_height);
135 
136 SizeConstraint {
137 min: vec2f(
138 default_constraint.min.x().min(size_constraint_max_x),
139 default_constraint.min.y().min(size_constraint_max_y),
140 ),
141 max: vec2f(size_constraint_max_x, size_constraint_max_y),
142 }
143 }
144}
145 
146/// Bounding behaviors that may be applied to parent-offset stack children (added via
147/// [`OffsetPositioning::offset_from_parent`].
148#[derive(Clone, Copy)]
149pub enum ParentOffsetBounds {
150 /// The element's position may be adjusted to ensure it does not overflow its parent's bounds
151 /// or the window's bounds. Size remains fixed.
152 ParentByPosition,
153 
154 /// The element's size may be adjusted to ensure it does not overflow its parent's bounds.
155 /// Position remains fixed.
156 ParentBySize,
157 
158 /// The element's position may be adjusted to ensure it does not overflow the window's bounds.
159 WindowByPosition,
160 
161 /// The element's position and size is unbounded relative to its parent element or window.
162 Unbounded,
163}
164 
165/// Bounding behaviors that may be applied to [`SavePosition`] element-offset stack children
166/// (added via [`OffsetPositioning::offset_from_save_position_element`].
167#[derive(Clone, Copy)]
168pub enum PositionedElementOffsetBounds {
169 /// The element's position may be adjusted to ensure it does not overflow its parent's bounds
170 /// or the window's bounds. Size remains fixed.
171 ParentByPosition,
172 
173 /// The element's position may be adjusted to ensure it does not overflow the bound of the
174 /// element it is anchored to. Size remains fixed.
175 AnchoredElement,
176 
177 /// The element's position may be adjusted to ensure it does not overflow the window's bounds.
178 /// Size remains fixed.
179 WindowByPosition,
180 
181 /// The element's size may be adjusted to ensure it does not overflow the window's bounds.
182 /// Position remains fixed.
183 WindowBySize,
184 
185 /// The element's position and size is unbounded relative to its parent element or window.
186 Unbounded,
187}
188 
189/// An 'anchor' point on the child element representing the point on the child's bounding rectangle
190/// that should be positioned to the parent or [`SavePosition`]ed element.
191#[derive(Clone, Copy, Debug)]
192pub enum ChildAnchor {
193 TopLeft,
194 TopRight,
195 TopMiddle,
196 MiddleLeft,
197 MiddleRight,
198 Center,
199 BottomLeft,
200 BottomRight,
201 BottomMiddle,
202}
203 
204/// An 'anchor' point on the parent element representing the point on the parents's bounding
205/// rectangle that should be used in concert with offset to position the [`Stack`]'s child element.
206#[derive(Clone, Copy, Debug)]
207pub enum ParentAnchor {
208 TopLeft,
209 TopRight,
210 TopMiddle,
211 MiddleLeft,
212 MiddleRight,
213 Center,
214 BottomLeft,
215 BottomRight,
216 BottomMiddle,
217}
218 
219/// An 'anchor' point on the [`SavePosition`]ed element representing the point on its bounding
220/// rectangle that should be used in concert with offset to position the [`Stack`]'s child element.
221#[derive(Clone, Copy, Debug)]
222pub enum PositionedElementAnchor {
223 TopLeft,
224 TopRight,
225 TopMiddle,
226 MiddleLeft,
227 MiddleRight,
228 Center,
229 BottomLeft,
230 BottomRight,
231 BottomMiddle,
232}
233 
234// An 'anchor' point on an element's bounding rectangle.
235//
236// A pair of [`Anchor`]s constitutes an [`AnchorPair`], which can be used to determine the position
237// of one element relative to another.
238#[derive(Clone, Debug)]
239enum Anchor {
240 TopLeft,
241 TopRight,
242 TopMiddle,
243 MiddleLeft,
244 MiddleRight,
245 Center,
246 BottomLeft,
247 BottomRight,
248 BottomMiddle,
249}
250 
251impl Anchor {
252 fn x(&self) -> XAxisAnchor {
253 match self {
254 Anchor::TopLeft | Anchor::MiddleLeft | Anchor::BottomLeft => XAxisAnchor::Left,
255 Anchor::TopRight | Anchor::MiddleRight | Anchor::BottomRight => XAxisAnchor::Right,
256 Anchor::Center | Anchor::TopMiddle | Anchor::BottomMiddle => XAxisAnchor::Middle,
257 }
258 }
259 
260 fn y(&self) -> YAxisAnchor {
261 match self {
262 Anchor::TopLeft | Anchor::TopRight | Anchor::TopMiddle => YAxisAnchor::Top,
263 Anchor::MiddleLeft | Anchor::MiddleRight | Anchor::Center => YAxisAnchor::Middle,
264 Anchor::BottomLeft | Anchor::BottomRight | Anchor::BottomMiddle => YAxisAnchor::Bottom,
265 }
266 }
267}
268 
269impl From<ChildAnchor> for Anchor {
270 fn from(child_anchor_point: ChildAnchor) -> Self {
271 match child_anchor_point {
272 ChildAnchor::TopLeft => Anchor::TopLeft,
273 ChildAnchor::TopRight => Anchor::TopRight,
274 ChildAnchor::TopMiddle => Anchor::TopMiddle,
275 ChildAnchor::MiddleLeft => Anchor::MiddleLeft,
276 ChildAnchor::MiddleRight => Anchor::MiddleRight,
277 ChildAnchor::Center => Anchor::Center,
278 ChildAnchor::BottomLeft => Anchor::BottomLeft,
279 ChildAnchor::BottomRight => Anchor::BottomRight,
280 ChildAnchor::BottomMiddle => Anchor::BottomMiddle,
281 }
282 }
283}
284 
285impl From<ParentAnchor> for Anchor {
286 fn from(parent_anchor_point: ParentAnchor) -> Self {
287 match parent_anchor_point {
288 ParentAnchor::TopLeft => Anchor::TopLeft,
289 ParentAnchor::TopRight => Anchor::TopRight,
290 ParentAnchor::TopMiddle => Anchor::TopMiddle,
291 ParentAnchor::MiddleLeft => Anchor::MiddleLeft,
292 ParentAnchor::MiddleRight => Anchor::MiddleRight,
293 ParentAnchor::Center => Anchor::Center,
294 ParentAnchor::BottomLeft => Anchor::BottomLeft,
295 ParentAnchor::BottomRight => Anchor::BottomRight,
296 ParentAnchor::BottomMiddle => Anchor::BottomMiddle,
297 }
298 }
299}
300 
301impl From<PositionedElementAnchor> for Anchor {
302 fn from(positioned_element_anchor_point: PositionedElementAnchor) -> Self {
303 match positioned_element_anchor_point {
304 PositionedElementAnchor::TopLeft => Anchor::TopLeft,
305 PositionedElementAnchor::TopRight => Anchor::TopRight,
306 PositionedElementAnchor::TopMiddle => Anchor::TopMiddle,
307 PositionedElementAnchor::MiddleLeft => Anchor::MiddleLeft,
308 PositionedElementAnchor::MiddleRight => Anchor::MiddleRight,
309 PositionedElementAnchor::Center => Anchor::Center,
310 PositionedElementAnchor::BottomLeft => Anchor::BottomLeft,
311 PositionedElementAnchor::BottomRight => Anchor::BottomRight,
312 PositionedElementAnchor::BottomMiddle => Anchor::BottomMiddle,
313 }
314 }
315}
316 
317#[derive(Clone, Copy)]
318pub enum XAxisAnchor {
319 Left,
320 Right,
321 Middle,
322}
323 
324#[derive(Clone, Copy)]
325pub enum YAxisAnchor {
326 Top,
327 Bottom,
328 Middle,
329}
330 
331pub trait AxisAnchor {}
332impl AxisAnchor for XAxisAnchor {}
333impl AxisAnchor for YAxisAnchor {}
334 
335#[derive(Clone)]
336pub struct AnchorPair<T>
337where
338 T: AxisAnchor + Clone,
339{
340 from: T,
341 to: T,
342}
343 
344impl<T> AnchorPair<T>
345where
346 T: AxisAnchor + Clone,
347{
348 pub fn new(from: T, to: T) -> Self {
349 AnchorPair { from, to }
350 }
351}
352 
353/// Internal enum used to represents the bounds of an element.
354///
355/// Users of the [`Stack`] should refer to [`ParentOffsetBounds`] and
356/// [`PositionedElementOffsetBounds`], which define the public API surface for specifying bounding
357/// behavior.
358#[derive(Clone, Copy)]
359enum Bounds {
360 /// The element's position or size is bound to the parent element.
361 Parent(ParentOffsetBounds),
362 
363 /// The element's position or size is bound to the anchor [`SavePosition`]-ed element.
364 PositionedElement(PositionedElementOffsetBounds),
365}
366 
367/// Type of offposition offsets.
368#[derive(Clone, Copy)]
369pub enum OffsetType {
370 /// Pixel value of the offset.
371 Pixel(f32),
372 /// Percentage offset based on the size of the anchored element. For example
373 /// if the percentage is 0.5 and the anchored element has a width of 100, the
374 /// pixel value of the offset will be 0.5 * 100. = 50. Note that this value
375 /// can only be between 0. and 1.
376 Percentage(f32),
377}
378 
379/// Specifies how to position a child element on a given axis. The child element is anchored from
380/// a corner (left/right on the x-axis, top-bottom on the y-axis) of the parent or relative element
381/// onto a corner of the child element and then offset by `offset`.
382#[derive(Clone)]
383pub struct PositioningAxis<T>
384where
385 T: AxisAnchor + Clone,
386{
387 /// The anchor to position the element relative to.
388 pub(super) anchor: PositioningAnchor,
389 
390 /// Specifies the bounding behavior of the positioned element.
391 bounds: Bounds,
392 
393 /// Specifies the pair of points on the anchor element and positioned element's bounding
394 /// rectangles which are used to calculate the element's final offset position.
395 anchor_pair: AnchorPair<T>,
396 
397 /// Constant 'offset' applied to the element's final position, after calculating the anchor or
398 /// parent element's anchored position. This could be either a pixel or percentage value.
399 offset: OffsetType,
400}
401 
402/// The anchor element that an element is positioned relative to. Currently,
403/// we support positioning relative to:
404/// * The element's parent
405/// * An anchor element identified by its saved position
406#[derive(Clone)]
407pub(super) enum PositioningAnchor {
408 RelativeToSavedPosition {
409 /// The ID in the saved positions cache to anchor against. This ID must have been
410 /// passed to the `SavePosition` element that wraps the anchor element
411 position_id: String,
412 /// Whether or not the element's display is conditional on the anchor element.
413 /// If the anchor's position is not saved, the element will not be rendered,
414 /// instead of panicking. This is useful when the anchor element is itself
415 /// only sometimes rendered (for example, it's a cursor/selection).
416 conditional: bool,
417 },
418 RelativeToParent,
419}
420 
421impl PositioningAnchor {
422 /// Whether or not the element's display is conditional on the anchor element
423 /// having been rendered.
424 pub fn is_conditional(&self) -> bool {
425 match self {
426 PositioningAnchor::RelativeToParent => false,
427 PositioningAnchor::RelativeToSavedPosition { conditional, .. } => *conditional,
428 }
429 }
430}
431 
432impl<T> PositioningAxis<T>
433where
434 T: AxisAnchor + Clone,
435{
436 pub fn relative_to_stack_child(
437 position_id: impl Into<String>,
438 bounds: PositionedElementOffsetBounds,
439 offset: OffsetType,
440 anchor_pair: AnchorPair<T>,
441 ) -> Self {
442 Self {
443 anchor: PositioningAnchor::RelativeToSavedPosition {
444 position_id: position_id.into(),
445 conditional: false,
446 },
447 bounds: Bounds::PositionedElement(bounds),
448 anchor_pair,
449 offset,
450 }
451 }
452 
453 pub fn relative_to_parent(
454 bounds: ParentOffsetBounds,
455 offset: OffsetType,
456 anchor_pair: AnchorPair<T>,
457 ) -> Self {
458 Self {
459 anchor: PositioningAnchor::RelativeToParent,
460 bounds: Bounds::Parent(bounds),
461 anchor_pair,
462 offset,
463 }
464 }
465 
466 /// Conditionally position the element along this axis. If the element
467 /// cannot be positioned relative to its anchor, it will be skipped, rather
468 /// than causing a panic.
469 ///
470 /// This is only supported for elements positioned relative to a stack child.
471 pub fn with_conditional_anchor(mut self) -> Self {
472 if let PositioningAnchor::RelativeToSavedPosition { conditional, .. } = &mut self.anchor {
473 *conditional = true;
474 } else {
475 debug_assert!(
476 false,
477 "Can only use conditional_anchor with child-relative positioning"
478 );
479 }
480 self
481 }
482}
483 
484impl PositioningAxis<XAxisAnchor> {
485 // Computes where on the x axis the stack child should be positioned given the element it's
486 // anchored to, the child's size, and the parent the stack is rendered into. The anchored
487 // element can be another child in the stack or the parent of the stack.
488 pub(super) fn compute_child_position(
489 &self,
490 child_size: Vector2F,
491 parent_rect: RectF,
492 window_size: Vector2F,
493 position_cache: &PositionCache,
494 ) -> Result<f32, String> {
495 let anchor_element_rect = match &self.anchor {
496 PositioningAnchor::RelativeToSavedPosition { position_id, .. } => {
497 match position_cache.get_position(position_id) {
498 Some(position) => position,
499 None => {
500 return Err(format!("Position not set for {position_id:?}"));
501 }
502 }
503 }
504 PositioningAnchor::RelativeToParent => parent_rect,
505 };
506 
507 let anchor_x = match self.anchor_pair.from {
508 XAxisAnchor::Left => anchor_element_rect.origin().x(),
509 XAxisAnchor::Right => anchor_element_rect.max_x(),
510 XAxisAnchor::Middle => {
511 anchor_element_rect.origin().x() + anchor_element_rect.width() / 2.
512 }
513 };
514 
515 let pixel_offset = match self.offset {
516 OffsetType::Percentage(ratio) => {
517 let total_width = match self.bounds {
518 Bounds::PositionedElement(PositionedElementOffsetBounds::AnchoredElement) => {
519 (anchor_element_rect.width() - child_size.x()).max(0.)
520 }
521 _ => anchor_element_rect.width(),
522 };
523 total_width * ratio
524 }
525 OffsetType::Pixel(value) => value,
526 };
527 
528 let child_position_x = match self.anchor_pair.to {
529 XAxisAnchor::Left => anchor_x,
530 XAxisAnchor::Right => anchor_x - child_size.x(),
531 XAxisAnchor::Middle => anchor_x - child_size.x() / 2.,
532 } + pixel_offset;
533 
534 let bounded_position_x = match self.bounds {
535 Bounds::Parent(ParentOffsetBounds::ParentByPosition)
536 | Bounds::PositionedElement(PositionedElementOffsetBounds::ParentByPosition) => {
537 // our first try: find a position for the element to sit comfortably
538 // within the left/right bounds of the parent.
539 let mut bounded_position_x = child_position_x.clamp(
540 parent_rect.min_x(),
541 (parent_rect.max_x() - child_size.x()).max(parent_rect.min_x()),
542 );
543 
544 // now, check if this position will cause the element to bleed offscreen.
545 // if so, make it right-align with its parent.
546 if bounded_position_x + child_size.x() > window_size.x() {
547 bounded_position_x = parent_rect.max_x() - child_size.x();
548 }
549 
550 // oops, we now made the left go offscreen.
551 // just center the element in the window then.
552 if bounded_position_x < 0.0 {
553 bounded_position_x = (window_size.x() - child_size.x()) / 2.0;
554 }
555 
556 bounded_position_x
557 }
558 Bounds::Parent(ParentOffsetBounds::ParentBySize) => {
559 child_position_x.clamp(parent_rect.min_x(), parent_rect.max_x())
560 }
561 Bounds::Parent(ParentOffsetBounds::WindowByPosition)
562 | Bounds::PositionedElement(PositionedElementOffsetBounds::WindowByPosition) => {
563 child_position_x.clamp(0., (window_size.x() - child_size.x()).max(0.))
564 }
565 Bounds::PositionedElement(PositionedElementOffsetBounds::WindowBySize) => {
566 child_position_x.clamp(0., window_size.x())
567 }
568 Bounds::PositionedElement(PositionedElementOffsetBounds::AnchoredElement) => {
569 child_position_x.clamp(
570 anchor_element_rect.min_x(),
571 (anchor_element_rect.max_x() - child_size.x()).max(anchor_element_rect.min_x()),
572 )
573 }
574 _ => child_position_x,
575 };
576 Ok(bounded_position_x)
577 }
578 
579 pub(super) fn compute_max_child_width(
580 &self,
581 max_parent_width: f32,
582 window_width: f32,
583 position_cache: &PositionCache,
584 ) -> Result<Option<f32>, String> {
585 match self.bounds {
586 Bounds::Parent(ParentOffsetBounds::ParentBySize) => {
587 let parent_anchor_x =
588 Self::parent_anchor_x(self.anchor_pair.from, max_parent_width, self.offset);
589 let mut max_child_width = match self.anchor_pair.to {
590 XAxisAnchor::Left => max_parent_width - parent_anchor_x,
591 XAxisAnchor::Right => parent_anchor_x,
592 XAxisAnchor::Middle => {
593 (max_parent_width - parent_anchor_x).min(parent_anchor_x) * 2.
594 }
595 };
596 max_child_width = max_child_width.clamp(0., max_parent_width);
597 Ok(Some(max_child_width))
598 }
599 Bounds::PositionedElement(PositionedElementOffsetBounds::WindowBySize) => {
600 match &self.anchor {
601 PositioningAnchor::RelativeToSavedPosition { position_id, .. } => {
602 let anchor_position_x = Self::positioned_element_anchor_x(
603 position_id.as_str(),
604 self.anchor_pair.from,
605 self.offset,
606 position_cache,
607 )?;
608 
609 let max_width = match self.anchor_pair.to {
610 XAxisAnchor::Left => window_width - anchor_position_x,
611 XAxisAnchor::Right => anchor_position_x,
612 XAxisAnchor::Middle => {
613 (window_width - anchor_position_x).min(anchor_position_x) * 2.
614 }
615 };
616 Ok(Some(max_width.clamp(0., window_width)))
617 }
618 PositioningAnchor::RelativeToParent => {
619 debug_assert!(false, "Bounding element size to window is not supported for parent-offset stack children.");
620 Ok(None)
621 }
622 }
623 }
624 _ => Ok(None),
625 }
626 }
627 
628 // Returns the x coordinate within the parent's max size constraint for the anchor point on the
629 // Parent element relative to which the stack child is positioned.
630 //
631 // If there is a `SavePosition` element (and the stack child is positioned relative to it,
632 // rather than its parent), then returns `None`.
633 fn parent_anchor_x(anchor: XAxisAnchor, width: f32, offset: OffsetType) -> f32 {
634 let pixel_offset = match offset {
635 OffsetType::Percentage(ratio) => ratio * width,
636 OffsetType::Pixel(value) => value,
637 };
638 let parent_anchor_position_x = match anchor {
639 XAxisAnchor::Left => 0.,
640 XAxisAnchor::Right => width,
641 XAxisAnchor::Middle => width / 2.,
642 };
643 parent_anchor_position_x + pixel_offset
644 }
645 
646 // Returns the x coordinate for the anchor point on the `SavePosition` element relative to
647 // which the stack child is positioned.
648 //
649 // If there is no `SavePosition` element (and the stack child is positioned relative to the
650 // parent), then returns `None`.
651 fn positioned_element_anchor_x(
652 position_id: &str,
653 anchor: XAxisAnchor,
654 offset: OffsetType,
655 position_cache: &PositionCache,
656 ) -> Result<f32, String> {
657 if let Some(anchor_element_position) = position_cache.get_position(position_id) {
658 let pixel_offset = match offset {
659 OffsetType::Pixel(value) => value,
660 OffsetType::Percentage(ratio) => ratio * anchor_element_position.width(),
661 };
662 let anchor_position_x = match anchor {
663 XAxisAnchor::Left => anchor_element_position.min_x(),
664 XAxisAnchor::Right => anchor_element_position.max_x(),
665 XAxisAnchor::Middle => anchor_element_position.center().x(),
666 };
667 Ok(anchor_position_x + pixel_offset)
668 } else {
669 Err(format!(
670 "Position not found for element with position_id {position_id}"
671 ))
672 }
673 }
674}
675 
676impl Default for PositioningAxis<XAxisAnchor> {
677 fn default() -> Self {
678 Self {
679 anchor_pair: AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Left),
680 bounds: Bounds::Parent(ParentOffsetBounds::Unbounded),
681 offset: OffsetType::Pixel(0.),
682 anchor: PositioningAnchor::RelativeToParent,
683 }
684 }
685}
686 
687impl PositioningAxis<YAxisAnchor> {
688 // Computes where on the y axis the stack child should be positioned given the element it's
689 // anchored to, the child's size, and the parent the stack is rendered into. The anchored
690 // element can be another child in the stack or the parent of the stack.
691 pub(super) fn compute_child_position(
692 &self,
693 child_size: Vector2F,
694 parent_rect: RectF,
695 window_size: Vector2F,
696 position_cache: &PositionCache,
697 ) -> Result<f32, String> {
698 let anchor_element_rect = match &self.anchor {
699 PositioningAnchor::RelativeToSavedPosition { position_id, .. } => {
700 match position_cache.get_position(position_id) {
701 Some(position) => position,
702 None => {
703 return Err(format!("Position not set for {position_id:?}"));
704 }
705 }
706 }
707 PositioningAnchor::RelativeToParent => parent_rect,
708 };
709 
710 let anchor_y = match self.anchor_pair.from {
711 YAxisAnchor::Top => anchor_element_rect.origin().y(),
712 YAxisAnchor::Bottom => anchor_element_rect.max_y(),
713 YAxisAnchor::Middle => anchor_element_rect.center().y(),
714 };
715 
716 let pixel_offset = match self.offset {
717 OffsetType::Percentage(ratio) => {
718 let total_height = match self.bounds {
719 Bounds::PositionedElement(PositionedElementOffsetBounds::AnchoredElement) => {
720 (anchor_element_rect.height() - child_size.y()).max(0.)
721 }
722 _ => anchor_element_rect.height(),
723 };
724 total_height * ratio
725 }
726 OffsetType::Pixel(value) => value,
727 };
728 
729 let child_position_y = match self.anchor_pair.to {
730 YAxisAnchor::Top => anchor_y,
731 YAxisAnchor::Bottom => anchor_y - child_size.y(),
732 YAxisAnchor::Middle => anchor_y - (child_size.y() / 2.),
733 } + pixel_offset;
734 
735 let bounded_position_y = match self.bounds {
736 Bounds::Parent(ParentOffsetBounds::ParentByPosition)
737 | Bounds::PositionedElement(PositionedElementOffsetBounds::ParentByPosition) => {
738 // our first try: find a position for the element to sit comfortably
739 // within the upper/lower bounds of the parent.
740 let mut bounded_position_y = child_position_y.clamp(
741 parent_rect.min_y(),
742 (parent_rect.max_y() - child_size.y()).max(parent_rect.min_y()),
743 );
744 
745 // now, check if this position will cause the element to bleed offscreen.
746 // if so, make it bottom-align with its parent.
747 if bounded_position_y + child_size.y() > window_size.y() {
748 bounded_position_y = parent_rect.max_y() - child_size.y();
749 }
750 
751 // oops, we now made the top go offscreen.
752 // just center the element in the window then.
753 if bounded_position_y < 0.0 {
754 bounded_position_y = (window_size.y() - child_size.y()) / 2.0;
755 }
756 
757 bounded_position_y
758 }
759 Bounds::Parent(ParentOffsetBounds::ParentBySize) => {
760 child_position_y.clamp(parent_rect.min_y(), parent_rect.max_y())
761 }
762 Bounds::Parent(ParentOffsetBounds::WindowByPosition)
763 | Bounds::PositionedElement(PositionedElementOffsetBounds::WindowByPosition) => {
764 child_position_y.clamp(0., (window_size.y() - child_size.y()).max(0.))
765 }
766 Bounds::PositionedElement(PositionedElementOffsetBounds::WindowBySize) => {
767 child_position_y.clamp(0., window_size.y())
768 }
769 Bounds::PositionedElement(PositionedElementOffsetBounds::AnchoredElement) => {
770 child_position_y.clamp(
771 anchor_element_rect.min_y(),
772 (anchor_element_rect.max_y() - child_size.y()).max(anchor_element_rect.min_y()),
773 )
774 }
775 _ => child_position_y,
776 };
777 
778 Ok(bounded_position_y)
779 }
780 
781 /// Returns the maximum height of the positioned child based on the window height, maximum
782 /// height given by the parent's size constraint, and the [`OffsetPositioning`]'s
783 /// bounding behavior.
784 pub(super) fn compute_max_child_height(
785 &self,
786 max_parent_height: f32,
787 window_height: f32,
788 position_cache: &PositionCache,
789 ) -> Result<Option<f32>, String> {
790 match self.bounds {
791 Bounds::Parent(ParentOffsetBounds::ParentBySize) => {
792 let parent_anchor_y =
793 Self::parent_anchor_y(self.anchor_pair.from, max_parent_height, self.offset);
794 let max_child_height = match self.anchor_pair.to {
795 YAxisAnchor::Top => max_parent_height - parent_anchor_y,
796 YAxisAnchor::Bottom => parent_anchor_y,
797 YAxisAnchor::Middle => {
798 (max_parent_height - parent_anchor_y).min(parent_anchor_y) * 2.
799 }
800 };
801 Ok(Some(max_child_height.clamp(0., max_parent_height)))
802 }
803 Bounds::PositionedElement(PositionedElementOffsetBounds::WindowBySize) => {
804 match &self.anchor {
805 PositioningAnchor::RelativeToSavedPosition { position_id, .. } => {
806 let anchor_position_y = Self::positioned_element_anchor_y(
807 position_id.as_str(),
808 self.anchor_pair.from,
809 self.offset,
810 position_cache,
811 )?;
812 let max_height = match self.anchor_pair.to {
813 YAxisAnchor::Top => window_height - anchor_position_y,
814 YAxisAnchor::Bottom => anchor_position_y,
815 YAxisAnchor::Middle => {
816 (window_height - anchor_position_y).min(anchor_position_y) * 2.
817 }
818 };
819 Ok(Some(max_height.clamp(0., window_height)))
820 }
821 PositioningAnchor::RelativeToParent => {
822 debug_assert!(false, "Bounding element size to window is not supported for parent-offset stack children.");
823 Ok(None)
824 }
825 }
826 }
827 _ => Ok(None),
828 }
829 }
830 
831 // Returns the y coordinate within the parent's max size constraint for the anchor point on the
832 // Parent element relative to which the stack child is positioned.
833 //
834 // If there is a `SavePosition` element (and the stack child is positioned relative to it,
835 // rather than its parent), then returns `None`.
836 fn parent_anchor_y(anchor: YAxisAnchor, height: f32, offset: OffsetType) -> f32 {
837 let pixel_offset = match offset {
838 OffsetType::Percentage(ratio) => ratio * height,
839 OffsetType::Pixel(value) => value,
840 };
841 let parent_anchor_position_y = match anchor {
842 YAxisAnchor::Top => 0.,
843 YAxisAnchor::Bottom => height,
844 YAxisAnchor::Middle => height / 2.,
845 };
846 parent_anchor_position_y + pixel_offset
847 }
848 
849 // Returns the y coordinate for the anchor point on the `SavePosition` element relative to
850 // which the stack child is positioned.
851 //
852 // If there is no `SavePosition` element (and the stack child is positioned relative to the
853 // parent), then returns `None`.
854 fn positioned_element_anchor_y(
855 position_id: &str,
856 anchor: YAxisAnchor,
857 offset: OffsetType,
858 position_cache: &PositionCache,
859 ) -> Result<f32, String> {
860 if let Some(positioned_element_position) = position_cache.get_position(position_id) {
861 let pixel_offset = match offset {
862 OffsetType::Pixel(value) => value,
863 OffsetType::Percentage(ratio) => ratio * positioned_element_position.height(),
864 };
865 let anchor_position_y = match anchor {
866 YAxisAnchor::Top => positioned_element_position.min_y(),
867 YAxisAnchor::Bottom => positioned_element_position.max_y(),
868 YAxisAnchor::Middle => positioned_element_position.center().y(),
869 };
870 Ok(anchor_position_y + pixel_offset)
871 } else {
872 Err(format!(
873 "Position not found for element with position_id {position_id}"
874 ))
875 }
876 }
877}
878 
879impl Default for PositioningAxis<YAxisAnchor> {
880 fn default() -> Self {
881 Self {
882 anchor_pair: AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top),
883 bounds: Bounds::Parent(ParentOffsetBounds::Unbounded),
884 offset: OffsetType::Pixel(0.),
885 anchor: PositioningAnchor::RelativeToParent,
886 }
887 }
888}
889 
890#[cfg(test)]
891#[path = "offset_positioning_test.rs"]
892mod tests;
893