Skip to main content

freya_components/
card.rs

1use freya_core::prelude::*;
2use torin::gaps::Gaps;
3
4use crate::{
5    define_theme,
6    get_theme,
7};
8
9define_theme! {
10    for = Card;
11    theme_field = theme_layout;
12
13    %[component]
14    pub CardLayout {
15        %[fields]
16        corner_radius: CornerRadius,
17        padding: Gaps,
18    }
19}
20
21define_theme! {
22    for = Card;
23    theme_field = theme_colors;
24
25    %[component]
26    pub CardColors {
27        %[fields]
28        background: Color,
29        hover_background: Color,
30        border_fill: Color,
31        color: Color,
32        shadow: Color,
33    }
34}
35
36/// Style variants for the Card component.
37#[derive(Clone, PartialEq)]
38pub enum CardStyleVariant {
39    Filled,
40    Outline,
41}
42
43/// Layout variants for the Card component.
44#[derive(Clone, PartialEq)]
45pub enum CardLayoutVariant {
46    Normal,
47    Compact,
48}
49
50/// A container component with styling variants.
51///
52/// # Example
53///
54/// ```rust
55/// # use freya::prelude::*;
56/// fn app() -> impl IntoElement {
57///     Card::new()
58///         .width(Size::percent(75.))
59///         .height(Size::percent(75.))
60///         .child("Hello, World!")
61/// }
62/// # use freya_testing::prelude::*;
63/// # launch_doc(|| {
64/// #   rect().center().expanded().child(app())
65/// # }, "./images/gallery_card.png").render();
66/// ```
67///
68/// # Preview
69/// ![Card Preview][card]
70#[cfg_attr(feature = "docs",
71    doc = embed_doc_image::embed_image!("card", "images/gallery_card.png"),
72)]
73#[derive(Clone, PartialEq)]
74pub struct Card {
75    pub(crate) theme_colors: Option<CardColorsThemePartial>,
76    pub(crate) theme_layout: Option<CardLayoutThemePartial>,
77    layout: LayoutData,
78    accessibility: AccessibilityData,
79    elements: Vec<Element>,
80    on_press: Option<EventHandler<Event<PressEventData>>>,
81    key: DiffKey,
82    style_variant: CardStyleVariant,
83    layout_variant: CardLayoutVariant,
84    hoverable: bool,
85    cursor_icon: CursorIcon,
86}
87
88impl Default for Card {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94impl ChildrenExt for Card {
95    fn get_children(&mut self) -> &mut Vec<Element> {
96        &mut self.elements
97    }
98}
99
100impl KeyExt for Card {
101    fn write_key(&mut self) -> &mut DiffKey {
102        &mut self.key
103    }
104}
105
106impl LayoutExt for Card {
107    fn get_layout(&mut self) -> &mut LayoutData {
108        &mut self.layout
109    }
110}
111
112impl ContainerExt for Card {}
113
114impl AccessibilityExt for Card {
115    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
116        &mut self.accessibility
117    }
118}
119
120impl CornerRadiusExt for Card {
121    fn with_corner_radius(self, corner_radius: f32) -> Self {
122        self.corner_radius(corner_radius)
123    }
124}
125
126impl Card {
127    pub fn new() -> Self {
128        Self {
129            theme_colors: None,
130            theme_layout: None,
131            layout: LayoutData::default(),
132            accessibility: AccessibilityData::default(),
133            style_variant: CardStyleVariant::Outline,
134            layout_variant: CardLayoutVariant::Normal,
135            on_press: None,
136            elements: Vec::default(),
137            hoverable: false,
138            cursor_icon: CursorIcon::default(),
139            key: DiffKey::None,
140        }
141    }
142
143    /// Get the current layout variant.
144    pub fn get_layout_variant(&self) -> &CardLayoutVariant {
145        &self.layout_variant
146    }
147
148    /// Get the layout theme override.
149    pub fn get_theme_layout(&self) -> Option<&CardLayoutThemePartial> {
150        self.theme_layout.as_ref()
151    }
152
153    /// Set the style variant.
154    pub fn style_variant(mut self, style_variant: impl Into<CardStyleVariant>) -> Self {
155        self.style_variant = style_variant.into();
156        self
157    }
158
159    /// Set the layout variant.
160    pub fn layout_variant(mut self, layout_variant: impl Into<CardLayoutVariant>) -> Self {
161        self.layout_variant = layout_variant.into();
162        self
163    }
164
165    /// Set whether the card should respond to hover interactions.
166    pub fn hoverable(mut self, hoverable: impl Into<bool>) -> Self {
167        self.hoverable = hoverable.into();
168        self
169    }
170
171    /// Set the press event handler.
172    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
173        self.on_press = Some(on_press.into());
174        self
175    }
176
177    /// Set custom color theme.
178    pub fn theme_colors(mut self, theme: CardColorsThemePartial) -> Self {
179        self.theme_colors = Some(theme);
180        self
181    }
182
183    /// Set custom layout theme.
184    pub fn theme_layout(mut self, theme: CardLayoutThemePartial) -> Self {
185        self.theme_layout = Some(theme);
186        self
187    }
188
189    /// Shortcut for [Self::style_variant] with [CardStyleVariant::Filled].
190    pub fn filled(self) -> Self {
191        self.style_variant(CardStyleVariant::Filled)
192    }
193
194    /// Shortcut for [Self::style_variant] with [CardStyleVariant::Outline].
195    pub fn outline(self) -> Self {
196        self.style_variant(CardStyleVariant::Outline)
197    }
198
199    /// Shortcut for [Self::layout_variant] with [CardLayoutVariant::Compact].
200    pub fn compact(self) -> Self {
201        self.layout_variant(CardLayoutVariant::Compact)
202    }
203
204    /// Override the cursor icon shown when hovering over this component while hoverable.
205    pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
206        self.cursor_icon = cursor_icon.into();
207        self
208    }
209}
210
211impl Component for Card {
212    fn render(&self) -> impl IntoElement {
213        let mut hovering = use_state(|| false);
214        let a11y_id = use_a11y();
215        let focus = use_focus(a11y_id);
216
217        let is_hoverable = self.hoverable;
218        let cursor_icon = self.cursor_icon;
219
220        use_drop(move || {
221            if hovering() && is_hoverable {
222                Cursor::set(CursorIcon::default());
223            }
224        });
225
226        let theme_colors = match self.style_variant {
227            CardStyleVariant::Filled => {
228                get_theme!(&self.theme_colors, CardColorsThemePreference, "filled_card")
229            }
230            CardStyleVariant::Outline => get_theme!(
231                &self.theme_colors,
232                CardColorsThemePreference,
233                "outline_card"
234            ),
235        };
236        let theme_layout = match self.layout_variant {
237            CardLayoutVariant::Normal => {
238                get_theme!(&self.theme_layout, CardLayoutThemePreference, "card_layout")
239            }
240            CardLayoutVariant::Compact => get_theme!(
241                &self.theme_layout,
242                CardLayoutThemePreference,
243                "compact_card_layout"
244            ),
245        };
246
247        let border = if focus() == Focus::Keyboard {
248            Border::new()
249                .fill(theme_colors.border_fill)
250                .width(2.)
251                .alignment(BorderAlignment::Inner)
252        } else {
253            Border::new()
254                .fill(theme_colors.border_fill)
255                .width(1.)
256                .alignment(BorderAlignment::Inner)
257        };
258
259        let background = if is_hoverable && hovering() {
260            theme_colors.hover_background
261        } else {
262            theme_colors.background
263        };
264
265        let shadow = if is_hoverable && hovering() {
266            Some(Shadow::new().y(4.).blur(8.).color(theme_colors.shadow))
267        } else {
268            None
269        };
270
271        rect()
272            .layout(self.layout.clone())
273            .overflow(Overflow::Clip)
274            .a11y_id(a11y_id)
275            .a11y_focusable(is_hoverable)
276            .a11y_role(AccessibilityRole::GenericContainer)
277            .accessibility(self.accessibility.clone())
278            .background(background)
279            .border(border)
280            .padding(theme_layout.padding)
281            .corner_radius(theme_layout.corner_radius)
282            .color(theme_colors.color)
283            .map(shadow, |rect, shadow| rect.shadow(shadow))
284            .map(self.on_press.clone(), |rect, on_press| {
285                rect.on_press(move |e: Event<PressEventData>| {
286                    a11y_id.request_focus();
287                    on_press.call(e);
288                })
289            })
290            .maybe(is_hoverable, |rect| {
291                rect.on_pointer_enter(move |_| {
292                    hovering.set(true);
293                    Cursor::set(cursor_icon);
294                })
295                .on_pointer_leave(move |_| {
296                    if hovering() {
297                        Cursor::set(CursorIcon::default());
298                        hovering.set(false);
299                    }
300                })
301            })
302            .children(self.elements.clone())
303    }
304
305    fn render_key(&self) -> DiffKey {
306        self.key.clone().or(self.default_key())
307    }
308}