freya_components/
card.rs

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