1use 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
12pub struct Container {
19 pub rect: Rect,
20 pub bg: Option<ColorLinPremul>,
22 pub border_color: ColorLinPremul,
24 pub border_width: f32,
26 pub radius: f32,
28 pub padding: [f32; 4],
30 pub scroll_x: f32,
32 pub scroll_y: f32,
34 pub content_width: f32,
36 pub content_height: f32,
38}
39
40impl Container {
41 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 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 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 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 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 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
98impl 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 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 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 fn focus_id(&self) -> Option<FocusId> {
161 None
162 }
163}
164
165impl 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 }
199
200 fn contains_point(&self, x: f32, y: f32) -> bool {
201 self.hit_test(x, y)
202 }
203}
204
205#[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}