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
171
172
173
174
175
176
#[allow(unused)]
use crate::*;
use pax_engine::api::Numeric;
use pax_engine::api::{Property, Size};
use pax_engine::*;
use pax_runtime::api::NodeContext;

/// 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]
#[engine_import_path("pax_engine")]
#[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 direction: Property<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 {
            direction: Property::new(StackerDirection::Vertical),
            _cell_specs: Property::new(vec![]),
            gutter: Property::new(Size::Pixels(Numeric::I32(0))),
            sizes: Property::new(vec![]),
        }
    }
}

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

        let deps = [
            bound.untyped(),
            direction.untyped(),
            sizes.untyped(),
            gutter.untyped(),
            slot_children_count.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 = slot_children_count.get() as f64;
                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::F64(active_bound) * (per / Numeric::F64(100.0)),
                    Size::Combined(pix, per) => {
                        pix + (Numeric::F64(active_bound) * (per / Numeric::F64(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 mut sizes = sizes.get();
                while sizes.len() < cell_space.len() {
                    sizes.push(None)
                }

                if sizes.len() > 0 {
                    let mut used_space = 0.0;
                    let mut remaining_cells = 0.0;
                    for (i, size) in sizes.iter().take(cells as usize).enumerate() {
                        if let Some(s) = size {
                            let space = match s {
                                Size::Pixels(pix) => *pix,
                                Size::Percent(per) => {
                                    Numeric::F64(active_bound) * (*per / Numeric::F64(100.0))
                                }
                                Size::Combined(pix, per) => {
                                    *pix + (Numeric::F64(active_bound)
                                        * (*per / Numeric::F64(100.0)))
                                }
                            }
                            .to_float();
                            used_space += space;
                            cell_space[i] = space;
                        } else {
                            cell_space[i] = -1.0;
                            remaining_cells += 1.0;
                        }
                    }

                    let remaining_per_cell_space =
                        (usable_interior_space - used_space) / remaining_cells;
                    // always show none cells at least 5px tall, even if it overflows the stacker
                    let remaining_per_cell_space = remaining_per_cell_space.max(5.0);
                    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.to_float() + 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.to_float() + used_space,
                            },
                        };
                        used_space += cell_space[i];
                        ret
                    })
                    .collect();
                new_cell_specs
            },
            &deps,
            "stacker _cell_specs",
        ));
    }
}

#[pax]
#[engine_import_path("pax_engine")]
pub struct StackerCell {
    pub x_px: f64,
    pub y_px: f64,
    pub width_px: f64,
    pub height_px: f64,
}

#[pax]
#[engine_import_path("pax_engine")]
pub enum StackerDirection {
    #[default]
    Vertical,
    Horizontal,
}