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}
86
87impl Default for Card {
88    fn default() -> Self {
89        Self::new()
90    }
91}
92
93impl ChildrenExt for Card {
94    fn get_children(&mut self) -> &mut Vec<Element> {
95        &mut self.elements
96    }
97}
98
99impl KeyExt for Card {
100    fn write_key(&mut self) -> &mut DiffKey {
101        &mut self.key
102    }
103}
104
105impl LayoutExt for Card {
106    fn get_layout(&mut self) -> &mut LayoutData {
107        &mut self.layout
108    }
109}
110
111impl ContainerExt for Card {}
112
113impl AccessibilityExt for Card {
114    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
115        &mut self.accessibility
116    }
117}
118
119impl CornerRadiusExt for Card {
120    fn with_corner_radius(self, corner_radius: f32) -> Self {
121        self.corner_radius(corner_radius)
122    }
123}
124
125impl Card {
126    pub fn new() -> Self {
127        Self {
128            theme_colors: None,
129            theme_layout: None,
130            layout: LayoutData::default(),
131            accessibility: AccessibilityData::default(),
132            style_variant: CardStyleVariant::Outline,
133            layout_variant: CardLayoutVariant::Normal,
134            on_press: None,
135            elements: Vec::default(),
136            hoverable: false,
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
203impl Component for Card {
204    fn render(&self) -> impl IntoElement {
205        let mut hovering = use_state(|| false);
206        let focus = use_focus();
207        let focus_status = use_focus_status(focus);
208
209        let is_hoverable = self.hoverable;
210
211        use_drop(move || {
212            if hovering() && is_hoverable {
213                Cursor::set(CursorIcon::default());
214            }
215        });
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_status() == FocusStatus::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(focus.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                    focus.request_focus();
278                    on_press.call(e);
279                })
280            })
281            .maybe(is_hoverable, |rect| {
282                rect.on_pointer_enter(move |_| {
283                    hovering.set(true);
284                    Cursor::set(CursorIcon::Pointer);
285                })
286                .on_pointer_leave(move |_| {
287                    if hovering() {
288                        Cursor::set(CursorIcon::default());
289                        hovering.set(false);
290                    }
291                })
292            })
293            .children(self.elements.clone())
294    }
295
296    fn render_key(&self) -> DiffKey {
297        self.key.clone().or(self.default_key())
298    }
299}