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            pressed: config.tonal_elevation,
77            dragged: config.tonal_elevation,
78            disabled: 0.0,
79        });
80    }
81    Box(m).color(fg).child(content())
82}
83
84/// M3 Elevated Card - card with elevation.
85pub fn ElevatedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
86    let th = theme();
87    Card(
88        CardConfig {
89            container_color: CardDefaults::elevated_container_color(),
90            state_elevation: Some(StateElevation {
91                default: th.elevation.level1,
92                hovered: th.elevation.level2,
93                pressed: th.elevation.level3,
94                dragged: th.elevation.level3,
95                disabled: 0.0,
96            }),
97            ..config
98        },
99        content,
100    )
101}
102
103/// M3 Outlined Card - card with border outline.
104pub fn OutlinedCard(config: CardConfig, content: impl FnOnce() -> View) -> View {
105    Card(
106        CardConfig {
107            container_color: CardDefaults::outlined_container_color(),
108            border: Some((1.0, CardDefaults::outlined_border_color())),
109            ..config
110        },
111        content,
112    )
113}
114
115fn card_state_colors(bg: Color) -> StateColors {
116    let th = theme();
117    StateColors {
118        default: Color::TRANSPARENT,
119        hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
120        pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
121        dragged: 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            dragged: th.elevation.level3,
196            disabled: 0.0,
197        }),
198        ..config
199    };
200    clickable_card_impl(
201        on_click,
202        modifier,
203        th.surface,
204        th.shapes.medium,
205        cfg,
206        content,
207    )
208}
209
210/// M3 Clickable Outlined Card - interactive card with border.
211pub fn ClickableOutlinedCard(
212    on_click: impl Fn() + 'static,
213    modifier: Modifier,
214    config: CardConfig,
215    content: impl FnOnce() -> View,
216) -> View {
217    let th = theme();
218    let cfg = CardConfig {
219        border: Some((1.0, th.outline_variant)),
220        ..config
221    };
222    clickable_card_impl(
223        on_click,
224        modifier,
225        th.surface,
226        th.shapes.medium,
227        cfg,
228        content,
229    )
230}