repose_material/material3/
icon_button.rs1#![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#[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#[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 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
130pub 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
155pub 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
200pub 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
245pub 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}