egui_material3/
card2.rs

1use egui::{
2    ecolor::Color32, 
3    epaint::{Stroke, CornerRadius},
4    Rect, Response, Sense, Ui, Vec2, Widget,
5};
6use crate::theme::get_global_color;
7
8/// Material Design card component variants (enhanced version).
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum Card2Variant {
11    Elevated,
12    Filled,
13    Outlined,
14}
15
16/// Enhanced Material Design card component.
17///
18/// This is an enhanced version of the card component with additional features
19/// like media support, action areas, and improved layout options.
20///
21/// ```
22/// # egui::__run_test_ui(|ui| {
23/// // Enhanced card with media and actions
24/// ui.add(MaterialCard2::elevated()
25///     .header("Card Title", Some("Subtitle"))
26///     .media_area(|ui| {
27///         ui.label("Media content goes here");
28///     })
29///     .content(|ui| {
30///         ui.label("Main card content");
31///     })
32///     .actions(|ui| {
33///         if ui.button("Action 1").clicked() {
34///             println!("Action 1 clicked!");
35///         }
36///     }));
37/// # });
38/// ```
39#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
40pub struct MaterialCard2<'a> {
41    variant: Card2Variant,
42    header_title: Option<String>,
43    header_subtitle: Option<String>,
44    media_content: Option<Box<dyn FnOnce(&mut Ui) -> Response + 'a>>,
45    main_content: Option<Box<dyn FnOnce(&mut Ui) -> Response + 'a>>,
46    actions_content: Option<Box<dyn FnOnce(&mut Ui) -> Response + 'a>>,
47    min_size: Vec2,
48    corner_radius: CornerRadius,
49    clickable: bool,
50    media_height: f32,
51}
52
53impl<'a> MaterialCard2<'a> {
54    /// Create a new elevated material card.
55    pub fn elevated() -> Self {
56        Self::new_with_variant(Card2Variant::Elevated)
57    }
58
59    /// Create a new filled material card.
60    pub fn filled() -> Self {
61        Self::new_with_variant(Card2Variant::Filled)
62    }
63
64    /// Create a new outlined material card.
65    pub fn outlined() -> Self {
66        Self::new_with_variant(Card2Variant::Outlined)
67    }
68
69    fn new_with_variant(variant: Card2Variant) -> Self {
70        Self {
71            variant,
72            header_title: None,
73            header_subtitle: None,
74            media_content: None,
75            main_content: None,
76            actions_content: None,
77            min_size: Vec2::new(280.0, 200.0), // Larger default size for enhanced card
78            corner_radius: CornerRadius::from(12.0),
79            clickable: false,
80            media_height: 160.0,
81        }
82    }
83
84    /// Set card header with title and optional subtitle.
85    pub fn header(mut self, title: impl Into<String>, subtitle: Option<impl Into<String>>) -> Self {
86        self.header_title = Some(title.into());
87        self.header_subtitle = subtitle.map(|s| s.into());
88        self
89    }
90
91    /// Set media area content.
92    pub fn media_area<F>(mut self, content: F) -> Self 
93    where
94        F: FnOnce(&mut Ui) + 'a,
95    {
96        self.media_content = Some(Box::new(move |ui| {
97            content(ui);
98            ui.allocate_response(Vec2::ZERO, Sense::hover())
99        }));
100        self
101    }
102
103    /// Set media area height.
104    pub fn media_height(mut self, height: f32) -> Self {
105        self.media_height = height;
106        self
107    }
108
109    /// Set main content for the card.
110    pub fn content<F>(mut self, content: F) -> Self 
111    where
112        F: FnOnce(&mut Ui) + 'a,
113    {
114        self.main_content = Some(Box::new(move |ui| {
115            content(ui);
116            ui.allocate_response(Vec2::ZERO, Sense::hover())
117        }));
118        self
119    }
120
121    /// Set actions area content.
122    pub fn actions<F>(mut self, content: F) -> Self 
123    where
124        F: FnOnce(&mut Ui) + 'a,
125    {
126        self.actions_content = Some(Box::new(move |ui| {
127            content(ui);
128            ui.allocate_response(Vec2::ZERO, Sense::hover())
129        }));
130        self
131    }
132
133    /// Set the minimum size of the card.
134    pub fn min_size(mut self, min_size: Vec2) -> Self {
135        self.min_size = min_size;
136        self
137    }
138
139    /// Set the corner radius of the card.
140    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
141        self.corner_radius = corner_radius.into();
142        self
143    }
144
145    /// Make the card clickable.
146    pub fn clickable(mut self, clickable: bool) -> Self {
147        self.clickable = clickable;
148        self
149    }
150
151    fn get_card_style(&self) -> (Color32, Option<Stroke>, bool) {
152        // Material Design theme colors
153        let md_surface = get_global_color("surface");
154        let md_surface_container_highest = get_global_color("surfaceContainerHighest");
155        let md_outline_variant = get_global_color("outlineVariant");
156
157        match self.variant {
158            Card2Variant::Elevated => {
159                // Elevated card: surface color with shadow
160                (md_surface, None, true)
161            },
162            Card2Variant::Filled => {
163                // Filled card: surface-container-highest color
164                (md_surface_container_highest, None, false)
165            },
166            Card2Variant::Outlined => {
167                // Outlined card: surface color with outline
168                let stroke = Some(Stroke::new(1.0, md_outline_variant));
169                (md_surface, stroke, false)
170            },
171        }
172    }
173}
174
175impl<'a> Default for MaterialCard2<'a> {
176    fn default() -> Self {
177        Self::elevated()
178    }
179}
180
181impl Widget for MaterialCard2<'_> {
182    fn ui(self, ui: &mut Ui) -> Response {
183        let (background_color, stroke, has_shadow) = self.get_card_style();
184        
185        let MaterialCard2 {
186            variant: _,
187            header_title,
188            header_subtitle,
189            media_content,
190            main_content,
191            actions_content,
192            min_size,
193            corner_radius,
194            clickable,
195            media_height,
196        } = self;
197
198        let sense = if clickable {
199            Sense::click()
200        } else {
201            Sense::hover()
202        };
203
204        // Calculate total height based on content
205        let header_height = if header_title.is_some() { 72.0 } else { 0.0 };
206        let media_height_actual = if media_content.is_some() { media_height } else { 0.0 };
207        let content_height = 80.0; // Default content area height
208        let actions_height = if actions_content.is_some() { 52.0 } else { 0.0 };
209        
210        let total_height = header_height + media_height_actual + content_height + actions_height;
211        let card_size = Vec2::new(min_size.x, total_height.max(min_size.y));
212        
213        let desired_size = ui.available_size().max(card_size);
214        let mut response = ui.allocate_response(desired_size, sense);
215        let rect = response.rect;
216
217        if ui.is_rect_visible(rect) {
218            // Draw shadow if present (for elevated cards)
219            if has_shadow {
220                let shadow_rect = Rect::from_min_size(
221                    rect.min + Vec2::new(0.0, 2.0),
222                    rect.size(),
223                );
224                ui.painter().rect_filled(
225                    shadow_rect,
226                    corner_radius,
227                    Color32::from_rgba_unmultiplied(0, 0, 0, 20),
228                );
229            }
230
231            // Draw card background
232            ui.painter().rect_filled(rect, corner_radius, background_color);
233
234            // Draw outline if present (for outlined cards)
235            if let Some(stroke) = stroke {
236                ui.painter().rect_stroke(rect, corner_radius, stroke, egui::epaint::StrokeKind::Outside);
237            }
238
239            let mut current_y = rect.min.y;
240
241            // Draw header
242            if let Some(title) = &header_title {
243                let _header_rect = Rect::from_min_size(
244                    egui::pos2(rect.min.x, current_y),
245                    Vec2::new(rect.width(), header_height)
246                );
247                
248                // Title
249                let title_pos = egui::pos2(rect.min.x + 16.0, current_y + 16.0);
250                ui.painter().text(
251                    title_pos,
252                    egui::Align2::LEFT_TOP,
253                    title,
254                    egui::FontId::proportional(20.0),
255                    get_global_color("onSurface")
256                );
257                
258                // Subtitle if present
259                if let Some(subtitle) = &header_subtitle {
260                    let subtitle_pos = egui::pos2(rect.min.x + 16.0, current_y + 44.0);
261                    ui.painter().text(
262                        subtitle_pos,
263                        egui::Align2::LEFT_TOP,
264                        subtitle,
265                        egui::FontId::proportional(14.0),
266                        get_global_color("onSurfaceVariant")
267                    );
268                }
269                
270                current_y += header_height;
271            }
272
273            // Draw media area
274            if let Some(media_fn) = media_content {
275                let media_rect = Rect::from_min_size(
276                    egui::pos2(rect.min.x, current_y),
277                    Vec2::new(rect.width(), media_height)
278                );
279                
280                // Clip media content to card bounds
281                let media_response = ui.scope_builder(
282                    egui::UiBuilder::new().max_rect(media_rect),
283                    |ui| {
284                        // Draw media background
285                        ui.painter().rect_filled(
286                            media_rect,
287                            CornerRadius::ZERO,
288                            get_global_color("surfaceVariant")
289                        );
290                        
291                        media_fn(ui)
292                    }
293                );
294                
295                response = response.union(media_response.response);
296                current_y += media_height;
297            }
298
299            // Draw main content
300            if let Some(content_fn) = main_content {
301                let content_rect = Rect::from_min_size(
302                    egui::pos2(rect.min.x, current_y),
303                    Vec2::new(rect.width(), content_height)
304                );
305                
306                let content_response = ui.scope_builder(
307                    egui::UiBuilder::new().max_rect(content_rect.shrink(16.0)),
308                    |ui| {
309                        content_fn(ui)
310                    }
311                );
312                
313                response = response.union(content_response.response);
314                current_y += content_height;
315            }
316
317            // Draw actions area
318            if let Some(actions_fn) = actions_content {
319                let actions_rect = Rect::from_min_size(
320                    egui::pos2(rect.min.x, current_y),
321                    Vec2::new(rect.width(), actions_height)
322                );
323                
324                let actions_response = ui.scope_builder(
325                    egui::UiBuilder::new().max_rect(actions_rect.shrink2(Vec2::new(8.0, 8.0))),
326                    |ui| {
327                        ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
328                            actions_fn(ui)
329                        }).inner
330                    }
331                );
332                
333                response = response.union(actions_response.response);
334            }
335        }
336
337        response
338    }
339}
340
341/// Convenience function to create an elevated enhanced card.
342pub fn elevated_card2() -> MaterialCard2<'static> {
343    MaterialCard2::elevated()
344}
345
346/// Convenience function to create a filled enhanced card.
347pub fn filled_card2() -> MaterialCard2<'static> {
348    MaterialCard2::filled()
349}
350
351/// Convenience function to create an outlined enhanced card.
352pub fn outlined_card2() -> MaterialCard2<'static> {
353    MaterialCard2::outlined()
354}