Skip to main content

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    /// Get the current layout variant.
118    pub fn get_layout_variant(&self) -> &CardLayoutVariant {
119        &self.layout_variant
120    }
121
122    /// Get the layout theme override.
123    pub fn get_theme_layout(&self) -> Option<&CardLayoutThemePartial> {
124        self.theme_layout.as_ref()
125    }
126
127    /// Set the style variant.
128    pub fn style_variant(mut self, style_variant: impl Into<CardStyleVariant>) -> Self {
129        self.style_variant = style_variant.into();
130        self
131    }
132
133    /// Set the layout variant.
134    pub fn layout_variant(mut self, layout_variant: impl Into<CardLayoutVariant>) -> Self {
135        self.layout_variant = layout_variant.into();
136        self
137    }
138
139    /// Set whether the card should respond to hover interactions.
140    pub fn hoverable(mut self, hoverable: impl Into<bool>) -> Self {
141        self.hoverable = hoverable.into();
142        self
143    }
144
145    /// Set the press event handler.
146    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
147        self.on_press = Some(on_press.into());
148        self
149    }
150
151    /// Set custom color theme.
152    pub fn theme_colors(mut self, theme: CardColorsThemePartial) -> Self {
153        self.theme_colors = Some(theme);
154        self
155    }
156
157    /// Set custom layout theme.
158    pub fn theme_layout(mut self, theme: CardLayoutThemePartial) -> Self {
159        self.theme_layout = Some(theme);
160        self
161    }
162
163    /// Shortcut for [Self::style_variant] with [CardStyleVariant::Filled].
164    pub fn filled(self) -> Self {
165        self.style_variant(CardStyleVariant::Filled)
166    }
167
168    /// Shortcut for [Self::style_variant] with [CardStyleVariant::Outline].
169    pub fn outline(self) -> Self {
170        self.style_variant(CardStyleVariant::Outline)
171    }
172
173    /// Shortcut for [Self::layout_variant] with [CardLayoutVariant::Compact].
174    pub fn compact(self) -> Self {
175        self.layout_variant(CardLayoutVariant::Compact)
176    }
177}
178
179impl Component for Card {
180    fn render(&self) -> impl IntoElement {
181        let mut hovering = use_state(|| false);
182        let focus = use_focus();
183        let focus_status = use_focus_status(focus);
184
185        let is_hoverable = self.hoverable;
186
187        use_drop(move || {
188            if hovering() && is_hoverable {
189                Cursor::set(CursorIcon::default());
190            }
191        });
192
193        let theme_colors = match self.style_variant {
194            CardStyleVariant::Filled => get_theme!(&self.theme_colors, filled_card),
195            CardStyleVariant::Outline => get_theme!(&self.theme_colors, outline_card),
196        };
197        let theme_layout = match self.layout_variant {
198            CardLayoutVariant::Normal => get_theme!(&self.theme_layout, card_layout),
199            CardLayoutVariant::Compact => get_theme!(&self.theme_layout, compact_card_layout),
200        };
201
202        let border = if focus_status() == FocusStatus::Keyboard {
203            Border::new()
204                .fill(theme_colors.border_fill)
205                .width(2.)
206                .alignment(BorderAlignment::Inner)
207        } else {
208            Border::new()
209                .fill(theme_colors.border_fill)
210                .width(1.)
211                .alignment(BorderAlignment::Inner)
212        };
213
214        let background = if is_hoverable && hovering() {
215            theme_colors.hover_background
216        } else {
217            theme_colors.background
218        };
219
220        let shadow = if is_hoverable && hovering() {
221            Some(Shadow::new().y(4.).blur(8.).color(theme_colors.shadow))
222        } else {
223            None
224        };
225
226        rect()
227            .layout(self.layout.clone())
228            .overflow(Overflow::Clip)
229            .a11y_id(focus.a11y_id())
230            .a11y_focusable(is_hoverable)
231            .a11y_role(AccessibilityRole::GenericContainer)
232            .accessibility(self.accessibility.clone())
233            .background(background)
234            .border(border)
235            .padding(theme_layout.padding)
236            .corner_radius(theme_layout.corner_radius)
237            .color(theme_colors.color)
238            .map(shadow, |rect, shadow| rect.shadow(shadow))
239            .map(self.on_press.clone(), |rect, on_press| {
240                rect.on_press(move |e: Event<PressEventData>| {
241                    focus.request_focus();
242                    on_press.call(e);
243                })
244            })
245            .maybe(is_hoverable, |rect| {
246                rect.on_pointer_enter(move |_| {
247                    hovering.set(true);
248                    Cursor::set(CursorIcon::Pointer);
249                })
250                .on_pointer_leave(move |_| {
251                    if hovering() {
252                        Cursor::set(CursorIcon::default());
253                        hovering.set(false);
254                    }
255                })
256            })
257            .children(self.elements.clone())
258    }
259
260    fn render_key(&self) -> DiffKey {
261        self.key.clone().or(self.default_key())
262    }
263}