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::util::apply_tonal_elevation;
9use super::*;
10
11/// Configuration for [`Card`].
12#[derive(Clone, Debug)]
13pub struct CardConfig {
14    pub modifier: Modifier,
15    /// When false, renders disabled colors and does not respond to clicks.
16    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
46/// M3 Card - a configurable container surface.
47pub 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
93/// M3 Elevated Card - card with elevation.
94pub 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
113/// M3 Outlined Card - card with border outline.
114pub 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
175/// M3 Clickable Filled Card - interactive card with state coloring.
176pub 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
191/// M3 Clickable Elevated Card - interactive card with elevation.
192pub 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
220/// M3 Clickable Outlined Card - interactive card with border.
221pub 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}