Skip to main content

jag_ui/elements/
container.rs

1//! Scrollable container element.
2
3use jag_draw::{Brush, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::KeyboardEvent;
7use crate::event::{EventHandler, EventResult, MouseClickEvent, MouseMoveEvent, ScrollEvent};
8use crate::focus::FocusId;
9
10use super::Element;
11
12/// A container that can hold children, with optional background, border,
13/// padding, and scroll support.
14///
15/// Containers are not focusable. They handle scroll events to update
16/// their internal scroll offset, and provide a `content_rect()` for
17/// callers to position children.
18pub struct Container {
19    pub rect: Rect,
20    /// Background color (transparent by default).
21    pub bg: Option<ColorLinPremul>,
22    /// Border color.
23    pub border_color: ColorLinPremul,
24    /// Border width in logical pixels (0 = no border).
25    pub border_width: f32,
26    /// Corner radius for background and border.
27    pub radius: f32,
28    /// Padding: [top, right, bottom, left].
29    pub padding: [f32; 4],
30    /// Current horizontal scroll offset.
31    pub scroll_x: f32,
32    /// Current vertical scroll offset.
33    pub scroll_y: f32,
34    /// Total content width (may exceed rect width).
35    pub content_width: f32,
36    /// Total content height (may exceed rect height).
37    pub content_height: f32,
38}
39
40impl Container {
41    /// Create a container with default styling.
42    pub fn new(rect: Rect) -> Self {
43        Self {
44            rect,
45            bg: None,
46            border_color: ColorLinPremul::from_srgba_u8([200, 200, 200, 255]),
47            border_width: 0.0,
48            radius: 0.0,
49            padding: [0.0; 4],
50            scroll_x: 0.0,
51            scroll_y: 0.0,
52            content_width: 0.0,
53            content_height: 0.0,
54        }
55    }
56
57    /// The inner content rectangle (rect minus padding).
58    pub fn content_rect(&self) -> Rect {
59        let pad_top = self.padding[0];
60        let pad_right = self.padding[1];
61        let pad_bottom = self.padding[2];
62        let pad_left = self.padding[3];
63        Rect {
64            x: self.rect.x + pad_left,
65            y: self.rect.y + pad_top,
66            w: (self.rect.w - pad_left - pad_right).max(0.0),
67            h: (self.rect.h - pad_top - pad_bottom).max(0.0),
68        }
69    }
70
71    /// Maximum horizontal scroll value (zero when content fits).
72    pub fn max_scroll_x(&self) -> f32 {
73        let inner_w = self.content_rect().w;
74        (self.content_width - inner_w).max(0.0)
75    }
76
77    /// Maximum vertical scroll value (zero when content fits).
78    pub fn max_scroll_y(&self) -> f32 {
79        let inner_h = self.content_rect().h;
80        (self.content_height - inner_h).max(0.0)
81    }
82
83    /// Clamp scroll offsets to valid ranges.
84    pub fn clamp_scroll(&mut self) {
85        self.scroll_x = self.scroll_x.clamp(0.0, self.max_scroll_x());
86        self.scroll_y = self.scroll_y.clamp(0.0, self.max_scroll_y());
87    }
88
89    /// Hit-test: is `(x, y)` inside the container rect?
90    pub fn hit_test(&self, x: f32, y: f32) -> bool {
91        x >= self.rect.x
92            && x <= self.rect.x + self.rect.w
93            && y >= self.rect.y
94            && y <= self.rect.y + self.rect.h
95    }
96}
97
98// ---------------------------------------------------------------------------
99// Element trait
100// ---------------------------------------------------------------------------
101
102impl Element for Container {
103    fn rect(&self) -> Rect {
104        self.rect
105    }
106
107    fn set_rect(&mut self, rect: Rect) {
108        self.rect = rect;
109    }
110
111    fn render(&self, canvas: &mut Canvas, z: i32) {
112        // Background
113        if let Some(bg) = self.bg {
114            if self.radius > 0.0 {
115                let rrect = RoundedRect {
116                    rect: self.rect,
117                    radii: RoundedRadii {
118                        tl: self.radius,
119                        tr: self.radius,
120                        br: self.radius,
121                        bl: self.radius,
122                    },
123                };
124                canvas.rounded_rect(rrect, Brush::Solid(bg), z);
125            } else {
126                canvas.fill_rect(
127                    self.rect.x,
128                    self.rect.y,
129                    self.rect.w,
130                    self.rect.h,
131                    Brush::Solid(bg),
132                    z,
133                );
134            }
135        }
136
137        // Border
138        if self.border_width > 0.0 {
139            let rrect = RoundedRect {
140                rect: self.rect,
141                radii: RoundedRadii {
142                    tl: self.radius,
143                    tr: self.radius,
144                    br: self.radius,
145                    bl: self.radius,
146                },
147            };
148            jag_surface::shapes::draw_snapped_rounded_rectangle(
149                canvas,
150                rrect,
151                None,
152                Some(self.border_width),
153                Some(Brush::Solid(self.border_color)),
154                z + 1,
155            );
156        }
157    }
158
159    /// Containers are not focusable.
160    fn focus_id(&self) -> Option<FocusId> {
161        None
162    }
163}
164
165// ---------------------------------------------------------------------------
166// EventHandler trait
167// ---------------------------------------------------------------------------
168
169impl EventHandler for Container {
170    fn handle_mouse_click(&mut self, _event: &MouseClickEvent) -> EventResult {
171        EventResult::Ignored
172    }
173
174    fn handle_keyboard(&mut self, _event: &KeyboardEvent) -> EventResult {
175        EventResult::Ignored
176    }
177
178    fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
179        EventResult::Ignored
180    }
181
182    fn handle_scroll(&mut self, event: &ScrollEvent) -> EventResult {
183        if !self.hit_test(event.x, event.y) {
184            return EventResult::Ignored;
185        }
186        self.scroll_x += event.delta_x;
187        self.scroll_y += event.delta_y;
188        self.clamp_scroll();
189        EventResult::Handled
190    }
191
192    fn is_focused(&self) -> bool {
193        false
194    }
195
196    fn set_focused(&mut self, _focused: bool) {
197        // Containers are not focusable.
198    }
199
200    fn contains_point(&self, x: f32, y: f32) -> bool {
201        self.hit_test(x, y)
202    }
203}
204
205// ---------------------------------------------------------------------------
206// Tests
207// ---------------------------------------------------------------------------
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn container_content_rect_with_padding() {
215        let mut c = Container::new(Rect {
216            x: 10.0,
217            y: 20.0,
218            w: 200.0,
219            h: 100.0,
220        });
221        c.padding = [5.0, 10.0, 5.0, 10.0];
222        let cr = c.content_rect();
223        assert!((cr.x - 20.0).abs() < f32::EPSILON);
224        assert!((cr.y - 25.0).abs() < f32::EPSILON);
225        assert!((cr.w - 180.0).abs() < f32::EPSILON);
226        assert!((cr.h - 90.0).abs() < f32::EPSILON);
227    }
228
229    #[test]
230    fn container_scroll_clamp() {
231        let mut c = Container::new(Rect {
232            x: 0.0,
233            y: 0.0,
234            w: 100.0,
235            h: 100.0,
236        });
237        c.content_width = 200.0;
238        c.content_height = 300.0;
239        c.scroll_x = 500.0;
240        c.scroll_y = 500.0;
241        c.clamp_scroll();
242        assert!((c.scroll_x - 100.0).abs() < f32::EPSILON);
243        assert!((c.scroll_y - 200.0).abs() < f32::EPSILON);
244    }
245
246    #[test]
247    fn container_not_focusable() {
248        let c = Container::new(Rect {
249            x: 0.0,
250            y: 0.0,
251            w: 50.0,
252            h: 50.0,
253        });
254        assert!(c.focus_id().is_none());
255        assert!(!c.is_focused());
256    }
257
258    #[test]
259    fn container_hit_test() {
260        let c = Container::new(Rect {
261            x: 10.0,
262            y: 10.0,
263            w: 100.0,
264            h: 80.0,
265        });
266        assert!(c.hit_test(50.0, 50.0));
267        assert!(!c.hit_test(0.0, 0.0));
268    }
269
270    #[test]
271    fn container_scroll_event_handling() {
272        let mut c = Container::new(Rect {
273            x: 0.0,
274            y: 0.0,
275            w: 100.0,
276            h: 100.0,
277        });
278        c.content_height = 300.0;
279        let evt = ScrollEvent {
280            x: 50.0,
281            y: 50.0,
282            delta_x: 0.0,
283            delta_y: 30.0,
284        };
285        let result = c.handle_scroll(&evt);
286        assert_eq!(result, EventResult::Handled);
287        assert!((c.scroll_y - 30.0).abs() < f32::EPSILON);
288    }
289}