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