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