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).child(super::util::icon_content_with_color(content_color, icon))
93}
94
95/// M3 Floating Action Button (regular, 56dp).
96pub fn FAB(icon: View, on_click: impl Fn() + 'static, mut config: FABConfig) -> View {
97    config.size = FABDefaults::SIZE;
98    config.shape_radius = FABDefaults::SHAPE_RADIUS;
99    fab_impl(icon, on_click, config)
100}
101
102/// M3 Small FAB (40dp).
103pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static, mut config: FABConfig) -> View {
104    config.size = FABDefaults::SMALL_SIZE;
105    config.shape_radius = FABDefaults::SMALL_SHAPE_RADIUS;
106    fab_impl(icon, on_click, config)
107}
108
109/// M3 Large FAB (96dp).
110pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, mut config: FABConfig) -> View {
111    config.size = FABDefaults::LARGE_SIZE;
112    config.shape_radius = FABDefaults::LARGE_SHAPE_RADIUS;
113    fab_impl(icon, on_click, config)
114}
115
116/// M3 Extended FAB - FAB with icon + label.
117pub fn ExtendedFAB(
118    icon: Option<View>,
119    label: impl Into<String>,
120    on_click: impl Fn() + 'static,
121    config: FABConfig,
122) -> View {
123    let th = theme();
124    let has_icon = icon.is_some();
125    let is_enabled = config.enabled;
126    let bg = if is_enabled {
127        config.container_color
128    } else {
129        th.on_surface
130            .with_alpha_f32(0.12)
131            .composite_over(th.surface_container_low)
132    };
133    let content_color = if is_enabled {
134        config.content_color
135    } else {
136        th.on_surface.with_alpha_f32(0.38)
137    };
138
139    let elev = if is_enabled {
140        config.state_elevation
141    } else {
142        StateElevation {
143            default: 0.0,
144            hovered: 0.0,
145            focused: 0.0,
146            pressed: 0.0,
147            dragged: 0.0,
148            disabled: 0.0,
149        }
150    };
151
152    let source: Rc<MutableInteractionSource> = config
153        .interaction_source
154        .clone()
155        .map(Rc::new)
156        .unwrap_or_else(|| remember(MutableInteractionSource::new));
157
158    let mut m = Modifier::new()
159        .height(56.0)
160        .min_width(80.0)
161        .background(bg)
162        .state_colors(StateColors {
163            default: Color::TRANSPARENT,
164            hovered: Color::TRANSPARENT,
165            focused: Color::TRANSPARENT,
166            pressed: Color::TRANSPARENT,
167            dragged: config.content_color.with_alpha_f32(0.12),
168            disabled: theme().on_surface.with_alpha_f32(0.12),
169        })
170        .state_elevation(elev)
171        .clip_rounded(FABDefaults::SHAPE_RADIUS)
172        .padding_values(PaddingValues {
173            left: 16.0,
174            right: 20.0,
175            top: 0.0,
176            bottom: 0.0,
177        })
178        .align_items(AlignItems::CENTER);
179
180    m = apply_m3_clickable(m, &source, content_color, is_enabled, on_click);
181    m = with_button_semantics(m, is_enabled);
182    m = m.then(config.modifier);
183    Row(m)
184        .child((
185            icon.map(|v| super::util::icon_content_with_color(content_color, v))
186                .unwrap_or(Box(Modifier::new())),
187            Box(Modifier::new()
188                .width(if has_icon { 12.0 } else { 0.0 })
189                .fill_max_height()),
190            Text(label)
191                .color(content_color)
192                .size(th.typography.label_large)
193                .single_line(),
194        ))
195        
196}