Skip to main content

aetna_core/tree/
layout_modifiers.rs

1//! Layout and child-list modifiers for [`El`].
2
3use crate::layout::{LayoutCtx, LayoutFn, VirtualAnchorPolicy};
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    /// Override how a dynamic virtual list chooses the in-viewport row
179    /// point that anchors the next frame.
180    pub fn virtual_anchor_policy(mut self, policy: VirtualAnchorPolicy) -> Self {
181        if let Some(items) = self.virtual_items.take() {
182            self.virtual_items = Some(items.anchor_policy(policy));
183        }
184        self
185    }
186
187    /// Treat this element's focusable children as a single
188    /// arrow-navigable group.
189    pub fn arrow_nav_siblings(mut self) -> Self {
190        self.arrow_nav_siblings = true;
191        self
192    }
193
194    /// Replace the column/row/overlay distribution for this node with
195    /// a custom child layout function.
196    pub fn layout<F>(mut self, f: F) -> Self
197    where
198        F: Fn(LayoutCtx) -> Vec<Rect> + Send + Sync + 'static,
199    {
200        self.layout_override = Some(LayoutFn::new(f));
201        self
202    }
203
204    // ---- Children ----
205    pub fn child(mut self, c: impl Into<El>) -> Self {
206        self.children.push(c.into());
207        self
208    }
209
210    pub fn children<I, E>(mut self, cs: I) -> Self
211    where
212        I: IntoIterator<Item = E>,
213        E: Into<El>,
214    {
215        self.children.extend(cs.into_iter().map(Into::into));
216        self
217    }
218
219    /// Set the layout axis directly.
220    pub fn axis(mut self, a: Axis) -> Self {
221        self.axis = a;
222        self
223    }
224
225    // ---- Internal stock defaults ----
226    pub(crate) fn default_width(mut self, w: Size) -> Self {
227        self.width = w;
228        self.explicit_width = false;
229        self
230    }
231
232    pub(crate) fn default_height(mut self, h: Size) -> Self {
233        self.height = h;
234        self.explicit_height = false;
235        self
236    }
237
238    pub(crate) fn default_padding(mut self, p: impl Into<Sides>) -> Self {
239        self.padding = p.into();
240        self.explicit_padding = false;
241        self
242    }
243
244    pub(crate) fn default_gap(mut self, g: f32) -> Self {
245        self.gap = g;
246        self.explicit_gap = false;
247        self
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::tree::Kind;
255
256    fn fresh() -> El {
257        El::new(Kind::Group)
258    }
259
260    #[test]
261    fn pt_sets_only_top_and_marks_explicit() {
262        let el = fresh().pt(7.0);
263        assert_eq!(
264            el.padding,
265            Sides {
266                left: 0.0,
267                right: 0.0,
268                top: 7.0,
269                bottom: 0.0
270            }
271        );
272        assert!(el.explicit_padding);
273    }
274
275    #[test]
276    fn px_py_set_only_their_axis() {
277        let el = fresh().px(4.0).py(2.0);
278        assert_eq!(
279            el.padding,
280            Sides {
281                left: 4.0,
282                right: 4.0,
283                top: 2.0,
284                bottom: 2.0
285            }
286        );
287        assert!(el.explicit_padding);
288    }
289
290    #[test]
291    fn pt_overrides_only_top_when_following_padding() {
292        // Tailwind shape: `p-4 pt-0` keeps left/right/bottom at 4 and zeros only top.
293        let el = fresh().padding(4.0).pt(0.0);
294        assert_eq!(
295            el.padding,
296            Sides {
297                left: 4.0,
298                right: 4.0,
299                top: 0.0,
300                bottom: 4.0
301            }
302        );
303        assert!(el.explicit_padding);
304    }
305
306    #[test]
307    fn pt_after_default_padding_preserves_other_sides_and_marks_explicit() {
308        // Constructor default of all-4, then author overrides just the top to 0.
309        // Other sides keep the default's value; explicit_padding flips so the
310        // metrics pass cannot stamp over the override.
311        let el = fresh().default_padding(4.0).pt(0.0);
312        assert_eq!(
313            el.padding,
314            Sides {
315                left: 4.0,
316                right: 4.0,
317                top: 0.0,
318                bottom: 4.0
319            }
320        );
321        assert!(el.explicit_padding);
322    }
323
324    #[test]
325    fn per_side_chainables_compose() {
326        let el = fresh().pl(1.0).pr(2.0).pt(3.0).pb(4.0);
327        assert_eq!(
328            el.padding,
329            Sides {
330                left: 1.0,
331                right: 2.0,
332                top: 3.0,
333                bottom: 4.0
334            }
335        );
336        assert!(el.explicit_padding);
337    }
338
339    #[test]
340    fn sides_x_and_y_constructors_only_populate_one_axis() {
341        assert_eq!(
342            Sides::x(5.0),
343            Sides {
344                left: 5.0,
345                right: 5.0,
346                top: 0.0,
347                bottom: 0.0
348            }
349        );
350        assert_eq!(
351            Sides::y(5.0),
352            Sides {
353                left: 0.0,
354                right: 0.0,
355                top: 5.0,
356                bottom: 5.0
357            }
358        );
359    }
360}