repose_material/material3/
card.rs1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::*;
6use repose_ui::{Box, Column, TextStyle, ViewExt};
7
8use super::*;
9
10#[derive(Clone, Debug)]
12pub struct CardConfig {
13 pub modifier: Modifier,
14 pub enabled: bool,
16 pub container_color: Color,
17 pub content_color: Color,
18 pub disabled_container_color: Color,
19 pub disabled_content_color: Color,
20 pub shape_radius: f32,
21 pub tonal_elevation: f32,
22 pub state_elevation: Option<StateElevation>,
23 pub border: Option<(f32, Color)>,
24 pub interaction_source: Option<MutableInteractionSource>,
25}
26
27impl Default for CardConfig {
28 fn default() -> Self {
29 Self {
30 modifier: Modifier::new(),
31 enabled: true,
32 container_color: CardDefaults::filled_container_color(),
33 content_color: CardDefaults::filled_content_color(),
34 disabled_container_color: CardDefaults::disabled_container_color(),
35 disabled_content_color: CardDefaults::disabled_content_color(),
36 shape_radius: CardDefaults::SHAPE_RADIUS,
37 tonal_elevation: CardDefaults::ELEVATION,
38 state_elevation: None,
39 border: None,
40 interaction_source: None,
41 }
42 }
43}
44
45pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
47 let bg = if !config.enabled {
48 config.disabled_container_color
49 } else {
50 config.container_color
51 };
52 let fg = if !config.enabled {
53 config.disabled_content_color
54 } else {
55 config.content_color
56 };
57 let source: Rc<MutableInteractionSource> = config
58 .interaction_source
59 .clone()
60 .map(Rc::new)
61 .unwrap_or_else(|| remember(MutableInteractionSource::new));
62 let mut m = Modifier::new()
63 .background(bg)
64 .clip_rounded(config.shape_radius)
65 .interaction_source(&source)
66 .then(config.modifier);
67 if let Some((w, c)) = config.border {
68 m = m.border(w, c, config.shape_radius);
69 }
70 if let Some(se) = config.state_elevation {
71 m = m.state_elevation(se);
72 } else if config.tonal_elevation > 0.0 {
73 m = m.state_elevation(StateElevation {
74 default: config.tonal_elevation,
75 hovered: config.tonal_elevation,
76 focused: config.tonal_elevation,
77 pressed: config.tonal_elevation,
78 dragged: config.tonal_elevation,
79 disabled: 0.0,
80 });
81 }
82 Box(m).color(fg).child(content())
83}
84
85pub fn ElevatedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
87 let th = theme();
88 Card(
89 CardConfig {
90 container_color: CardDefaults::elevated_container_color(),
91 state_elevation: Some(StateElevation {
92 default: th.elevation.level1,
93 hovered: th.elevation.level2,
94 focused: th.elevation.level2,
95 pressed: th.elevation.level3,
96 dragged: th.elevation.level3,
97 disabled: 0.0,
98 }),
99 ..config
100 },
101 content,
102 )
103}
104
105pub fn OutlinedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
107 Card(
108 CardConfig {
109 container_color: CardDefaults::outlined_container_color(),
110 border: Some((1.0, CardDefaults::outlined_border_color())),
111 ..config
112 },
113 content,
114 )
115}
116
117fn card_state_colors(bg: Color) -> StateColors {
118 let th = theme();
119 StateColors {
120 default: Color::TRANSPARENT,
121 hovered: Color::TRANSPARENT,
122 focused: Color::TRANSPARENT,
123 pressed: Color::TRANSPARENT,
124 dragged: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
125 disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
126 }
127}
128
129fn clickable_card_impl(
130 on_click: impl Fn() + 'static,
131 modifier: Modifier,
132 bg: Color,
133 shape_radius: f32,
134 config: CardConfig,
135 content: impl FnOnce() -> View,
136) -> View {
137 let m = modifier
138 .state_colors(card_state_colors(bg))
139 .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
140 color: Some(theme().on_surface),
141 bounded: true,
142 ..Default::default()
143 }))
144 .clickable()
145 .on_pointer_down({
146 let cb = on_click;
147 let en = config.enabled;
148 move |_| {
149 if en {
150 cb();
151 }
152 }
153 });
154 Card(
155 CardConfig {
156 modifier: m,
157 enabled: config.enabled,
158 container_color: bg,
159 content_color: config.content_color,
160 disabled_container_color: config.disabled_container_color,
161 disabled_content_color: config.disabled_content_color,
162 shape_radius,
163 border: config.border,
164 state_elevation: config.state_elevation,
165 tonal_elevation: config.tonal_elevation,
166 interaction_source: config.interaction_source.clone(),
167 },
168 || Column(Modifier::new().fill_max_size()).child(content()),
169 )
170}
171
172pub fn ClickableCard(
174 on_click: impl Fn() + 'static,
175 modifier: Modifier,
176 config: CardConfig,
177 content: impl FnOnce() -> View,
178) -> View {
179 let th = theme();
180 clickable_card_impl(
181 on_click,
182 modifier,
183 th.surface_container_highest,
184 th.shapes.medium,
185 config,
186 content,
187 )
188}
189
190pub fn ClickableElevatedCard(
192 on_click: impl Fn() + 'static,
193 modifier: Modifier,
194 config: CardConfig,
195 content: impl FnOnce() -> View,
196) -> View {
197 let th = theme();
198 let cfg = CardConfig {
199 state_elevation: Some(StateElevation {
200 default: th.elevation.level1,
201 hovered: th.elevation.level2,
202 focused: th.elevation.level2,
203 pressed: th.elevation.level3,
204 dragged: th.elevation.level3,
205 disabled: 0.0,
206 }),
207 ..config
208 };
209 clickable_card_impl(
210 on_click,
211 modifier,
212 th.surface,
213 th.shapes.medium,
214 cfg,
215 content,
216 )
217}
218
219pub fn ClickableOutlinedCard(
221 on_click: impl Fn() + 'static,
222 modifier: Modifier,
223 config: CardConfig,
224 content: impl FnOnce() -> View,
225) -> View {
226 let th = theme();
227 let cfg = CardConfig {
228 border: Some((1.0, th.outline_variant)),
229 ..config
230 };
231 clickable_card_impl(
232 on_click,
233 modifier,
234 th.surface,
235 th.shapes.medium,
236 cfg,
237 content,
238 )
239}