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