Skip to main content

repose_material/material3/
card.rs

1#![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/// Configuration for [`Card`].
11#[derive(Clone, Debug)]
12pub struct CardConfig {
13    pub modifier: Modifier,
14    /// When false, renders disabled colors and does not respond to clicks.
15    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
45/// M3 Card - a configurable container surface.
46pub 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
85/// M3 Elevated Card - card with elevation.
86pub 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
105/// M3 Outlined Card - card with border outline.
106pub 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
172/// M3 Clickable Filled Card - interactive card with state coloring.
173pub 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
190/// M3 Clickable Elevated Card - interactive card with elevation.
191pub 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
219/// M3 Clickable Outlined Card - interactive card with border.
220pub 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}