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/// ```
63///
64/// See the [interactive components demo](https://freyaui.dev/demo).
65#[derive(Clone, PartialEq)]
66pub struct Card {
67    pub(crate) theme_colors: Option<CardColorsThemePartial>,
68    pub(crate) theme_layout: Option<CardLayoutThemePartial>,
69    layout: LayoutData,
70    accessibility: AccessibilityData,
71    elements: Vec<Element>,
72    on_press: Option<EventHandler<Event<PressEventData>>>,
73    key: DiffKey,
74    style_variant: CardStyleVariant,
75    layout_variant: CardLayoutVariant,
76    hoverable: bool,
77    cursor_icon: Option<CursorIcon>,
78}
79
80impl Default for Card {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl ChildrenExt for Card {
87    fn children_mut(&mut self) -> &mut Vec<Element> {
88        &mut self.elements
89    }
90}
91
92impl KeyExt for Card {
93    fn key_mut(&mut self) -> &mut DiffKey {
94        &mut self.key
95    }
96}
97
98impl LayoutExt for Card {
99    fn layout_mut(&mut self) -> &mut LayoutData {
100        &mut self.layout
101    }
102}
103
104impl ContainerSizeExt for Card {}
105
106impl ContainerPositionExt for Card {}
107
108impl ContainerConstraintsExt for Card {}
109
110impl ContainerWithContentExt for Card {}
111
112impl AccessibilityExt for Card {
113    fn accessibility_data_mut(&mut self) -> &mut AccessibilityData {
114        &mut self.accessibility
115    }
116}
117
118impl CornerRadiusExt for Card {
119    fn with_corner_radius(self, corner_radius: f32) -> Self {
120        self.corner_radius(corner_radius)
121    }
122}
123
124impl Card {
125    pub fn new() -> Self {
126        Self {
127            theme_colors: None,
128            theme_layout: None,
129            layout: LayoutData::default(),
130            accessibility: AccessibilityData::default(),
131            style_variant: CardStyleVariant::Outline,
132            layout_variant: CardLayoutVariant::Normal,
133            on_press: None,
134            elements: Vec::default(),
135            hoverable: false,
136            cursor_icon: None,
137            key: DiffKey::None,
138        }
139    }
140
141    /// Get the current layout variant.
142    pub fn get_layout_variant(&self) -> &CardLayoutVariant {
143        &self.layout_variant
144    }
145
146    /// Get the layout theme override.
147    pub fn get_theme_layout(&self) -> Option<&CardLayoutThemePartial> {
148        self.theme_layout.as_ref()
149    }
150
151    /// Set the style variant.
152    pub fn style_variant(mut self, style_variant: impl Into<CardStyleVariant>) -> Self {
153        self.style_variant = style_variant.into();
154        self
155    }
156
157    /// Set the layout variant.
158    pub fn layout_variant(mut self, layout_variant: impl Into<CardLayoutVariant>) -> Self {
159        self.layout_variant = layout_variant.into();
160        self
161    }
162
163    /// Set whether the card should respond to hover interactions.
164    pub fn hoverable(mut self, hoverable: impl Into<bool>) -> Self {
165        self.hoverable = hoverable.into();
166        self
167    }
168
169    /// Set the press event handler.
170    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
171        self.on_press = Some(on_press.into());
172        self
173    }
174
175    /// Set custom color theme.
176    pub fn theme_colors(mut self, theme: CardColorsThemePartial) -> Self {
177        self.theme_colors = Some(theme);
178        self
179    }
180
181    /// Set custom layout theme.
182    pub fn theme_layout(mut self, theme: CardLayoutThemePartial) -> Self {
183        self.theme_layout = Some(theme);
184        self
185    }
186
187    /// Shortcut for [Self::style_variant] with [CardStyleVariant::Filled].
188    pub fn filled(self) -> Self {
189        self.style_variant(CardStyleVariant::Filled)
190    }
191
192    /// Shortcut for [Self::style_variant] with [CardStyleVariant::Outline].
193    pub fn outline(self) -> Self {
194        self.style_variant(CardStyleVariant::Outline)
195    }
196
197    /// Shortcut for [Self::layout_variant] with [CardLayoutVariant::Compact].
198    pub fn compact(self) -> Self {
199        self.layout_variant(CardLayoutVariant::Compact)
200    }
201
202    /// Override the cursor icon shown when hovering over this component while hoverable.
203    pub fn cursor(mut self, cursor_icon: impl Into<Option<CursorIcon>>) -> Self {
204        self.cursor_icon = cursor_icon.into();
205        self
206    }
207}
208
209impl Component for Card {
210    fn render(&self) -> impl IntoElement {
211        let mut hovering = use_state(|| false);
212        let a11y_id = use_a11y();
213        let focus = use_focus(a11y_id);
214
215        let is_hoverable = self.hoverable;
216
217        let theme_colors = match self.style_variant {
218            CardStyleVariant::Filled => {
219                get_theme!(&self.theme_colors, CardColorsThemePreference, "filled_card")
220            }
221            CardStyleVariant::Outline => get_theme!(
222                &self.theme_colors,
223                CardColorsThemePreference,
224                "outline_card"
225            ),
226        };
227        let theme_layout = match self.layout_variant {
228            CardLayoutVariant::Normal => {
229                get_theme!(&self.theme_layout, CardLayoutThemePreference, "card_layout")
230            }
231            CardLayoutVariant::Compact => get_theme!(
232                &self.theme_layout,
233                CardLayoutThemePreference,
234                "compact_card_layout"
235            ),
236        };
237
238        let border = if focus() == Focus::Keyboard {
239            Border::new()
240                .fill(theme_colors.border_fill)
241                .width(2.)
242                .alignment(BorderAlignment::Inner)
243        } else {
244            Border::new()
245                .fill(theme_colors.border_fill)
246                .width(1.)
247                .alignment(BorderAlignment::Inner)
248        };
249
250        let background = if is_hoverable && hovering() {
251            theme_colors.hover_background
252        } else {
253            theme_colors.background
254        };
255
256        let shadow = if is_hoverable && hovering() {
257            Some(Shadow::new().y(4.).blur(8.).color(theme_colors.shadow))
258        } else {
259            None
260        };
261
262        rect()
263            .layout(self.layout.clone())
264            .overflow(Overflow::Clip)
265            .a11y_id(a11y_id)
266            .a11y_focusable(is_hoverable)
267            .a11y_role(AccessibilityRole::GenericContainer)
268            .accessibility(self.accessibility.clone())
269            .background(background)
270            .border(border)
271            .padding(theme_layout.padding)
272            .corner_radius(theme_layout.corner_radius)
273            .color(theme_colors.color)
274            .map(shadow, |rect, shadow| rect.shadow(shadow))
275            .map(self.on_press.clone(), |rect, on_press| {
276                rect.on_press(move |e: Event<PressEventData>| {
277                    a11y_id.request_focus();
278                    on_press.call(e);
279                })
280            })
281            .maybe(is_hoverable, |rect| {
282                rect.cursor(self.cursor_icon)
283                    .on_pointer_enter(move |_| {
284                        hovering.set(true);
285                    })
286                    .on_pointer_leave(move |_| {
287                        hovering.set_if_modified(false);
288                    })
289            })
290            .children(self.elements.clone())
291    }
292
293    fn render_key(&self) -> DiffKey {
294        self.key.clone().or(self.default_key())
295    }
296}