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