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#[derive(Clone, PartialEq)]
38pub enum CardStyleVariant {
39 Filled,
40 Outline,
41}
42
43#[derive(Clone, PartialEq)]
45pub enum CardLayoutVariant {
46 Normal,
47 Compact,
48}
49
50#[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 cursor_icon: CursorIcon,
86}
87
88impl Default for Card {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94impl ChildrenExt for Card {
95 fn get_children(&mut self) -> &mut Vec<Element> {
96 &mut self.elements
97 }
98}
99
100impl KeyExt for Card {
101 fn write_key(&mut self) -> &mut DiffKey {
102 &mut self.key
103 }
104}
105
106impl LayoutExt for Card {
107 fn get_layout(&mut self) -> &mut LayoutData {
108 &mut self.layout
109 }
110}
111
112impl ContainerSizeExt for Card {}
113
114impl ContainerPositionExt for Card {}
115
116impl ContainerConstraintsExt for Card {}
117
118impl ContainerWithContentExt for Card {}
119
120impl AccessibilityExt for Card {
121 fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
122 &mut self.accessibility
123 }
124}
125
126impl CornerRadiusExt for Card {
127 fn with_corner_radius(self, corner_radius: f32) -> Self {
128 self.corner_radius(corner_radius)
129 }
130}
131
132impl Card {
133 pub fn new() -> Self {
134 Self {
135 theme_colors: None,
136 theme_layout: None,
137 layout: LayoutData::default(),
138 accessibility: AccessibilityData::default(),
139 style_variant: CardStyleVariant::Outline,
140 layout_variant: CardLayoutVariant::Normal,
141 on_press: None,
142 elements: Vec::default(),
143 hoverable: false,
144 cursor_icon: CursorIcon::default(),
145 key: DiffKey::None,
146 }
147 }
148
149 pub fn get_layout_variant(&self) -> &CardLayoutVariant {
151 &self.layout_variant
152 }
153
154 pub fn get_theme_layout(&self) -> Option<&CardLayoutThemePartial> {
156 self.theme_layout.as_ref()
157 }
158
159 pub fn style_variant(mut self, style_variant: impl Into<CardStyleVariant>) -> Self {
161 self.style_variant = style_variant.into();
162 self
163 }
164
165 pub fn layout_variant(mut self, layout_variant: impl Into<CardLayoutVariant>) -> Self {
167 self.layout_variant = layout_variant.into();
168 self
169 }
170
171 pub fn hoverable(mut self, hoverable: impl Into<bool>) -> Self {
173 self.hoverable = hoverable.into();
174 self
175 }
176
177 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
179 self.on_press = Some(on_press.into());
180 self
181 }
182
183 pub fn theme_colors(mut self, theme: CardColorsThemePartial) -> Self {
185 self.theme_colors = Some(theme);
186 self
187 }
188
189 pub fn theme_layout(mut self, theme: CardLayoutThemePartial) -> Self {
191 self.theme_layout = Some(theme);
192 self
193 }
194
195 pub fn filled(self) -> Self {
197 self.style_variant(CardStyleVariant::Filled)
198 }
199
200 pub fn outline(self) -> Self {
202 self.style_variant(CardStyleVariant::Outline)
203 }
204
205 pub fn compact(self) -> Self {
207 self.layout_variant(CardLayoutVariant::Compact)
208 }
209
210 pub fn cursor_icon(mut self, cursor_icon: impl Into<CursorIcon>) -> Self {
212 self.cursor_icon = cursor_icon.into();
213 self
214 }
215}
216
217impl Component for Card {
218 fn render(&self) -> impl IntoElement {
219 let mut hovering = use_state(|| false);
220 let a11y_id = use_a11y();
221 let focus = use_focus(a11y_id);
222
223 let is_hoverable = self.hoverable;
224 let cursor_icon = self.cursor_icon;
225
226 use_drop(move || {
227 if hovering() && is_hoverable {
228 Cursor::set(CursorIcon::default());
229 }
230 });
231
232 let theme_colors = match self.style_variant {
233 CardStyleVariant::Filled => {
234 get_theme!(&self.theme_colors, CardColorsThemePreference, "filled_card")
235 }
236 CardStyleVariant::Outline => get_theme!(
237 &self.theme_colors,
238 CardColorsThemePreference,
239 "outline_card"
240 ),
241 };
242 let theme_layout = match self.layout_variant {
243 CardLayoutVariant::Normal => {
244 get_theme!(&self.theme_layout, CardLayoutThemePreference, "card_layout")
245 }
246 CardLayoutVariant::Compact => get_theme!(
247 &self.theme_layout,
248 CardLayoutThemePreference,
249 "compact_card_layout"
250 ),
251 };
252
253 let border = if focus() == Focus::Keyboard {
254 Border::new()
255 .fill(theme_colors.border_fill)
256 .width(2.)
257 .alignment(BorderAlignment::Inner)
258 } else {
259 Border::new()
260 .fill(theme_colors.border_fill)
261 .width(1.)
262 .alignment(BorderAlignment::Inner)
263 };
264
265 let background = if is_hoverable && hovering() {
266 theme_colors.hover_background
267 } else {
268 theme_colors.background
269 };
270
271 let shadow = if is_hoverable && hovering() {
272 Some(Shadow::new().y(4.).blur(8.).color(theme_colors.shadow))
273 } else {
274 None
275 };
276
277 rect()
278 .layout(self.layout.clone())
279 .overflow(Overflow::Clip)
280 .a11y_id(a11y_id)
281 .a11y_focusable(is_hoverable)
282 .a11y_role(AccessibilityRole::GenericContainer)
283 .accessibility(self.accessibility.clone())
284 .background(background)
285 .border(border)
286 .padding(theme_layout.padding)
287 .corner_radius(theme_layout.corner_radius)
288 .color(theme_colors.color)
289 .map(shadow, |rect, shadow| rect.shadow(shadow))
290 .map(self.on_press.clone(), |rect, on_press| {
291 rect.on_press(move |e: Event<PressEventData>| {
292 a11y_id.request_focus();
293 on_press.call(e);
294 })
295 })
296 .maybe(is_hoverable, |rect| {
297 rect.on_pointer_enter(move |_| {
298 hovering.set(true);
299 Cursor::set(cursor_icon);
300 })
301 .on_pointer_leave(move |_| {
302 if hovering() {
303 Cursor::set(CursorIcon::default());
304 hovering.set(false);
305 }
306 })
307 })
308 .children(self.elements.clone())
309 }
310
311 fn render_key(&self) -> DiffKey {
312 self.key.clone().or(self.default_key())
313 }
314}