Skip to main content

agg_gui/widgets/
container.rs

1//! `Container` — a rectangular box with optional background, border, and
2//! padding that holds zero or more child widgets.
3//!
4//! Phase 4 child layout is a simple top-down vertical stack (bottom-most child
5//! at `y = padding`, each subsequent child placed above the previous). Flex
6//! layout arrives in Phase 5.
7
8use crate::color::Color;
9use crate::device_scale::device_scale;
10use crate::draw_ctx::DrawCtx;
11use crate::event::{Event, EventResult};
12use crate::geometry::{Rect, Size};
13use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
14use crate::widget::Widget;
15
16/// A rectangular container widget.
17///
18/// Paints a background rounded-rect (optional border), then lets the framework
19/// recurse into its children. Children are stacked bottom-to-top inside the
20/// padding area.
21pub struct Container {
22    bounds: Rect,
23    children: Vec<Box<dyn Widget>>,
24    base: WidgetBase,
25    pub background: Color,
26    pub border_color: Option<Color>,
27    pub border_width: f64,
28    pub corner_radius: f64,
29    pub inner_padding: Insets,
30    /// When `true`, `layout` returns the content's natural height + vertical
31    /// padding instead of the full available height.  Off by default for
32    /// backward compatibility (callers that used `Container` as a fill-
33    /// parent decoration still work).  Match egui's `Frame` by opting in.
34    pub fit_height: bool,
35}
36
37impl Container {
38    /// Create a transparent container with no border and default padding.
39    pub fn new() -> Self {
40        Self {
41            bounds: Rect::default(),
42            children: Vec::new(),
43            base: WidgetBase::new(),
44            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
45            border_color: None,
46            border_width: 1.0,
47            corner_radius: 0.0,
48            inner_padding: Insets::ZERO,
49            fit_height: false,
50        }
51    }
52
53    /// Opt into content-fit height — [`layout`] returns
54    /// `content_height + vertical_padding` instead of the full
55    /// available height.  Required when this `Container` sits inside
56    /// an auto-sized ancestor (e.g. `Window::with_auto_size(true)`),
57    /// which would otherwise pick up the full available height as
58    /// the container's preferred size and inflate the window.
59    pub fn with_fit_height(mut self, fit: bool) -> Self {
60        self.fit_height = fit;
61        self
62    }
63
64    /// Append a child widget.
65    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
66        self.children.push(child);
67        self
68    }
69
70    pub fn with_background(mut self, color: Color) -> Self {
71        self.background = color;
72        self
73    }
74
75    pub fn with_border(mut self, color: Color, width: f64) -> Self {
76        self.border_color = Some(color);
77        self.border_width = width;
78        self
79    }
80
81    pub fn with_corner_radius(mut self, r: f64) -> Self {
82        self.corner_radius = r;
83        self
84    }
85
86    pub fn with_padding(mut self, p: f64) -> Self {
87        self.inner_padding = Insets::all(p);
88        self
89    }
90
91    pub fn with_inner_padding(mut self, p: Insets) -> Self {
92        self.inner_padding = p;
93        self
94    }
95
96    pub fn with_margin(mut self, m: Insets) -> Self {
97        self.base.margin = m;
98        self
99    }
100    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
101        self.base.h_anchor = h;
102        self
103    }
104    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
105        self.base.v_anchor = v;
106        self
107    }
108    pub fn with_min_size(mut self, s: Size) -> Self {
109        self.base.min_size = s;
110        self
111    }
112    pub fn with_max_size(mut self, s: Size) -> Self {
113        self.base.max_size = s;
114        self
115    }
116}
117
118impl Default for Container {
119    fn default() -> Self {
120        Self::new()
121    }
122}
123
124impl Widget for Container {
125    fn type_name(&self) -> &'static str {
126        "Container"
127    }
128    fn bounds(&self) -> Rect {
129        self.bounds
130    }
131    fn set_bounds(&mut self, bounds: Rect) {
132        self.bounds = bounds;
133    }
134
135    fn children(&self) -> &[Box<dyn Widget>] {
136        &self.children
137    }
138    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
139        &mut self.children
140    }
141
142    fn margin(&self) -> Insets {
143        self.base.margin
144    }
145    fn h_anchor(&self) -> HAnchor {
146        self.base.h_anchor
147    }
148    fn v_anchor(&self) -> VAnchor {
149        self.base.v_anchor
150    }
151    fn min_size(&self) -> Size {
152        self.base.min_size
153    }
154    fn max_size(&self) -> Size {
155        self.base.max_size
156    }
157
158    fn layout(&mut self, available: Size) -> Size {
159        let pad_l = self.inner_padding.left;
160        let pad_r = self.inner_padding.right;
161        let pad_t = self.inner_padding.top;
162        let pad_b = self.inner_padding.bottom;
163        let inner_w = (available.width - pad_l - pad_r).max(0.0);
164
165        // Stack children top-to-bottom (first child = visually highest).
166        // In Y-up coordinates, "top" = higher Y values.
167        // Start cursor at the top of the inner area; move it downward each step.
168        // Child margins are additive: top margin pushes the cursor down before
169        // placing the child; bottom margin is consumed after it.
170        let scale = device_scale();
171        let start_cursor = available.height - pad_t;
172        let mut cursor_y = start_cursor;
173
174        for child in self.children.iter_mut() {
175            let m = child.margin().scale(scale);
176            let avail_w = (inner_w - m.left - m.right).max(0.0);
177            let avail_h = (cursor_y - pad_b - m.top - m.bottom).max(0.0);
178            let desired = child.layout(Size::new(avail_w, avail_h));
179
180            // Top margin moves the cursor down before the child is placed.
181            cursor_y -= m.top;
182            let child_y = cursor_y - desired.height;
183            let child_bounds = Rect::new(
184                pad_l + m.left,
185                child_y,
186                desired.width.min(avail_w),
187                desired.height,
188            );
189            child.set_bounds(child_bounds);
190            // Bottom margin is consumed below the child.
191            cursor_y = child_y - m.bottom;
192        }
193
194        // Default: fill the full available area (legacy — many demo
195        // sites use `Container` as a decorated wrapper around content
196        // that should stretch).  Opt in to content-fit via
197        // `with_fit_height(true)` — matches egui `Frame` semantics.
198        if self.fit_height {
199            let consumed_h = (start_cursor - cursor_y).max(0.0);
200            let natural_h = (consumed_h + pad_t + pad_b).min(available.height);
201            Size::new(available.width, natural_h)
202        } else {
203            Size::new(available.width, available.height)
204        }
205    }
206
207    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
208        let w = self.bounds.width;
209        let h = self.bounds.height;
210        let r = self.corner_radius;
211
212        // Background
213        if self.background.a > 0.001 {
214            ctx.set_fill_color(self.background);
215            ctx.begin_path();
216            ctx.rounded_rect(0.0, 0.0, w, h, r);
217            ctx.fill();
218        }
219
220        // Border
221        if let Some(bc) = self.border_color {
222            ctx.set_stroke_color(bc);
223            ctx.set_line_width(self.border_width);
224            ctx.begin_path();
225            let inset = self.border_width * 0.5;
226            ctx.rounded_rect(
227                inset,
228                inset,
229                (w - self.border_width).max(0.0),
230                (h - self.border_width).max(0.0),
231                r,
232            );
233            ctx.stroke();
234        }
235    }
236
237    fn on_event(&mut self, _event: &Event) -> EventResult {
238        EventResult::Ignored
239    }
240}