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