1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
use crate::primitives::*;
use crate::types::{StackerCell, StackerDirection};
use pax_engine::api::Numeric;
use pax_engine::api::{Property, Size, Transform2D};
use pax_engine::*;
use pax_runtime::api::{NodeContext, StringBox};

/// Stacker lays out a series of nodes either
/// vertically or horizontally (i.e. a single row or column) with a specified gutter in between
/// each node.  `Stacker`s can be stacked inside of each other, horizontally
/// and vertically, along with percentage-based positioning and `Transform2D.anchor` to compose any rectilinear 2D layout.
#[pax]
#[custom(Default)]
#[inlined(
    for (cell_spec, i) in self._cell_specs {
        <Group
            transform={Transform2D::translate((cell_spec.x_px)px, (cell_spec.y_px)px)}
            width={(cell_spec.width_px)px}
            height={(cell_spec.height_px)px}
        >
            slot(i)
        </Group>
    }

    @settings {
        @mount: on_mount
    }

)]
pub struct Stacker {
    pub cells: Property<Numeric>,
    pub direction: Property<crate::types::StackerDirection>,
    pub _cell_specs: Property<Vec<StackerCell>>,
    pub gutter: Property<Size>,

    /// For for specifying sizes of each cell.  None-values (or array-index out-of-bounds values)
    /// will fall back to computed, equal-sizing
    pub sizes: Property<Vec<Option<Size>>>,
}

impl Default for Stacker {
    fn default() -> Self {
        Self {
            cells: Property::new(1.into()),
            direction: Property::new(StackerDirection::Horizontal),
            _cell_specs: Property::new(vec![]),
            gutter: Property::new(Size::Pixels(Numeric::Integer(0))),
            sizes: Property::new(vec![]),
        }
    }
}

impl Stacker {
    pub fn on_mount(&mut self, ctx: &NodeContext) {
        let cells = self.cells.clone();
        let sizes = self.sizes.clone();
        let bound = ctx.bounds_self.clone();
        let gutter = self.gutter.clone();
        let direction = self.direction.clone();

        let deps = [
            cells.untyped(),
            bound.untyped(),
            direction.untyped(),
            sizes.untyped(),
            gutter.untyped(),
        ];

        //NOTE: replace with is needed since the for loop already has a connection to the prop
        self._cell_specs.replace_with(Property::computed_with_name(
            move || {
                let cells: f64 = cells.get().to_float();
                let bounds = bound.get();

                let active_bound = match direction.get() {
                    StackerDirection::Horizontal => bounds.0,
                    StackerDirection::Vertical => bounds.1,
                };

                let gutter_calc = match gutter.get() {
                    Size::Pixels(pix) => pix,
                    Size::Percent(per) => {
                        Numeric::from(active_bound) * (per / Numeric::from(100.0))
                    }
                    Size::Combined(pix, per) => {
                        pix + (Numeric::from(active_bound) * (per / Numeric::from(100.0)))
                    }
                };

                let usable_interior_space = active_bound - (cells - 1.0) * gutter_calc.to_float();

                let per_cell_space = usable_interior_space / cells;

                let mut cell_space = vec![per_cell_space; cells as usize];
                let sizes = sizes.get();

                if sizes.len() > 0 {
                    if sizes.len() != (cells as usize) {
                        unreachable!(
                            "Sizes is not a valid length. Please specify {} sizes",
                            (cells as usize)
                        );
                    }
                    let mut used_space = 0.0;
                    let mut remaining_cells = 0.0;
                    for (i, size) in sizes.iter().enumerate() {
                        if let Some(s) = size {
                            let space = match s {
                                Size::Pixels(pix) => *pix,
                                Size::Percent(per) => {
                                    Numeric::from(active_bound) * (*per / Numeric::from(100.0))
                                }
                                Size::Combined(pix, per) => {
                                    *pix + (Numeric::from(active_bound)
                                        * (*per / Numeric::from(100.0)))
                                }
                            }
                            .to_float();
                            used_space += space;
                            cell_space[i] = space;
                        } else {
                            cell_space[i] = -1.0;
                            remaining_cells += 1.0;
                        }
                    }
                    if used_space > usable_interior_space {
                        unreachable!(
                            "Invalid sizes. Usable interior space is {}px",
                            usable_interior_space
                        );
                    }

                    let remaining_per_cell_space =
                        (usable_interior_space - used_space) / remaining_cells;
                    for i in &mut cell_space {
                        if *i == -1.0 {
                            *i = remaining_per_cell_space;
                        }
                    }
                }

                let mut used_space = 0.0;
                let new_cell_specs = (0..cells as usize)
                    .into_iter()
                    .map(|i| {
                        let ret = match direction.get() {
                            StackerDirection::Horizontal => StackerCell {
                                height_px: bounds.1,
                                width_px: cell_space[i],
                                x_px: ((i) as f64) * (gutter_calc) + used_space,
                                y_px: 0.0,
                            },
                            StackerDirection::Vertical => StackerCell {
                                height_px: cell_space[i],
                                width_px: bounds.0,
                                x_px: 0.0,
                                y_px: ((i) as f64) * (gutter_calc) + used_space,
                            },
                        };
                        used_space += cell_space[i];
                        ret
                    })
                    .collect();
                new_cell_specs
            },
            &deps,
            "stacker _cell_specs",
        ));
    }
}