Skip to main content

aetna_core/tree/
layout_modifiers.rs

1//! Layout and child-list modifiers for [`El`].
2
3use crate::layout::{LayoutCtx, LayoutFn};
4use crate::metrics::{ComponentSize, MetricsRole};
5
6use super::geometry::{Rect, Sides};
7use super::layout_types::{Align, Axis, Justify, Size};
8use super::node::El;
9
10impl El {
11    // ---- Sizing ----
12    pub fn width(mut self, w: Size) -> Self {
13        self.width = w;
14        self.explicit_width = true;
15        self
16    }
17
18    pub fn height(mut self, h: Size) -> Self {
19        self.height = h;
20        self.explicit_height = true;
21        self
22    }
23
24    pub fn hug(mut self) -> Self {
25        self.width = Size::Hug;
26        self.height = Size::Hug;
27        self.explicit_width = true;
28        self.explicit_height = true;
29        self
30    }
31
32    pub fn fill_size(mut self) -> Self {
33        self.width = Size::Fill(1.0);
34        self.height = Size::Fill(1.0);
35        self.explicit_width = true;
36        self.explicit_height = true;
37        self
38    }
39
40    /// Set the t-shirt size for stock controls.
41    pub fn size(mut self, size: ComponentSize) -> Self {
42        self.component_size = Some(size);
43        self
44    }
45
46    pub fn medium(self) -> Self {
47        self.size(ComponentSize::Md)
48    }
49
50    pub fn large(self) -> Self {
51        self.size(ComponentSize::Lg)
52    }
53
54    /// Set the theme-facing stock metrics role for this widget.
55    pub fn metrics_role(mut self, role: MetricsRole) -> Self {
56        self.metrics_role = Some(role);
57        self
58    }
59
60    // ---- Layout (container) ----
61    pub fn padding(mut self, p: impl Into<Sides>) -> Self {
62        self.padding = p.into();
63        self.explicit_padding = true;
64        self
65    }
66
67    /// Override only the top padding side, preserving the other three
68    /// sides at their current value (whether from a constructor's
69    /// `default_padding` or a previous explicit `.padding(...)`).
70    /// Mirrors Tailwind's `pt-N`. Marks the padding as explicit, so
71    /// the metrics pass will not stamp a density-driven value over it.
72    pub fn pt(mut self, v: f32) -> Self {
73        self.padding.top = v;
74        self.explicit_padding = true;
75        self
76    }
77
78    /// Override only the bottom padding side. Mirrors Tailwind's `pb-N`.
79    /// See [`Self::pt`] for composition semantics.
80    pub fn pb(mut self, v: f32) -> Self {
81        self.padding.bottom = v;
82        self.explicit_padding = true;
83        self
84    }
85
86    /// Override only the left padding side. Mirrors Tailwind's `pl-N`.
87    /// See [`Self::pt`] for composition semantics.
88    pub fn pl(mut self, v: f32) -> Self {
89        self.padding.left = v;
90        self.explicit_padding = true;
91        self
92    }
93
94    /// Override only the right padding side. Mirrors Tailwind's `pr-N`.
95    /// See [`Self::pt`] for composition semantics.
96    pub fn pr(mut self, v: f32) -> Self {
97        self.padding.right = v;
98        self.explicit_padding = true;
99        self
100    }
101
102    /// Override the horizontal padding sides (left + right), preserving
103    /// `top` and `bottom`. Mirrors Tailwind's `px-N`.
104    /// See [`Self::pt`] for composition semantics.
105    pub fn px(mut self, v: f32) -> Self {
106        self.padding.left = v;
107        self.padding.right = v;
108        self.explicit_padding = true;
109        self
110    }
111
112    /// Override the vertical padding sides (top + bottom), preserving
113    /// `left` and `right`. Mirrors Tailwind's `py-N`.
114    /// See [`Self::pt`] for composition semantics.
115    pub fn py(mut self, v: f32) -> Self {
116        self.padding.top = v;
117        self.padding.bottom = v;
118        self.explicit_padding = true;
119        self
120    }
121
122    pub fn gap(mut self, g: f32) -> Self {
123        self.gap = g;
124        self.explicit_gap = true;
125        self
126    }
127
128    pub fn align(mut self, a: Align) -> Self {
129        self.align = a;
130        self
131    }
132
133    pub fn justify(mut self, j: Justify) -> Self {
134        self.justify = j;
135        self
136    }
137
138    pub fn clip(mut self) -> Self {
139        self.clip = true;
140        self
141    }
142
143    pub fn scrollable(mut self) -> Self {
144        self.scrollable = true;
145        self
146    }
147
148    /// Show a draggable vertical scrollbar thumb when this scrollable
149    /// node's content overflows.
150    pub fn scrollbar(mut self) -> Self {
151        self.scrollbar = true;
152        self
153    }
154
155    /// Suppress the default scrollbar thumb on this scrollable node.
156    pub fn no_scrollbar(mut self) -> Self {
157        self.scrollbar = false;
158        self
159    }
160
161    /// Stick this scroll viewport's offset to the tail of its content
162    /// the way chat logs and activity feeds do — when new children land
163    /// below the current bottom, the offset follows them; when the user
164    /// scrolls up, the pin releases; when the user scrolls back to the
165    /// bottom, it re-engages. Mirrors `egui::ScrollArea::stick_to_bottom`.
166    ///
167    /// On first layout the offset starts at `max_offset`, so a freshly
168    /// mounted `scroll([...]).pin_end()` paints with its tail visible
169    /// rather than its head. Programmatic
170    /// [`crate::scroll::ScrollRequest::EnsureVisible`] requests that
171    /// resolve away from the tail also release the pin, so a
172    /// "jump-to-message N" action behaves as the user expects.
173    pub fn pin_end(mut self) -> Self {
174        self.pin_end = true;
175        self
176    }
177
178    /// Treat this element's focusable children as a single
179    /// arrow-navigable group.
180    pub fn arrow_nav_siblings(mut self) -> Self {
181        self.arrow_nav_siblings = true;
182        self
183    }
184
185    /// Replace the column/row/overlay distribution for this node with
186    /// a custom child layout function.
187    pub fn layout<F>(mut self, f: F) -> Self
188    where
189        F: Fn(LayoutCtx) -> Vec<Rect> + Send + Sync + 'static,
190    {
191        self.layout_override = Some(LayoutFn::new(f));
192        self
193    }
194
195    // ---- Children ----
196    pub fn child(mut self, c: impl Into<El>) -> Self {
197        self.children.push(c.into());
198        self
199    }
200
201    pub fn children<I, E>(mut self, cs: I) -> Self
202    where
203        I: IntoIterator<Item = E>,
204        E: Into<El>,
205    {
206        self.children.extend(cs.into_iter().map(Into::into));
207        self
208    }
209
210    /// Set the layout axis directly.
211    pub fn axis(mut self, a: Axis) -> Self {
212        self.axis = a;
213        self
214    }
215
216    // ---- Internal stock defaults ----
217    pub(crate) fn default_width(mut self, w: Size) -> Self {
218        self.width = w;
219        self.explicit_width = false;
220        self
221    }
222
223    pub(crate) fn default_height(mut self, h: Size) -> Self {
224        self.height = h;
225        self.explicit_height = false;
226        self
227    }
228
229    pub(crate) fn default_padding(mut self, p: impl Into<Sides>) -> Self {
230        self.padding = p.into();
231        self.explicit_padding = false;
232        self
233    }
234
235    pub(crate) fn default_gap(mut self, g: f32) -> Self {
236        self.gap = g;
237        self.explicit_gap = false;
238        self
239    }
240}
241
242#[cfg(test)]
243mod tests {
244    use super::*;
245    use crate::tree::Kind;
246
247    fn fresh() -> El {
248        El::new(Kind::Group)
249    }
250
251    #[test]
252    fn pt_sets_only_top_and_marks_explicit() {
253        let el = fresh().pt(7.0);
254        assert_eq!(
255            el.padding,
256            Sides {
257                left: 0.0,
258                right: 0.0,
259                top: 7.0,
260                bottom: 0.0
261            }
262        );
263        assert!(el.explicit_padding);
264    }
265
266    #[test]
267    fn px_py_set_only_their_axis() {
268        let el = fresh().px(4.0).py(2.0);
269        assert_eq!(
270            el.padding,
271            Sides {
272                left: 4.0,
273                right: 4.0,
274                top: 2.0,
275                bottom: 2.0
276            }
277        );
278        assert!(el.explicit_padding);
279    }
280
281    #[test]
282    fn pt_overrides_only_top_when_following_padding() {
283        // Tailwind shape: `p-4 pt-0` keeps left/right/bottom at 4 and zeros only top.
284        let el = fresh().padding(4.0).pt(0.0);
285        assert_eq!(
286            el.padding,
287            Sides {
288                left: 4.0,
289                right: 4.0,
290                top: 0.0,
291                bottom: 4.0
292            }
293        );
294        assert!(el.explicit_padding);
295    }
296
297    #[test]
298    fn pt_after_default_padding_preserves_other_sides_and_marks_explicit() {
299        // Constructor default of all-4, then author overrides just the top to 0.
300        // Other sides keep the default's value; explicit_padding flips so the
301        // metrics pass cannot stamp over the override.
302        let el = fresh().default_padding(4.0).pt(0.0);
303        assert_eq!(
304            el.padding,
305            Sides {
306                left: 4.0,
307                right: 4.0,
308                top: 0.0,
309                bottom: 4.0
310            }
311        );
312        assert!(el.explicit_padding);
313    }
314
315    #[test]
316    fn per_side_chainables_compose() {
317        let el = fresh().pl(1.0).pr(2.0).pt(3.0).pb(4.0);
318        assert_eq!(
319            el.padding,
320            Sides {
321                left: 1.0,
322                right: 2.0,
323                top: 3.0,
324                bottom: 4.0
325            }
326        );
327        assert!(el.explicit_padding);
328    }
329
330    #[test]
331    fn sides_x_and_y_constructors_only_populate_one_axis() {
332        assert_eq!(
333            Sides::x(5.0),
334            Sides {
335                left: 5.0,
336                right: 5.0,
337                top: 0.0,
338                bottom: 0.0
339            }
340        );
341        assert_eq!(
342            Sides::y(5.0),
343            Sides {
344                left: 0.0,
345                right: 0.0,
346                top: 5.0,
347                bottom: 5.0
348            }
349        );
350    }
351}