zng_wgt_stack/
types.rs

1use std::{fmt, mem, ops};
2use zng_layout::context::LayoutMask;
3use zng_var::animation::{Transitionable, easing::EasingStep};
4use zng_wgt::prelude::*;
5
6/// Defines a placement point in the previous item and the origin point of the next.
7///
8/// Defining stack direction like this allows expressing the traditional stack directions along an axis, as well as
9/// intermediary for transition animations or diagonal directions.
10///
11/// Note that collapsed items (layout size zero) are skipped, so the previous and next items are both non-empty in layout.
12///
13/// # Alignment & Spacing
14///
15/// The direction type can express non-fill alignment and spacing by it self, but prefer using the [`stack::children_align`] and
16/// [`stack::spacing`] properties as they are more readable and include fill alignment.
17///
18/// The [`Stack!`] widget implements alignment along the axis that does not change, so if the computed layout vector
19/// is zero in a dimension the items can fill in that dimension.
20///
21/// The [`Stack!`] widget adds the spacing along non-zero axis for each item offset after the first, so the spacing is not
22/// added for a perfect straight column or row, but it is added even for a single pixel shift *diagonal* stack.
23///
24/// [`stack::children_align`]: fn@crate::children_align
25/// [`stack::spacing`]: fn@crate::spacing
26/// [`Stack!`]: struct@crate::Stack
27#[derive(Default, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
28pub struct StackDirection {
29    /// Point on the previous item where the next item is placed.
30    pub place: Point,
31    /// Point on the next item that is offset to match `place`.
32    pub origin: Point,
33
34    /// If `place.x` and `origin.x` are swapped in [`LayoutDirection::RTL`] contexts.
35    ///
36    /// [`LayoutDirection::RTL`]: zng_wgt::prelude::LayoutDirection::RTL
37    pub is_rtl_aware: bool,
38}
39
40impl fmt::Debug for StackDirection {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        if f.alternate() {
43            f.debug_struct("StackDirection")
44                .field("place", &self.place)
45                .field("origin", &self.origin)
46                .field("is_rtl_aware", &self.is_rtl_aware)
47                .finish()
48        } else if self.is_rtl_aware {
49            write!(f, "({:?}, {:?}, {:?})", self.place, self.origin, self.is_rtl_aware)
50        } else {
51            write!(f, "({:?}, {:?})", self.place, self.origin)
52        }
53    }
54}
55
56impl fmt::Display for StackDirection {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        if let Some(p) = f.precision() {
59            write!(f, "({:.p$}, {:.p$}", self.place, self.origin, p = p)?;
60        } else {
61            write!(f, "({}, {}", self.place, self.origin)?;
62        }
63
64        if self.is_rtl_aware {
65            write!(f, ", {})", self.is_rtl_aware)
66        } else {
67            write!(f, ")")
68        }
69    }
70}
71
72impl StackDirection {
73    /// New custom direction.
74    pub fn new<P: Into<Point>, O: Into<Point>>(place: P, origin: O, is_rtl_aware: bool) -> Self {
75        Self {
76            place: place.into(),
77            origin: origin.into(),
78            is_rtl_aware,
79        }
80    }
81
82    /// `((100.pct(), 0), (0, 0))`, items are placed in a row from left to right.
83    ///
84    /// Alignment works on the `y` direction because it is not affected.
85    pub fn left_to_right() -> Self {
86        Self {
87            place: (100.pct(), 0).into(),
88            origin: (0, 0).into(),
89            is_rtl_aware: false,
90        }
91    }
92
93    /// `((0, 0), (100.pct(), 0))`, items are placed in a row from right to left.
94    ///
95    /// Alignment works on the `y` direction because it is not affected.
96    pub fn right_to_left() -> Self {
97        Self {
98            place: (0, 0).into(),
99            origin: (100.pct(), 0).into(),
100            is_rtl_aware: false,
101        }
102    }
103
104    /// `((100.pct(), 0), (0, 0), true)`, items are placed in a row from left to right or from right to left in RTL contexts.
105    ///
106    /// In [`LayoutDirection::RTL`] contexts the `place.x` and `origin.x` values are swapped before they are computed.
107    ///
108    /// Alignment works on the `y` direction because it is not affected.
109    ///
110    /// [`LayoutDirection::RTL`]: zng_wgt::prelude::LayoutDirection::RTL
111    pub fn start_to_end() -> Self {
112        Self {
113            place: (100.pct(), 0).into(),
114            origin: (0, 0).into(),
115            is_rtl_aware: true,
116        }
117    }
118
119    /// `((0, 0), (100.pct(), 0)), true)`, items are placed in a row from right to left or from left to right in RTL contexts.
120    ///
121    /// In [`LayoutDirection::RTL`] contexts the `place.x` and `origin.x` values are swapped before they are computed.
122    ///
123    /// Alignment works on the `y` direction because it is not affected.
124    ///
125    /// [`LayoutDirection::RTL`]: zng_wgt::prelude::LayoutDirection::RTL
126    pub fn end_to_start() -> Self {
127        Self {
128            place: (0, 0).into(),
129            origin: (100.pct(), 0).into(),
130            is_rtl_aware: true,
131        }
132    }
133
134    /// `((0, 100.pct()), (0, 0))`, items are placed in a column from top to bottom.
135    ///  
136    /// Alignment works on the `x` direction because it is not affected.
137    pub fn top_to_bottom() -> Self {
138        Self {
139            place: (0, 100.pct()).into(),
140            origin: (0, 0).into(),
141            is_rtl_aware: false,
142        }
143    }
144
145    /// `(0, 0), (0, 100.pct())`, items are placed in a column from bottom to top.
146    ///  
147    /// Alignment works on the `x` direction because it is not affected.
148    pub fn bottom_to_top() -> Self {
149        Self {
150            place: (0, 0).into(),
151            origin: (0, 100.pct()).into(),
152            is_rtl_aware: false,
153        }
154    }
155
156    /// `(0, 0)`, items are just stacked in the Z order.
157    ///
158    /// Fill alignment works in both dimensions because they don't change.
159    ///
160    /// Note that items are always rendered in the order defined by the [`z_index`] property.
161    ///
162    /// [`z_index`]: fn@zng_wgt::z_index
163    pub fn none() -> Self {
164        Self {
165            place: Point::zero(),
166            origin: Point::zero(),
167            is_rtl_aware: false,
168        }
169    }
170
171    /// Compute offset of the next item in the current [`LAYOUT`] context.
172    ///
173    /// [`LAYOUT`]: zng_wgt::prelude::LAYOUT
174    pub fn layout(&self, prev_item: PxRect, next_item: PxSize) -> PxVector {
175        if self.is_rtl_aware && LAYOUT.direction().is_rtl() {
176            let mut d = self.clone();
177            mem::swap(&mut d.place.x, &mut d.origin.x);
178            d.is_rtl_aware = false;
179            return d.layout_resolved_rtl(prev_item, next_item);
180        }
181
182        self.layout_resolved_rtl(prev_item, next_item)
183    }
184    pub(crate) fn layout_resolved_rtl(&self, prev_item: PxRect, next_item: PxSize) -> PxVector {
185        let c = LAYOUT.constraints();
186        let place = LAYOUT.with_constraints(c.with_exact_size(prev_item.size), || self.place.layout());
187        let origin = LAYOUT.with_constraints(c.with_exact_size(next_item), || self.origin.layout());
188        prev_item.origin.to_vector() + place.to_vector() - origin.to_vector()
189    }
190
191    /// Factor that defines the proportional direction.
192    ///
193    /// Values are in the range of `-1.0..=1.0`.
194    pub fn direction_factor(&self, direction: LayoutDirection) -> Factor2d {
195        let size = PxSize::new(Px(1000), Px(1000));
196        let metrics = LayoutMetrics::new(1.fct(), size, Px(1000)).with_direction(direction);
197        let p = LAYOUT.with_context(metrics, || self.layout(PxRect::from_size(size), size));
198
199        fn v(px: Px) -> Factor {
200            (px.0 as f32 / 1000.0).fct()
201        }
202        (v(p.x), v(p.y)).into()
203    }
204
205    /// Scale proportional to how one dimensional the direction is.
206    ///
207    /// Values are in the `0.0..=1.0` range where 0 is 20º or more from a single direction and 1 is 0º or 90º.
208    pub fn direction_scale(&self) -> Factor2d {
209        let scale = self.direction_factor(LayoutDirection::LTR).abs().yx();
210        if scale.x == 0.fct() && scale.y == 0.fct() {
211            return Factor2d::new(1.0, 1.0);
212        }
213        let angle = scale.y.0.atan2(scale.x.0).to_degrees();
214        if angle <= 20.0 {
215            Factor2d::new(1.0 - angle / 20.0, 0.0)
216        } else if angle >= 70.0 {
217            Factor2d::new(0.0, (angle - 70.0) / 20.0)
218        } else {
219            Factor2d::new(0.0, 0.0)
220        }
221    }
222
223    /// Compute a [`LayoutMask`] that flags all contextual values that affect the result of [`layout`].
224    ///
225    /// [`layout`]: Self::layout
226    /// [`LayoutMask`]: zng_layout::context::LayoutMask
227    pub fn affect_mask(&self) -> LayoutMask {
228        self.place.affect_mask() | self.origin.affect_mask()
229    }
230
231    /// Returns `true` if all values are [`Length::Default`].
232    ///
233    /// [`Length::Default`]: zng_wgt::prelude::Length::Default
234    pub fn is_default(&self) -> bool {
235        self.place.is_default() && self.origin.is_default()
236    }
237}
238
239impl_from_and_into_var! {
240    /// New from place and origin, not RTL aware.
241    fn from<P: Into<Point>, O: Into<Point>>((origin, size): (P, O)) -> StackDirection {
242        (origin, size, false).into()
243    }
244
245    /// New from place, origin, and RTL aware flag.
246    fn from<P: Into<Point>, O: Into<Point>>((origin, size, rtl_aware): (P, O, bool)) -> StackDirection {
247        StackDirection::new(origin, size, rtl_aware)
248    }
249}
250impl<S: Into<Factor2d>> ops::Mul<S> for StackDirection {
251    type Output = Self;
252
253    fn mul(mut self, rhs: S) -> Self {
254        self *= rhs;
255        self
256    }
257}
258impl<S: Into<Factor2d>> ops::Mul<S> for &StackDirection {
259    type Output = StackDirection;
260
261    fn mul(self, rhs: S) -> Self::Output {
262        self.clone() * rhs
263    }
264}
265impl<S: Into<Factor2d>> ops::MulAssign<S> for StackDirection {
266    fn mul_assign(&mut self, rhs: S) {
267        let rhs = rhs.into();
268        self.place *= rhs;
269        self.origin *= rhs;
270    }
271}
272impl<S: Into<Factor2d>> ops::Div<S> for StackDirection {
273    type Output = Self;
274
275    fn div(mut self, rhs: S) -> Self {
276        self /= rhs;
277        self
278    }
279}
280impl<S: Into<Factor2d>> ops::Div<S> for &StackDirection {
281    type Output = StackDirection;
282
283    fn div(self, rhs: S) -> Self::Output {
284        self.clone() / rhs
285    }
286}
287impl<S: Into<Factor2d>> ops::DivAssign<S> for StackDirection {
288    fn div_assign(&mut self, rhs: S) {
289        let rhs = rhs.into();
290        self.place /= rhs;
291        self.origin /= rhs;
292    }
293}
294impl ops::Add for StackDirection {
295    type Output = Self;
296
297    fn add(mut self, rhs: Self) -> Self {
298        self += rhs;
299        self
300    }
301}
302impl ops::AddAssign for StackDirection {
303    fn add_assign(&mut self, rhs: Self) {
304        self.place += rhs.place;
305        self.origin += rhs.origin;
306    }
307}
308impl ops::Sub for StackDirection {
309    type Output = Self;
310
311    fn sub(mut self, rhs: Self) -> Self {
312        self -= rhs;
313        self
314    }
315}
316impl ops::SubAssign for StackDirection {
317    fn sub_assign(&mut self, rhs: Self) {
318        self.place -= rhs.place;
319        self.origin -= rhs.origin;
320    }
321}
322
323impl Transitionable for StackDirection {
324    fn lerp(self, to: &Self, step: EasingStep) -> Self {
325        Self {
326            place: self.place.lerp(&to.place, step),
327            origin: self.origin.lerp(&to.origin, step),
328            is_rtl_aware: if step < 1.fct() { self.is_rtl_aware } else { to.is_rtl_aware },
329        }
330    }
331}