1use jag_draw::{Brush, ColorLinPremul, Rect, RoundedRadii, RoundedRect};
4use jag_surface::Canvas;
5
6use crate::event::{
7 EventHandler, EventResult, KeyboardEvent, MouseClickEvent, MouseMoveEvent, ScrollEvent,
8};
9use crate::focus::FocusId;
10
11use super::Element;
12
13#[derive(Debug, Clone, Copy)]
15pub struct CardLayout {
16 pub header: Rect,
17 pub content: Rect,
18}
19
20pub struct Card {
22 pub rect: Rect,
23 pub title: Option<String>,
25 pub title_size: f32,
27 pub title_color: ColorLinPremul,
29 pub bg: ColorLinPremul,
31 pub border_color: ColorLinPremul,
33 pub border_width: f32,
35 pub radius: f32,
37 pub header_height: f32,
39 pub show_shadow: bool,
41 pub shadow_color: ColorLinPremul,
43 pub shadow_offset: [f32; 2],
45 pub shadow_spread: f32,
47}
48
49impl Card {
50 pub fn new(rect: Rect) -> Self {
52 Self {
53 rect,
54 title: None,
55 title_size: 16.0,
56 title_color: ColorLinPremul::from_srgba_u8([20, 20, 20, 255]),
57 bg: ColorLinPremul::from_srgba_u8([255, 255, 255, 255]),
58 border_color: ColorLinPremul::from_srgba_u8([226, 232, 240, 255]),
59 border_width: 1.0,
60 radius: 8.0,
61 header_height: 48.0,
62 show_shadow: true,
63 shadow_color: ColorLinPremul::from_srgba_u8([0, 0, 0, 30]),
64 shadow_offset: [0.0, 2.0],
65 shadow_spread: 4.0,
66 }
67 }
68
69 pub fn with_title(mut self, title: impl Into<String>) -> Self {
71 self.title = Some(title.into());
72 self
73 }
74
75 pub fn layout(&self) -> CardLayout {
77 let has_title = self.title.is_some();
78 let header_h = if has_title {
79 self.header_height.clamp(0.0, self.rect.h)
80 } else {
81 0.0
82 };
83 let content_h = (self.rect.h - header_h).max(0.0);
84
85 CardLayout {
86 header: Rect {
87 x: self.rect.x,
88 y: self.rect.y,
89 w: self.rect.w,
90 h: header_h,
91 },
92 content: Rect {
93 x: self.rect.x,
94 y: self.rect.y + header_h,
95 w: self.rect.w,
96 h: content_h,
97 },
98 }
99 }
100
101 pub fn hit_test(&self, x: f32, y: f32) -> bool {
103 x >= self.rect.x
104 && x <= self.rect.x + self.rect.w
105 && y >= self.rect.y
106 && y <= self.rect.y + self.rect.h
107 }
108}
109
110impl Element for Card {
115 fn rect(&self) -> Rect {
116 self.rect
117 }
118
119 fn set_rect(&mut self, rect: Rect) {
120 self.rect = rect;
121 }
122
123 fn render(&self, canvas: &mut Canvas, z: i32) {
124 if self.show_shadow {
126 let shadow_rect = Rect {
127 x: self.rect.x + self.shadow_offset[0] - self.shadow_spread,
128 y: self.rect.y + self.shadow_offset[1] - self.shadow_spread,
129 w: self.rect.w + self.shadow_spread * 2.0,
130 h: self.rect.h + self.shadow_spread * 2.0,
131 };
132 let shadow_rrect = RoundedRect {
133 rect: shadow_rect,
134 radii: RoundedRadii {
135 tl: self.radius + self.shadow_spread,
136 tr: self.radius + self.shadow_spread,
137 br: self.radius + self.shadow_spread,
138 bl: self.radius + self.shadow_spread,
139 },
140 };
141 canvas.rounded_rect(shadow_rrect, Brush::Solid(self.shadow_color), z);
142 }
143
144 let rrect = RoundedRect {
146 rect: self.rect,
147 radii: RoundedRadii {
148 tl: self.radius,
149 tr: self.radius,
150 br: self.radius,
151 bl: self.radius,
152 },
153 };
154
155 let border_w = if self.border_width > 0.0 {
156 Some(self.border_width)
157 } else {
158 None
159 };
160 let border_b = if self.border_width > 0.0 {
161 Some(Brush::Solid(self.border_color))
162 } else {
163 None
164 };
165
166 jag_surface::shapes::draw_snapped_rounded_rectangle(
167 canvas,
168 rrect,
169 Some(Brush::Solid(self.bg)),
170 border_w,
171 border_b,
172 z + 1,
173 );
174
175 if let Some(ref title) = self.title {
177 let layout = self.layout();
178 let text_x = layout.header.x + 16.0;
179 let text_y = layout.header.y + layout.header.h * 0.5 + self.title_size * 0.35;
180 canvas.draw_text_run_weighted(
181 [text_x, text_y],
182 title.clone(),
183 self.title_size,
184 600.0,
185 self.title_color,
186 z + 2,
187 );
188 }
189 }
190
191 fn focus_id(&self) -> Option<FocusId> {
193 None
194 }
195}
196
197impl EventHandler for Card {
202 fn handle_mouse_click(&mut self, _event: &MouseClickEvent) -> EventResult {
203 EventResult::Ignored
204 }
205
206 fn handle_keyboard(&mut self, _event: &KeyboardEvent) -> EventResult {
207 EventResult::Ignored
208 }
209
210 fn handle_mouse_move(&mut self, _event: &MouseMoveEvent) -> EventResult {
211 EventResult::Ignored
212 }
213
214 fn handle_scroll(&mut self, _event: &ScrollEvent) -> EventResult {
215 EventResult::Ignored
216 }
217
218 fn is_focused(&self) -> bool {
219 false
220 }
221
222 fn set_focused(&mut self, _focused: bool) {}
223
224 fn contains_point(&self, x: f32, y: f32) -> bool {
225 self.hit_test(x, y)
226 }
227}
228
229#[cfg(test)]
234mod tests {
235 use super::*;
236
237 #[test]
238 fn card_defaults() {
239 let card = Card::new(Rect {
240 x: 0.0,
241 y: 0.0,
242 w: 300.0,
243 h: 200.0,
244 });
245 assert!(card.title.is_none());
246 assert!(card.show_shadow);
247 assert!((card.radius - 8.0).abs() < f32::EPSILON);
248 }
249
250 #[test]
251 fn card_with_title() {
252 let card = Card::new(Rect {
253 x: 0.0,
254 y: 0.0,
255 w: 300.0,
256 h: 200.0,
257 })
258 .with_title("My Card");
259 assert_eq!(card.title.as_deref(), Some("My Card"));
260 }
261
262 #[test]
263 fn card_layout_with_title() {
264 let card = Card::new(Rect {
265 x: 10.0,
266 y: 20.0,
267 w: 300.0,
268 h: 200.0,
269 })
270 .with_title("Header");
271 let layout = card.layout();
272 assert!((layout.header.h - 48.0).abs() < f32::EPSILON);
273 assert!((layout.content.h - 152.0).abs() < f32::EPSILON);
274 assert!((layout.content.y - 68.0).abs() < f32::EPSILON);
275 }
276
277 #[test]
278 fn card_layout_without_title() {
279 let card = Card::new(Rect {
280 x: 0.0,
281 y: 0.0,
282 w: 300.0,
283 h: 200.0,
284 });
285 let layout = card.layout();
286 assert!((layout.header.h).abs() < f32::EPSILON);
287 assert!((layout.content.h - 200.0).abs() < f32::EPSILON);
288 }
289
290 #[test]
291 fn card_hit_test() {
292 let card = Card::new(Rect {
293 x: 10.0,
294 y: 10.0,
295 w: 100.0,
296 h: 80.0,
297 });
298 assert!(card.hit_test(50.0, 50.0));
299 assert!(!card.hit_test(0.0, 0.0));
300 }
301
302 #[test]
303 fn card_not_focusable() {
304 let card = Card::new(Rect {
305 x: 0.0,
306 y: 0.0,
307 w: 100.0,
308 h: 100.0,
309 });
310 assert!(card.focus_id().is_none());
311 }
312}