Skip to main content

damascene_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    /// Shorthand for `.width(Size::Fill(1.0))` — fill the available width
41    /// of the parent (or, in a column, span its inner content box). Mirrors
42    /// CSS `width: 100%`. For a non-default weight, use `.width(Size::Fill(w))`.
43    pub fn fill_width(mut self) -> Self {
44        self.width = Size::Fill(1.0);
45        self.explicit_width = true;
46        self
47    }
48
49    /// Shorthand for `.height(Size::Fill(1.0))` — fill the available height
50    /// of the parent (or, in a row, span its inner content box). Mirrors
51    /// CSS `height: 100%`. For a non-default weight, use `.height(Size::Fill(w))`.
52    pub fn fill_height(mut self) -> Self {
53        self.height = Size::Fill(1.0);
54        self.explicit_height = true;
55        self
56    }
57
58    /// Lower-bound the resolved width in logical pixels. Composes with
59    /// any [`Size`] choice — `Hug` won't shrink below the floor, `Fill`
60    /// won't lose space below it. See [`El::min_width`] for the full
61    /// semantic.
62    pub fn min_width(mut self, w: f32) -> Self {
63        self.min_width = Some(w);
64        self
65    }
66
67    /// Upper-bound the resolved width in logical pixels. Pairs naturally
68    /// with `Size::Fill` to cap a column at a readable measure.
69    pub fn max_width(mut self, w: f32) -> Self {
70        self.max_width = Some(w);
71        self
72    }
73
74    /// Lower-bound the resolved height in logical pixels. See
75    /// [`Self::min_width`] for the semantic.
76    pub fn min_height(mut self, h: f32) -> Self {
77        self.min_height = Some(h);
78        self
79    }
80
81    /// Upper-bound the resolved height in logical pixels. See
82    /// [`Self::max_width`] for the semantic.
83    pub fn max_height(mut self, h: f32) -> Self {
84        self.max_height = Some(h);
85        self
86    }
87
88    /// Set the t-shirt size for stock controls.
89    pub fn size(mut self, size: ComponentSize) -> Self {
90        self.component_size = Some(size);
91        self
92    }
93
94    pub fn medium(self) -> Self {
95        self.size(ComponentSize::Md)
96    }
97
98    pub fn large(self) -> Self {
99        self.size(ComponentSize::Lg)
100    }
101
102    /// Set the theme-facing stock metrics role for this widget.
103    pub fn metrics_role(mut self, role: MetricsRole) -> Self {
104        self.metrics_role = Some(role);
105        self
106    }
107
108    // ---- Layout (container) ----
109    pub fn padding(mut self, p: impl Into<Sides>) -> Self {
110        self.padding = p.into();
111        self.explicit_padding = true;
112        self
113    }
114
115    /// Override only the top padding side, preserving the other three
116    /// sides at their current value (whether from a constructor's
117    /// `default_padding` or a previous explicit `.padding(...)`).
118    /// Mirrors Tailwind's `pt-N`. Marks the padding as explicit, so
119    /// the metrics pass will not stamp a density-driven value over it.
120    pub fn pt(mut self, v: f32) -> Self {
121        self.padding.top = v;
122        self.explicit_padding = true;
123        self
124    }
125
126    /// Override only the bottom padding side. Mirrors Tailwind's `pb-N`.
127    /// See [`Self::pt`] for composition semantics.
128    pub fn pb(mut self, v: f32) -> Self {
129        self.padding.bottom = v;
130        self.explicit_padding = true;
131        self
132    }
133
134    /// Override only the left padding side. Mirrors Tailwind's `pl-N`.
135    /// See [`Self::pt`] for composition semantics.
136    pub fn pl(mut self, v: f32) -> Self {
137        self.padding.left = v;
138        self.explicit_padding = true;
139        self
140    }
141
142    /// Override only the right padding side. Mirrors Tailwind's `pr-N`.
143    /// See [`Self::pt`] for composition semantics.
144    pub fn pr(mut self, v: f32) -> Self {
145        self.padding.right = v;
146        self.explicit_padding = true;
147        self
148    }
149
150    /// Override the horizontal padding sides (left + right), preserving
151    /// `top` and `bottom`. Mirrors Tailwind's `px-N`.
152    /// See [`Self::pt`] for composition semantics.
153    pub fn px(mut self, v: f32) -> Self {
154        self.padding.left = v;
155        self.padding.right = v;
156        self.explicit_padding = true;
157        self
158    }
159
160    /// Override the vertical padding sides (top + bottom), preserving
161    /// `left` and `right`. Mirrors Tailwind's `py-N`.
162    /// See [`Self::pt`] for composition semantics.
163    pub fn py(mut self, v: f32) -> Self {
164        self.padding.top = v;
165        self.padding.bottom = v;
166        self.explicit_padding = true;
167        self
168    }
169
170    pub fn gap(mut self, g: f32) -> Self {
171        self.gap = g;
172        self.explicit_gap = true;
173        self
174    }
175
176    pub fn align(mut self, a: Align) -> Self {
177        self.align = a;
178        self
179    }
180
181    pub fn justify(mut self, j: Justify) -> Self {
182        self.justify = j;
183        self
184    }
185
186    pub fn clip(mut self) -> Self {
187        self.clip = true;
188        self
189    }
190
191    pub fn scrollable(mut self) -> Self {
192        self.scrollable = true;
193        self
194    }
195
196    /// Show a draggable vertical scrollbar thumb when this scrollable
197    /// node's content overflows.
198    pub fn scrollbar(mut self) -> Self {
199        self.scrollbar = true;
200        self
201    }
202
203    /// Suppress the default scrollbar thumb on this scrollable node.
204    pub fn no_scrollbar(mut self) -> Self {
205        self.scrollbar = false;
206        self
207    }
208
209    /// Stick this scroll viewport's offset to the tail of its content
210    /// the way chat logs and activity feeds do — when new children land
211    /// below the current bottom, the offset follows them; when the user
212    /// scrolls up, the pin releases; when the user scrolls back to the
213    /// bottom, it re-engages. Mirrors `egui::ScrollArea::stick_to_bottom`.
214    ///
215    /// On first layout the offset starts at `max_offset`, so a freshly
216    /// mounted `scroll([...]).pin_end()` paints with its tail visible
217    /// rather than its head. Programmatic
218    /// [`crate::scroll::ScrollRequest::EnsureVisible`] requests that
219    /// resolve away from the tail also release the pin, so a
220    /// "jump-to-message N" action behaves as the user expects.
221    pub fn pin_end(mut self) -> Self {
222        self.pin_policy = crate::tree::PinPolicy::End;
223        self
224    }
225
226    /// Stick this scroll viewport's offset to the head of its content —
227    /// useful for virtual lists whose newest rows arrive at the top
228    /// (commit logs, reverse-chronological activity feeds). When the
229    /// pin is engaged the offset stays at `0` so the new rows stay
230    /// visible; the user scrolling down releases the pin, and
231    /// scrolling back to the top re-engages it. The symmetric
232    /// counterpart to [`Self::pin_end`].
233    ///
234    /// On first layout the offset is `0` (which is also the default
235    /// without any pin), so the visible effect of `pin_start` only
236    /// kicks in across rebuilds — in particular it overrides a
237    /// dynamic virtual list's anchor preservation so newly prepended
238    /// rows don't get hidden by the anchor that was preserving the
239    /// previously-visible row.
240    pub fn pin_start(mut self) -> Self {
241        self.pin_policy = crate::tree::PinPolicy::Start;
242        self
243    }
244
245    /// Override how a dynamic virtual list chooses the in-viewport row
246    /// point that anchors the next frame.
247    pub fn virtual_anchor_policy(mut self, policy: VirtualAnchorPolicy) -> Self {
248        if let Some(items) = self.virtual_items.take() {
249            self.virtual_items = Some(items.anchor_policy(policy));
250        }
251        self
252    }
253
254    /// Treat this element's focusable children as a single
255    /// arrow-navigable group.
256    pub fn arrow_nav_siblings(mut self) -> Self {
257        self.arrow_nav_siblings = true;
258        self
259    }
260
261    /// Replace the column/row/overlay distribution for this node with
262    /// a custom child layout function.
263    pub fn layout<F>(mut self, f: F) -> Self
264    where
265        F: Fn(LayoutCtx) -> Vec<Rect> + Send + Sync + 'static,
266    {
267        self.layout_override = Some(LayoutFn::new(f));
268        self
269    }
270
271    // ---- Children ----
272    pub fn child(mut self, c: impl Into<El>) -> Self {
273        self.children.push(c.into());
274        self
275    }
276
277    pub fn children<I, E>(mut self, cs: I) -> Self
278    where
279        I: IntoIterator<Item = E>,
280        E: Into<El>,
281    {
282        self.children.extend(cs.into_iter().map(Into::into));
283        self
284    }
285
286    /// Set the layout axis directly.
287    pub fn axis(mut self, a: Axis) -> Self {
288        self.axis = a;
289        self
290    }
291
292    // ---- Internal stock defaults ----
293    pub(crate) fn default_width(mut self, w: Size) -> Self {
294        self.width = w;
295        self.explicit_width = false;
296        self
297    }
298
299    pub(crate) fn default_height(mut self, h: Size) -> Self {
300        self.height = h;
301        self.explicit_height = false;
302        self
303    }
304
305    pub(crate) fn default_padding(mut self, p: impl Into<Sides>) -> Self {
306        self.padding = p.into();
307        self.explicit_padding = false;
308        self
309    }
310
311    pub(crate) fn default_gap(mut self, g: f32) -> Self {
312        self.gap = g;
313        self.explicit_gap = false;
314        self
315    }
316}
317
318#[cfg(test)]
319mod tests {
320    use super::*;
321    use crate::tree::Kind;
322
323    fn fresh() -> El {
324        El::new(Kind::Group)
325    }
326
327    #[test]
328    fn pt_sets_only_top_and_marks_explicit() {
329        let el = fresh().pt(7.0);
330        assert_eq!(
331            el.padding,
332            Sides {
333                left: 0.0,
334                right: 0.0,
335                top: 7.0,
336                bottom: 0.0
337            }
338        );
339        assert!(el.explicit_padding);
340    }
341
342    #[test]
343    fn px_py_set_only_their_axis() {
344        let el = fresh().px(4.0).py(2.0);
345        assert_eq!(
346            el.padding,
347            Sides {
348                left: 4.0,
349                right: 4.0,
350                top: 2.0,
351                bottom: 2.0
352            }
353        );
354        assert!(el.explicit_padding);
355    }
356
357    #[test]
358    fn pt_overrides_only_top_when_following_padding() {
359        // Tailwind shape: `p-4 pt-0` keeps left/right/bottom at 4 and zeros only top.
360        let el = fresh().padding(4.0).pt(0.0);
361        assert_eq!(
362            el.padding,
363            Sides {
364                left: 4.0,
365                right: 4.0,
366                top: 0.0,
367                bottom: 4.0
368            }
369        );
370        assert!(el.explicit_padding);
371    }
372
373    #[test]
374    fn pt_after_default_padding_preserves_other_sides_and_marks_explicit() {
375        // Constructor default of all-4, then author overrides just the top to 0.
376        // Other sides keep the default's value; explicit_padding flips so the
377        // metrics pass cannot stamp over the override.
378        let el = fresh().default_padding(4.0).pt(0.0);
379        assert_eq!(
380            el.padding,
381            Sides {
382                left: 4.0,
383                right: 4.0,
384                top: 0.0,
385                bottom: 4.0
386            }
387        );
388        assert!(el.explicit_padding);
389    }
390
391    #[test]
392    fn per_side_chainables_compose() {
393        let el = fresh().pl(1.0).pr(2.0).pt(3.0).pb(4.0);
394        assert_eq!(
395            el.padding,
396            Sides {
397                left: 1.0,
398                right: 2.0,
399                top: 3.0,
400                bottom: 4.0
401            }
402        );
403        assert!(el.explicit_padding);
404    }
405
406    #[test]
407    fn fill_width_sets_only_width_to_fill_one() {
408        let el = fresh().fill_width();
409        assert_eq!(el.width, Size::Fill(1.0));
410        assert!(el.explicit_width);
411        assert!(!el.explicit_height);
412    }
413
414    #[test]
415    fn fill_height_sets_only_height_to_fill_one() {
416        let el = fresh().fill_height();
417        assert_eq!(el.height, Size::Fill(1.0));
418        assert!(el.explicit_height);
419        assert!(!el.explicit_width);
420    }
421
422    #[test]
423    fn fill_width_and_fill_height_compose_to_fill_size() {
424        let combined = fresh().fill_width().fill_height();
425        let either = fresh().fill_size();
426        assert_eq!(combined.width, either.width);
427        assert_eq!(combined.height, either.height);
428        assert_eq!(combined.explicit_width, either.explicit_width);
429        assert_eq!(combined.explicit_height, either.explicit_height);
430    }
431
432    #[test]
433    fn sides_x_and_y_constructors_only_populate_one_axis() {
434        assert_eq!(
435            Sides::x(5.0),
436            Sides {
437                left: 5.0,
438                right: 5.0,
439                top: 0.0,
440                bottom: 0.0
441            }
442        );
443        assert_eq!(
444            Sides::y(5.0),
445            Sides {
446                left: 0.0,
447                right: 0.0,
448                top: 5.0,
449                bottom: 5.0
450            }
451        );
452    }
453}