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            disabled: 0.0,
81        });
82    }
83    Box(m).color(fg).child(content())
84}
85
86/// M3 Elevated Card - card with elevation.
87pub fn ElevatedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
88    let th = theme();
89    Card(
90        CardConfig {
91            container_color: CardDefaults::elevated_container_color(),
92            state_elevation: Some(StateElevation {
93                default: th.elevation.level1,
94                hovered: th.elevation.level2,
95                pressed: th.elevation.level3,
96                disabled: 0.0,
97            }),
98            ..config
99        },
100        content,
101    )
102}
103
104/// M3 Outlined Card - card with border outline.
105pub fn OutlinedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
106    Card(
107        CardConfig {
108            container_color: CardDefaults::outlined_container_color(),
109            border: Some((1.0, CardDefaults::outlined_border_color())),
110            ..config
111        },
112        content,
113    )
114}
115
116fn card_state_colors(bg: Color) -> StateColors {
117    let th = theme();
118    StateColors {
119        default: Color::TRANSPARENT,
120        hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
121        pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
122        disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
123    }
124}
125
126fn clickable_card_impl(
127    on_click: impl Fn() + 'static,
128    modifier: Modifier,
129    bg: Color,
130    shape_radius: f32,
131    config: CardConfig,
132    content: impl FnOnce() -> View,
133) -> View {
134    let m = modifier
135        .state_colors(card_state_colors(bg))
136        .clickable()
137        .on_pointer_down({
138            let cb = on_click;
139            let en = config.enabled;
140            move |_| {
141                if en {
142                    cb();
143                }
144            }
145        });
146    Card(
147        CardConfig {
148            modifier: m,
149            enabled: config.enabled,
150            container_color: bg,
151            content_color: config.content_color,
152            disabled_container_color: config.disabled_container_color,
153            disabled_content_color: config.disabled_content_color,
154            shape_radius,
155            border: config.border,
156            state_elevation: config.state_elevation,
157            tonal_elevation: config.tonal_elevation,
158            interaction_source: config.interaction_source.clone(),
159        },
160        || Column(Modifier::new().fill_max_size()).child(content()),
161    )
162}
163
164/// M3 Clickable Filled Card - interactive card with state coloring.
165pub fn ClickableCard(
166    on_click: impl Fn() + 'static,
167    modifier: Modifier,
168    config: CardConfig,
169    content: impl FnOnce() -> View,
170) -> View {
171    let th = theme();
172    clickable_card_impl(
173        on_click,
174        modifier,
175        th.surface_container_highest,
176        th.shapes.medium,
177        config,
178        content,
179    )
180}
181
182/// M3 Clickable Elevated Card - interactive card with elevation.
183pub fn ClickableElevatedCard(
184    on_click: impl Fn() + 'static,
185    modifier: Modifier,
186    config: CardConfig,
187    content: impl FnOnce() -> View,
188) -> View {
189    let th = theme();
190    let cfg = CardConfig {
191        state_elevation: Some(StateElevation {
192            default: th.elevation.level1,
193            hovered: th.elevation.level2,
194            pressed: th.elevation.level3,
195            disabled: 0.0,
196        }),
197        ..config
198    };
199    clickable_card_impl(
200        on_click,
201        modifier,
202        th.surface,
203        th.shapes.medium,
204        cfg,
205        content,
206    )
207}
208
209/// M3 Clickable Outlined Card - interactive card with border.
210pub fn ClickableOutlinedCard(
211    on_click: impl Fn() + 'static,
212    modifier: Modifier,
213    config: CardConfig,
214    content: impl FnOnce() -> View,
215) -> View {
216    let th = theme();
217    let cfg = CardConfig {
218        border: Some((1.0, th.outline_variant)),
219        ..config
220    };
221    clickable_card_impl(
222        on_click,
223        modifier,
224        th.surface,
225        th.shapes.medium,
226        cfg,
227        content,
228    )
229}