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::draw_ctx::DrawCtx;
10use crate::event::{Event, EventResult};
11use crate::geometry::{Rect, Size};
12use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
13use crate::widget::Widget;
14
15/// Inspector-visible properties of a [`Container`].
16#[cfg_attr(feature = "reflect", derive(bevy_reflect::Reflect))]
17#[derive(Clone, Debug)]
18pub struct ContainerProps {
19    pub background: Color,
20    pub border_color: Option<Color>,
21    pub border_width: f64,
22    pub corner_radius: f64,
23    pub inner_padding: Insets,
24    /// When `true`, `layout` returns the content's natural height + vertical
25    /// padding instead of the full available height.  Off by default for
26    /// backward compatibility (callers that used `Container` as a fill-
27    /// parent decoration still work).  Match egui's `Frame` by opting in.
28    pub fit_height: bool,
29}
30
31impl Default for ContainerProps {
32    fn default() -> Self {
33        Self {
34            background: Color::rgba(0.0, 0.0, 0.0, 0.0),
35            border_color: None,
36            border_width: 1.0,
37            corner_radius: 0.0,
38            inner_padding: Insets::ZERO,
39            fit_height: false,
40        }
41    }
42}
43
44/// A rectangular container widget.
45///
46/// Paints a background rounded-rect (optional border), then lets the framework
47/// recurse into its children. Children are stacked bottom-to-top inside the
48/// padding area.
49pub struct Container {
50    bounds: Rect,
51    children: Vec<Box<dyn Widget>>,
52    base: WidgetBase,
53    pub props: ContainerProps,
54}
55
56impl Container {
57    /// Create a transparent container with no border and default padding.
58    pub fn new() -> Self {
59        Self {
60            bounds: Rect::default(),
61            children: Vec::new(),
62            base: WidgetBase::new(),
63            props: ContainerProps::default(),
64        }
65    }
66
67    /// Opt into content-fit height — [`layout`] returns
68    /// `content_height + vertical_padding` instead of the full
69    /// available height.  Required when this `Container` sits inside
70    /// an auto-sized ancestor (e.g. `Window::with_auto_size(true)`),
71    /// which would otherwise pick up the full available height as
72    /// the container's preferred size and inflate the window.
73    pub fn with_fit_height(mut self, fit: bool) -> Self {
74        self.props.fit_height = fit;
75        self
76    }
77
78    /// Append a child widget.
79    pub fn add(mut self, child: Box<dyn Widget>) -> Self {
80        self.children.push(child);
81        self
82    }
83
84    pub fn with_background(mut self, color: Color) -> Self {
85        self.props.background = color;
86        self
87    }
88
89    pub fn with_border(mut self, color: Color, width: f64) -> Self {
90        self.props.border_color = Some(color);
91        self.props.border_width = width;
92        self
93    }
94
95    pub fn with_corner_radius(mut self, r: f64) -> Self {
96        self.props.corner_radius = r;
97        self
98    }
99
100    pub fn with_padding(mut self, p: f64) -> Self {
101        self.props.inner_padding = Insets::all(p);
102        self
103    }
104
105    pub fn with_inner_padding(mut self, p: Insets) -> Self {
106        self.props.inner_padding = p;
107        self
108    }
109
110    pub fn with_margin(mut self, m: Insets) -> Self {
111        self.base.margin = m;
112        self
113    }
114    pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
115        self.base.h_anchor = h;
116        self
117    }
118    pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
119        self.base.v_anchor = v;
120        self
121    }
122    pub fn with_min_size(mut self, s: Size) -> Self {
123        self.base.min_size = s;
124        self
125    }
126    pub fn with_max_size(mut self, s: Size) -> Self {
127        self.base.max_size = s;
128        self
129    }
130}
131
132impl Default for Container {
133    fn default() -> Self {
134        Self::new()
135    }
136}
137
138impl Widget for Container {
139    fn type_name(&self) -> &'static str {
140        "Container"
141    }
142    fn bounds(&self) -> Rect {
143        self.bounds
144    }
145    fn set_bounds(&mut self, bounds: Rect) {
146        self.bounds = bounds;
147    }
148
149    fn children(&self) -> &[Box<dyn Widget>] {
150        &self.children
151    }
152    fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
153        &mut self.children
154    }
155
156    #[cfg(feature = "reflect")]
157    fn as_reflect(&self) -> Option<&dyn bevy_reflect::Reflect> {
158        Some(&self.props)
159    }
160    #[cfg(feature = "reflect")]
161    fn as_reflect_mut(&mut self) -> Option<&mut dyn bevy_reflect::Reflect> {
162        Some(&mut self.props)
163    }
164
165    fn margin(&self) -> Insets {
166        self.base.margin
167    }
168    fn widget_base(&self) -> Option<&WidgetBase> {
169        Some(&self.base)
170    }
171    fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
172        Some(&mut self.base)
173    }
174    fn padding(&self) -> Insets {
175        self.props.inner_padding
176    }
177    fn h_anchor(&self) -> HAnchor {
178        self.base.h_anchor
179    }
180    fn v_anchor(&self) -> VAnchor {
181        self.base.v_anchor
182    }
183    fn min_size(&self) -> Size {
184        self.base.min_size
185    }
186    fn max_size(&self) -> Size {
187        self.base.max_size
188    }
189
190    fn layout(&mut self, available: Size) -> Size {
191        let pad_l = self.props.inner_padding.left;
192        let pad_r = self.props.inner_padding.right;
193        let pad_t = self.props.inner_padding.top;
194        let pad_b = self.props.inner_padding.bottom;
195        let inner_w = (available.width - pad_l - pad_r).max(0.0);
196
197        fn layout_children(
198            children: &mut [Box<dyn Widget>],
199            inner_w: f64,
200            pad_l: f64,
201            pad_t: f64,
202            pad_b: f64,
203            height: f64,
204        ) -> f64 {
205            // Stack children top-to-bottom (first child = visually highest).
206            // In Y-up coordinates, "top" = higher Y values.
207            let start_cursor = height - pad_t;
208            let mut cursor_y = start_cursor;
209
210            for child in children.iter_mut() {
211                // Margins are logical units; DPI is applied at the App
212                // paint boundary, never during layout.
213                let m = child.margin();
214                let avail_w = (inner_w - m.left - m.right).max(0.0);
215                let avail_h = (cursor_y - pad_b - m.top - m.bottom).max(0.0);
216                let desired = child.layout(Size::new(avail_w, avail_h));
217
218                cursor_y -= m.top;
219                let child_y = cursor_y - desired.height;
220                child.set_bounds(Rect::new(
221                    pad_l + m.left,
222                    child_y,
223                    desired.width.min(avail_w),
224                    desired.height,
225                ));
226                cursor_y = child_y - m.bottom;
227            }
228
229            (start_cursor - cursor_y).max(0.0)
230        }
231
232        let consumed_h = layout_children(
233            &mut self.children,
234            inner_w,
235            pad_l,
236            pad_t,
237            pad_b,
238            available.height,
239        );
240
241        // Default: fill the full available area (legacy — many demo
242        // sites use `Container` as a decorated wrapper around content
243        // that should stretch).  Opt in to content-fit via
244        // `with_fit_height(true)` — matches egui `Frame` semantics.
245        if self.props.fit_height {
246            let natural_h = (consumed_h + pad_t + pad_b).min(available.height);
247            // The first pass measured content from the parent-provided height.
248            // A fit-height container paints at `natural_h`, so lay children out
249            // again in that tight height to keep their bounds inside the frame.
250            if (available.height - natural_h).abs() > 0.5 {
251                layout_children(
252                    &mut self.children,
253                    inner_w,
254                    pad_l,
255                    pad_t,
256                    pad_b,
257                    natural_h,
258                );
259            }
260            Size::new(available.width, natural_h)
261        } else {
262            Size::new(available.width, available.height)
263        }
264    }
265
266    fn paint(&mut self, ctx: &mut dyn DrawCtx) {
267        let w = self.bounds.width;
268        let h = self.bounds.height;
269        let r = self.props.corner_radius;
270
271        // Background
272        if self.props.background.a > 0.001 {
273            ctx.set_fill_color(self.props.background);
274            ctx.begin_path();
275            ctx.rounded_rect(0.0, 0.0, w, h, r);
276            ctx.fill();
277        }
278
279        // Border
280        if let Some(bc) = self.props.border_color {
281            ctx.set_stroke_color(bc);
282            ctx.set_line_width(self.props.border_width);
283            ctx.begin_path();
284            let inset = self.props.border_width * 0.5;
285            ctx.rounded_rect(
286                inset,
287                inset,
288                (w - self.props.border_width).max(0.0),
289                (h - self.props.border_width).max(0.0),
290                r,
291            );
292            ctx.stroke();
293        }
294    }
295
296    fn on_event(&mut self, _event: &Event) -> EventResult {
297        EventResult::Ignored
298    }
299}