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