1use freya_core::prelude::*;
2
3use crate::{
4 get_theme,
5 theming::component_themes::{
6 CardColorsThemePartial,
7 CardLayoutThemePartial,
8 CardLayoutThemePartialExt,
9 },
10};
11
12#[derive(Clone, PartialEq)]
14pub enum CardStyleVariant {
15 Filled,
16 Outline,
17}
18
19#[derive(Clone, PartialEq)]
21pub enum CardLayoutVariant {
22 Normal,
23 Compact,
24}
25
26#[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 pub fn style_variant(mut self, style_variant: impl Into<CardStyleVariant>) -> Self {
119 self.style_variant = style_variant.into();
120 self
121 }
122
123 pub fn layout_variant(mut self, layout_variant: impl Into<CardLayoutVariant>) -> Self {
125 self.layout_variant = layout_variant.into();
126 self
127 }
128
129 pub fn hoverable(mut self, hoverable: impl Into<bool>) -> Self {
131 self.hoverable = hoverable.into();
132 self
133 }
134
135 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 pub fn theme_colors(mut self, theme: CardColorsThemePartial) -> Self {
143 self.theme_colors = Some(theme);
144 self
145 }
146
147 pub fn theme_layout(mut self, theme: CardLayoutThemePartial) -> Self {
149 self.theme_layout = Some(theme);
150 self
151 }
152
153 pub fn filled(self) -> Self {
155 self.style_variant(CardStyleVariant::Filled)
156 }
157
158 pub fn outline(self) -> Self {
160 self.style_variant(CardStyleVariant::Outline)
161 }
162
163 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}