1use std::fmt;
2use std::rc::Rc;
3
4use ravenrs_core::{Border, Color, CursorIcon, Insets, Role, Shadow};
5use ravenrs_layout::{Align, Axis, LayoutStyle};
6use ravenrs_style::Semantic;
7
8use crate::context::{BuildCx, NodeId, Paint, QuadPaint, Semantics};
9use crate::{Fill, View};
10
11pub struct Card<V> {
14 content: V,
15 fill: Fill,
16 corner_radius: Option<f32>,
17 shadow: Option<Option<Shadow>>,
18 padding: Option<Insets>,
19 border: bool,
20 spacing: Option<f32>,
21 on_click: Option<Rc<dyn Fn()>>,
22 selected: Option<bool>,
23}
24
25impl<V: fmt::Debug> fmt::Debug for Card<V> {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 f.debug_struct("Card").field("content", &self.content).finish_non_exhaustive()
28 }
29}
30
31#[cfg_attr(feature = "live", ravenrs_live::expose)]
32impl<V: View> Card<V> {
33 pub fn new(content: V) -> Self {
35 Self {
36 content,
37 fill: Semantic::SurfaceElevated.into(),
38 corner_radius: None,
39 shadow: None,
40 padding: None,
41 border: true,
42 spacing: None,
43 on_click: None,
44 selected: None,
45 }
46 }
47
48 pub fn corner_radius(mut self, radius: f32) -> Self {
50 self.corner_radius = Some(radius);
51 self
52 }
53
54 pub fn shadow(mut self, shadow: Shadow) -> Self {
56 self.shadow = Some(Some(shadow));
57 self
58 }
59
60 pub fn no_shadow(mut self) -> Self {
62 self.shadow = Some(None);
63 self
64 }
65
66 pub fn padding(mut self, padding: impl Into<Insets>) -> Self {
68 self.padding = Some(padding.into());
69 self
70 }
71
72 pub fn fill(mut self, fill: impl Into<Fill>) -> Self {
74 self.fill = fill.into();
75 self
76 }
77
78 pub fn border(mut self, visible: bool) -> Self {
80 self.border = visible;
81 self
82 }
83
84 pub fn on_click(mut self, handler: impl Fn() + 'static) -> Self {
87 self.on_click = Some(Rc::new(handler));
88 self
89 }
90
91 pub fn selected(mut self, selected: bool) -> Self {
95 self.selected = Some(selected);
96 self
97 }
98
99 fn paint(&self, cx: &BuildCx<'_>, fill: Color) -> QuadPaint {
100 let theme = cx.theme();
101 let active = self.on_click.is_some() && (cx.is_hovered() || cx.is_focus_visible());
102 let selected = self.selected == Some(true);
103 let accent = theme.color(Semantic::Accent);
104 QuadPaint {
105 fill: if selected { fill.lerp(accent, 0.12).into() } else { fill.into() },
106 corner_radius: self.corner_radius.unwrap_or(theme.radii.xl),
107 border: if selected {
109 Some(Border { width: 2.0, color: accent })
110 } else {
111 self.border.then(|| Border {
112 width: 1.0,
113 color: theme.color(if active { Semantic::Accent } else { Semantic::Border }),
114 })
115 },
116 shadow: self.shadow.unwrap_or(Some(theme.elevation.soft)),
117 }
118 }
119
120 fn build_with(&self, cx: &mut BuildCx<'_>, fill: Color) -> NodeId {
121 let paint = self.paint(cx, fill);
122 let style = LayoutStyle {
123 padding: self.padding.unwrap_or_else(|| cx.theme().card_padding()),
124 spacing: self.spacing.unwrap_or(cx.theme().spacing.s2),
125 align: Align::Stretch,
126 ..LayoutStyle::stack(Axis::Vertical)
127 };
128 let corner_radius = paint.corner_radius;
129 let child = cx.child(&self.content);
130 let node = cx.container(style, &[child], Paint::Quad(paint));
131 if self.on_click.is_some() {
132 crate::motion::control(cx, node);
133 }
134 if let Some(handler) = &self.on_click {
135 let mut semantics = Semantics::new(Role::Button);
136 if let Some(selected) = self.selected {
137 semantics = semantics.selected(selected);
138 }
139 cx.semantics(node, semantics);
140 cx.on_click(node, handler.clone());
141 cx.focusable(node, corner_radius);
142 cx.cursor(node, CursorIcon::Pointer);
143 } else if let Some(selected) = self.selected {
144 cx.semantics(node, Semantics::new(Role::Group).selected(selected));
145 }
146 node
147 }
148}
149
150impl<V: View> View for Card<V> {
151 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
152 let fill = self.fill.resolve(cx.theme());
153 self.build_with(cx, fill)
154 }
155}
156
157#[derive(Debug)]
163pub struct GlassCard<V> {
164 card: Card<V>,
165}
166
167#[cfg_attr(feature = "live", ravenrs_live::expose)]
168impl<V: View> GlassCard<V> {
169 pub fn new(content: V) -> Self {
171 Self { card: Card::new(content).fill(Color::hex_alpha(0xffffff1a)) }
172 }
173
174 pub fn corner_radius(mut self, radius: f32) -> Self {
176 self.card = self.card.corner_radius(radius);
177 self
178 }
179
180 pub fn shadow(mut self, shadow: Shadow) -> Self {
182 self.card = self.card.shadow(shadow);
183 self
184 }
185
186 pub fn padding(mut self, padding: impl Into<Insets>) -> Self {
188 self.card = self.card.padding(padding);
189 self
190 }
191}
192
193impl<V: View> View for GlassCard<V> {
194 fn build(&self, cx: &mut BuildCx<'_>) -> NodeId {
195 let fill = self.card.fill.resolve(cx.theme());
196 self.card.build_with(cx, fill)
197 }
198}