Skip to main content

repose_material/material3/
icon_button.rs

1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use repose_core::*;
6use repose_ui::{Box, ViewExt};
7
8use super::util::{apply_m3_clickable_ex, with_button_semantics};
9use super::*;
10
11/// Color slots for icon buttons.
12#[derive(Clone, Copy, Debug)]
13pub struct IconButtonColors {
14    pub container_color: Color,
15    pub content_color: Color,
16    pub disabled_container_color: Color,
17    pub disabled_content_color: Color,
18}
19
20impl IconButtonColors {
21    pub fn container(&self, enabled: bool) -> Color {
22        if enabled {
23            self.container_color
24        } else {
25            self.disabled_container_color
26        }
27    }
28    pub fn content(&self, enabled: bool) -> Color {
29        if enabled {
30            self.content_color
31        } else {
32            self.disabled_content_color
33        }
34    }
35}
36
37/// Configuration for [`IconButton`], [`FilledIconButton`], [`FilledTonalIconButton`], and [`OutlinedIconButton`].
38#[derive(Clone, Debug)]
39pub struct IconButtonConfig {
40    pub modifier: Modifier,
41    pub enabled: bool,
42    pub colors: IconButtonColors,
43    pub container_size: Option<f32>,
44    pub interaction_source: Option<MutableInteractionSource>,
45    pub shape_radius: Option<f32>,
46}
47
48impl Default for IconButtonConfig {
49    fn default() -> Self {
50        Self {
51            modifier: Modifier::new(),
52            enabled: true,
53            colors: IconButtonColors {
54                container_color: Color::TRANSPARENT,
55                content_color: IconButtonDefaults::content_color(),
56                disabled_container_color: Color::TRANSPARENT,
57                disabled_content_color: IconButtonDefaults::disabled_content_color(),
58            },
59            container_size: None,
60            interaction_source: None,
61            shape_radius: None,
62        }
63    }
64}
65
66#[allow(clippy::too_many_arguments)]
67fn icon_button_render(
68    icon: View,
69    on_click: impl Fn() + 'static,
70    config: &IconButtonConfig,
71    sz: f32,
72    bg: Option<Color>,
73    bdr: Option<(f32, Color)>,
74    state_colors: StateColors,
75    ripple_bounded: bool,
76) -> View {
77    let is_enabled = config.enabled;
78    let content_color = config.colors.content(is_enabled);
79    let radius = config.shape_radius.unwrap_or(sz * 0.5);
80
81    let touch = IconButtonDefaults::MIN_INTERACTIVE_SIZE.max(sz);
82
83    let outer = Modifier::new()
84        .size(touch, touch)
85        .align_items(AlignItems::CENTER)
86        .justify_content(JustifyContent::CENTER)
87        .then(config.modifier.clone());
88
89    // Inner = visual container + ripple + click (Compose Surface).
90    let mut inner = Modifier::new()
91        .size(sz, sz)
92        .clip_rounded(radius)
93        .state_colors(state_colors)
94        .align_items(AlignItems::CENTER)
95        .justify_content(JustifyContent::CENTER);
96
97    if let Some(bg_color) = bg {
98        inner = inner.background(bg_color);
99    }
100    if let Some((w, c)) = bdr {
101        inner = inner.border(w, c, radius);
102    }
103
104    let source: Rc<MutableInteractionSource> = config
105        .interaction_source
106        .clone()
107        .map(Rc::new)
108        .unwrap_or_else(|| remember(MutableInteractionSource::new));
109
110    let (bounded, r) = if ripple_bounded {
111        (true, None)
112    } else {
113        (false, Some(IconButtonDefaults::STATE_LAYER_RADIUS))
114    };
115
116    inner = apply_m3_clickable_ex(
117        inner,
118        &source,
119        content_color,
120        is_enabled,
121        on_click,
122        bounded,
123        r,
124    );
125    inner = with_button_semantics(inner, is_enabled);
126
127    Box(outer).child(Box(inner).child(with_content_color(content_color, move || icon)))
128}
129
130/// M3 Icon Button - a tappable circular container for an icon.
131pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
132    let th = theme();
133    let sz = config
134        .container_size
135        .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
136    icon_button_render(
137        icon,
138        on_click,
139        &config,
140        sz,
141        None,
142        None,
143        StateColors {
144            default: Color::TRANSPARENT,
145            hovered: Color::TRANSPARENT,
146            focused: Color::TRANSPARENT,
147            pressed: Color::TRANSPARENT,
148            dragged: th.on_surface.with_alpha_f32(0.12),
149            disabled: Color::TRANSPARENT,
150        },
151        false,
152    )
153}
154
155/// M3 Filled Icon Button - icon button with a filled container background.
156pub fn FilledIconButton(
157    icon: View,
158    on_click: impl Fn() + 'static,
159    mut config: IconButtonConfig,
160) -> View {
161    let th = theme();
162    if config.colors.container_color == Color::TRANSPARENT
163        && config.colors.disabled_container_color == Color::TRANSPARENT
164    {
165        config.colors = IconButtonColors {
166            container_color: IconButtonDefaults::filled_container_color(),
167            content_color: IconButtonDefaults::filled_content_color(),
168            disabled_container_color: th
169                .on_surface
170                .with_alpha_f32(0.12)
171                .composite_over(th.surface),
172            disabled_content_color: th.on_surface.with_alpha_f32(0.38),
173        };
174    }
175    let is_enabled = config.enabled;
176    let sz = config
177        .container_size
178        .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
179    let bg = config.colors.container(is_enabled);
180    let content_color = config.colors.content(is_enabled);
181    icon_button_render(
182        icon,
183        on_click,
184        &config,
185        sz,
186        Some(bg),
187        None,
188        StateColors {
189            default: Color::TRANSPARENT,
190            hovered: Color::TRANSPARENT,
191            focused: Color::TRANSPARENT,
192            pressed: Color::TRANSPARENT,
193            dragged: content_color.with_alpha_f32(0.12),
194            disabled: th.on_surface.with_alpha_f32(0.12),
195        },
196        true,
197    )
198}
199
200/// M3 Filled Tonal Icon Button - icon button with a secondary container background.
201pub fn FilledTonalIconButton(
202    icon: View,
203    on_click: impl Fn() + 'static,
204    mut config: IconButtonConfig,
205) -> View {
206    let th = theme();
207    if config.colors.container_color == Color::TRANSPARENT
208        && config.colors.disabled_container_color == Color::TRANSPARENT
209    {
210        config.colors = IconButtonColors {
211            container_color: IconButtonDefaults::filled_tonal_container_color(),
212            content_color: IconButtonDefaults::filled_tonal_content_color(),
213            disabled_container_color: th
214                .on_surface
215                .with_alpha_f32(0.12)
216                .composite_over(th.surface),
217            disabled_content_color: th.on_surface.with_alpha_f32(0.38),
218        };
219    }
220    let is_enabled = config.enabled;
221    let sz = config
222        .container_size
223        .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
224    let bg = config.colors.container(is_enabled);
225    let content_color = config.colors.content(is_enabled);
226    icon_button_render(
227        icon,
228        on_click,
229        &config,
230        sz,
231        Some(bg),
232        None,
233        StateColors {
234            default: Color::TRANSPARENT,
235            hovered: Color::TRANSPARENT,
236            focused: Color::TRANSPARENT,
237            pressed: Color::TRANSPARENT,
238            dragged: content_color.with_alpha_f32(0.12),
239            disabled: th.on_surface.with_alpha_f32(0.12),
240        },
241        true,
242    )
243}
244
245/// M3 Outlined Icon Button - icon button with a transparent background and border.
246pub fn OutlinedIconButton(
247    icon: View,
248    on_click: impl Fn() + 'static,
249    config: IconButtonConfig,
250) -> View {
251    let th = theme();
252    let sz = config
253        .container_size
254        .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
255    let border_color = if config.enabled {
256        th.outline
257    } else {
258        th.on_surface.with_alpha_f32(0.12)
259    };
260    icon_button_render(
261        icon,
262        on_click,
263        &config,
264        sz,
265        None,
266        Some((1.0, border_color)),
267        StateColors {
268            default: Color::TRANSPARENT,
269            hovered: Color::TRANSPARENT,
270            focused: Color::TRANSPARENT,
271            pressed: Color::TRANSPARENT,
272            dragged: th.on_surface.with_alpha_f32(0.12),
273            disabled: Color::TRANSPARENT,
274        },
275        true,
276    )
277}