Skip to main content

repose_material/material3/
fab.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::*;
6use repose_ui::{Box, Row, Text, TextStyle, ViewExt};
7
8use super::util::{apply_m3_clickable, with_button_semantics};
9use super::*;
10
11/// Configuration for FAB components.
12#[derive(Clone, Debug)]
13pub struct FABConfig {
14    pub modifier: Modifier,
15    pub enabled: bool,
16    pub container_color: Color,
17    pub content_color: Color,
18    pub state_elevation: StateElevation,
19    pub shape_radius: f32,
20    pub size: f32,
21    pub interaction_source: Option<MutableInteractionSource>,
22}
23
24impl Default for FABConfig {
25    fn default() -> Self {
26        Self {
27            modifier: Modifier::new(),
28            enabled: true,
29            container_color: FABDefaults::container_color(),
30            content_color: FABDefaults::content_color(),
31            state_elevation: FABDefaults::state_elevation(),
32            shape_radius: FABDefaults::SHAPE_RADIUS,
33            size: FABDefaults::SIZE,
34            interaction_source: None,
35        }
36    }
37}
38
39fn fab_impl(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
40    let th = theme();
41    let is_enabled = config.enabled;
42    let bg = if is_enabled {
43        config.container_color
44    } else {
45        th.on_surface
46            .with_alpha_f32(0.12)
47            .composite_over(th.surface_container_low)
48    };
49    let content_color = if is_enabled {
50        config.content_color
51    } else {
52        th.on_surface.with_alpha_f32(0.38)
53    };
54
55    let elev = if is_enabled {
56        config.state_elevation
57    } else {
58        StateElevation {
59            default: 0.0,
60            hovered: 0.0,
61            focused: 0.0,
62            pressed: 0.0,
63            dragged: 0.0,
64            disabled: 0.0,
65        }
66    };
67
68    let mut m = Modifier::new()
69        .size(config.size, config.size)
70        .background(bg)
71        .state_colors(StateColors {
72            default: Color::TRANSPARENT,
73            hovered: Color::TRANSPARENT,
74            focused: Color::TRANSPARENT,
75            pressed: Color::TRANSPARENT,
76            dragged: config.content_color.with_alpha_f32(0.12),
77            disabled: th.on_surface.with_alpha_f32(0.12),
78        })
79        .state_elevation(elev)
80        .clip_rounded(config.shape_radius)
81        .align_items(AlignItems::CENTER)
82        .justify_content(JustifyContent::CENTER)
83        .then(config.modifier);
84
85    let source: Rc<MutableInteractionSource> = config
86        .interaction_source
87        .map(Rc::new)
88        .unwrap_or_else(|| remember(MutableInteractionSource::new));
89    m = apply_m3_clickable(m, &source, content_color, is_enabled, on_click);
90    m = with_button_semantics(m, is_enabled);
91
92    Box(m)
93        .child(with_content_color(content_color, move || icon))
94        
95}
96
97/// M3 Floating Action Button (regular, 56dp).
98pub fn FAB(icon: View, on_click: impl Fn() + 'static, mut config: FABConfig) -> View {
99    config.size = FABDefaults::SIZE;
100    config.shape_radius = FABDefaults::SHAPE_RADIUS;
101    fab_impl(icon, on_click, config)
102}
103
104/// M3 Small FAB (40dp).
105pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static, mut config: FABConfig) -> View {
106    config.size = FABDefaults::SMALL_SIZE;
107    config.shape_radius = FABDefaults::SMALL_SHAPE_RADIUS;
108    fab_impl(icon, on_click, config)
109}
110
111/// M3 Large FAB (96dp).
112pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, mut config: FABConfig) -> View {
113    config.size = FABDefaults::LARGE_SIZE;
114    config.shape_radius = FABDefaults::LARGE_SHAPE_RADIUS;
115    fab_impl(icon, on_click, config)
116}
117
118/// M3 Extended FAB - FAB with icon + label.
119pub fn ExtendedFAB(
120    icon: Option<View>,
121    label: impl Into<String>,
122    on_click: impl Fn() + 'static,
123    config: FABConfig,
124) -> View {
125    let th = theme();
126    let has_icon = icon.is_some();
127    let is_enabled = config.enabled;
128    let bg = if is_enabled {
129        config.container_color
130    } else {
131        th.on_surface
132            .with_alpha_f32(0.12)
133            .composite_over(th.surface_container_low)
134    };
135    let content_color = if is_enabled {
136        config.content_color
137    } else {
138        th.on_surface.with_alpha_f32(0.38)
139    };
140
141    let elev = if is_enabled {
142        config.state_elevation
143    } else {
144        StateElevation {
145            default: 0.0,
146            hovered: 0.0,
147            focused: 0.0,
148            pressed: 0.0,
149            dragged: 0.0,
150            disabled: 0.0,
151        }
152    };
153
154    let source: Rc<MutableInteractionSource> = config
155        .interaction_source
156        .clone()
157        .map(Rc::new)
158        .unwrap_or_else(|| remember(MutableInteractionSource::new));
159
160    let mut m = Modifier::new()
161        .height(56.0)
162        .min_width(80.0)
163        .background(bg)
164        .state_colors(StateColors {
165            default: Color::TRANSPARENT,
166            hovered: Color::TRANSPARENT,
167            focused: Color::TRANSPARENT,
168            pressed: Color::TRANSPARENT,
169            dragged: config.content_color.with_alpha_f32(0.12),
170            disabled: theme().on_surface.with_alpha_f32(0.12),
171        })
172        .state_elevation(elev)
173        .clip_rounded(FABDefaults::SHAPE_RADIUS)
174        .padding_values(PaddingValues {
175            left: 16.0,
176            right: 20.0,
177            top: 0.0,
178            bottom: 0.0,
179        })
180        .align_items(AlignItems::CENTER);
181
182    m = apply_m3_clickable(m, &source, content_color, is_enabled, on_click);
183    m = with_button_semantics(m, is_enabled);
184    m = m.then(config.modifier);
185    Row(m)
186        .child((
187            icon.map(|v| with_content_color(content_color, move || v))
188                .unwrap_or(Box(Modifier::new())),
189            Box(Modifier::new()
190                .width(if has_icon { 12.0 } else { 0.0 })
191                .fill_max_height()),
192            Text(label)
193                .color(content_color)
194                .size(th.typography.label_large)
195                .single_line(),
196        ))
197        
198}