Skip to main content

elegance/
card.rs

1//! Card container — a rounded, bordered surface for grouping widgets.
2
3use egui::{Color32, CornerRadius, InnerResponse, Margin, Stroke, Ui, WidgetText};
4
5use crate::theme::Theme;
6
7/// A styled card surface.
8///
9/// ```no_run
10/// # use elegance::Card;
11/// # egui::__run_test_ui(|ui| {
12/// Card::new().heading("Setup").show(ui, |ui| {
13///     ui.label("Card contents go here.");
14/// });
15/// # });
16/// ```
17#[derive(Default)]
18#[must_use = "Call `.show(ui, ...)` to render the card."]
19pub struct Card {
20    heading: Option<WidgetText>,
21    padding: Option<f32>,
22    fill: Option<Color32>,
23    bordered: bool,
24    corner_radius: Option<CornerRadius>,
25}
26
27impl std::fmt::Debug for Card {
28    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
29        f.debug_struct("Card")
30            .field("heading", &self.heading.as_ref().map(|h| h.text()))
31            .field("padding", &self.padding)
32            .field("fill", &self.fill)
33            .field("bordered", &self.bordered)
34            .field("corner_radius", &self.corner_radius)
35            .finish()
36    }
37}
38
39impl Card {
40    /// Create a card with the default padding, fill, and border.
41    pub fn new() -> Self {
42        Self {
43            heading: None,
44            padding: None,
45            fill: None,
46            bordered: true,
47            corner_radius: None,
48        }
49    }
50
51    /// Show a small caption at the top of the card.
52    pub fn heading(mut self, heading: impl Into<WidgetText>) -> Self {
53        self.heading = Some(heading.into());
54        self
55    }
56
57    /// Override the default inner padding (points).
58    pub fn padding(mut self, padding: f32) -> Self {
59        self.padding = Some(padding);
60        self
61    }
62
63    /// Override the fill colour.
64    pub fn fill(mut self, fill: Color32) -> Self {
65        self.fill = Some(fill);
66        self
67    }
68
69    /// Toggle the 1-px border. Defaults to on.
70    pub fn bordered(mut self, bordered: bool) -> Self {
71        self.bordered = bordered;
72        self
73    }
74
75    /// Override the corner radius (per-corner). Useful for segmented
76    /// layouts where only some corners should be rounded.
77    pub fn corner_radius(mut self, radius: impl Into<CornerRadius>) -> Self {
78        self.corner_radius = Some(radius.into());
79        self
80    }
81
82    /// Render the card and its body contents, returning whatever the
83    /// closure returns inside an [`InnerResponse`].
84    pub fn show<R>(self, ui: &mut Ui, add_contents: impl FnOnce(&mut Ui) -> R) -> InnerResponse<R> {
85        let theme = Theme::current(ui.ctx());
86        let p = &theme.palette;
87
88        let padding = self.padding.unwrap_or(theme.card_padding);
89        let stroke = if self.bordered {
90            Stroke::new(1.0, p.border)
91        } else {
92            Stroke::NONE
93        };
94
95        let radius = self
96            .corner_radius
97            .unwrap_or_else(|| CornerRadius::same(theme.card_radius as u8));
98
99        let frame = egui::Frame::new()
100            .fill(self.fill.unwrap_or(p.card))
101            .stroke(stroke)
102            .corner_radius(radius)
103            .inner_margin(Margin::same(padding as i8));
104
105        frame.show(ui, |ui| {
106            if let Some(h) = &self.heading {
107                let rt = egui::RichText::new(h.text())
108                    .color(p.text_muted)
109                    .size(theme.typography.heading)
110                    .strong();
111                ui.add(egui::Label::new(rt).wrap_mode(egui::TextWrapMode::Extend));
112                ui.add_space(8.0);
113            }
114            add_contents(ui)
115        })
116    }
117}