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, icon_content_with_color, 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 pub fn ensuring_contrast(mut self) -> Self {
39 if self.container_color.3 == 0 {
41 return self;
42 }
43 let paired = content_color_for(self.container_color);
44 let cl = self.content_color.relative_luminance();
46 let bl = self.container_color.relative_luminance();
47 if (cl - bl).abs() < 0.25 {
48 self.content_color = paired;
49 }
50 let dcl = self.disabled_content_color.relative_luminance();
51 let dbl = self.disabled_container_color.relative_luminance();
52 if self.disabled_container_color.3 != 0 && (dcl - dbl).abs() < 0.25 {
53 self.disabled_content_color = theme().on_surface.with_alpha_f32(0.38);
54 }
55 self
56 }
57}
58
59#[derive(Clone, Debug)]
60pub struct IconButtonConfig {
61 pub modifier: Modifier,
62 pub enabled: bool,
63 pub colors: IconButtonColors,
64 pub container_size: Option<f32>,
65 pub interaction_source: Option<MutableInteractionSource>,
66 pub shape_radius: Option<f32>,
67}
68
69impl Default for IconButtonConfig {
70 fn default() -> Self {
71 Self {
72 modifier: Modifier::new(),
73 enabled: true,
74 colors: IconButtonColors {
75 container_color: Color::TRANSPARENT,
76 content_color: Color::TRANSPARENT,
77 disabled_container_color: Color::TRANSPARENT,
78 disabled_content_color: Color::TRANSPARENT,
79 },
80 container_size: None,
81 interaction_source: None,
82 shape_radius: None,
83 }
84 }
85}
86
87fn is_default_colors(c: &IconButtonColors) -> bool {
88 c.container_color == Color::TRANSPARENT
89 && c.disabled_container_color == Color::TRANSPARENT
90 && c.content_color == Color::TRANSPARENT
91 && c.disabled_content_color == Color::TRANSPARENT
92}
93
94fn resolve_colors(
95 config: &IconButtonConfig,
96 variant_defaults: IconButtonColors,
97) -> IconButtonColors {
98 if is_default_colors(&config.colors) {
99 return variant_defaults.ensuring_contrast();
100 }
101 let mut c = config.colors;
102 if c.content_color == Color::TRANSPARENT {
103 c.content_color = if c.container_color.3 == 0 {
104 IconButtonDefaults::content_color()
105 } else {
106 content_color_for(c.container_color)
107 };
108 }
109 if c.disabled_content_color == Color::TRANSPARENT {
110 c.disabled_content_color = theme().on_surface.with_alpha_f32(0.38);
111 }
112 c.ensuring_contrast()
113}
114
115#[allow(clippy::too_many_arguments)]
116fn icon_button_render(
117 icon: View,
118 on_click: impl Fn() + 'static,
119 config: &IconButtonConfig,
120 colors: IconButtonColors,
121 sz: f32,
122 bg: Option<Color>,
123 bdr: Option<(f32, Color)>,
124 state_colors: StateColors,
125 ripple_bounded: bool,
126) -> View {
127 let is_enabled = config.enabled;
128 let content_color = colors.content(is_enabled);
129 let radius = config.shape_radius.unwrap_or(sz * 0.5);
130 let touch = IconButtonDefaults::MIN_INTERACTIVE_SIZE.max(sz);
131
132 let outer = Modifier::new()
133 .size(touch, touch)
134 .align_items(AlignItems::CENTER)
135 .justify_content(JustifyContent::CENTER)
136 .then(config.modifier.clone());
137
138 let mut inner = Modifier::new()
139 .size(sz, sz)
140 .clip_rounded(radius)
141 .state_colors(state_colors)
142 .align_items(AlignItems::CENTER)
143 .justify_content(JustifyContent::CENTER);
144
145 if let Some(bg_color) = bg {
146 inner = inner.background(bg_color);
147 }
148 if let Some((w, c)) = bdr {
149 inner = inner.border(w, c, radius);
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 (bounded, r) = if ripple_bounded {
159 (true, None)
160 } else {
161 (false, Some(IconButtonDefaults::STATE_LAYER_RADIUS))
162 };
163
164 inner = apply_m3_clickable_ex(
165 inner,
166 &source,
167 content_color,
168 is_enabled,
169 on_click,
170 bounded,
171 r,
172 );
173 inner = with_button_semantics(inner, is_enabled);
174
175 let icon = icon_content_with_color(content_color, icon);
176
177 Box(outer).child(Box(inner).child(icon))
178}
179
180pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
182 let colors = resolve_colors(&config, IconButtonDefaults::colors());
183 let sz = config
184 .container_size
185 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
186 let cc = colors.content(config.enabled);
187 icon_button_render(
188 icon,
189 on_click,
190 &config,
191 colors,
192 sz,
193 None, None,
195 StateColors {
196 default: Color::TRANSPARENT,
197 hovered: Color::TRANSPARENT,
198 focused: Color::TRANSPARENT,
199 pressed: Color::TRANSPARENT,
200 dragged: cc.with_alpha_f32(0.12),
201 disabled: Color::TRANSPARENT,
202 },
203 false,
204 )
205}
206
207pub fn FilledIconButton(
208 icon: View,
209 on_click: impl Fn() + 'static,
210 config: IconButtonConfig,
211) -> View {
212 let th = theme();
213 let colors = resolve_colors(&config, IconButtonDefaults::filled_colors());
214 let is_enabled = config.enabled;
215 let sz = config
216 .container_size
217 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
218 let bg = colors.container(is_enabled);
219 let content_color = colors.content(is_enabled);
220 icon_button_render(
221 icon,
222 on_click,
223 &config,
224 colors,
225 sz,
226 Some(bg),
227 None,
228 StateColors {
229 default: Color::TRANSPARENT,
230 hovered: Color::TRANSPARENT,
231 focused: Color::TRANSPARENT,
232 pressed: Color::TRANSPARENT,
233 dragged: content_color.with_alpha_f32(0.12),
234 disabled: th.on_surface.with_alpha_f32(0.12),
235 },
236 true,
237 )
238}
239
240pub fn FilledTonalIconButton(
241 icon: View,
242 on_click: impl Fn() + 'static,
243 config: IconButtonConfig,
244) -> View {
245 let th = theme();
246 let colors = resolve_colors(&config, IconButtonDefaults::filled_tonal_colors());
247 let is_enabled = config.enabled;
248 let sz = config
249 .container_size
250 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
251 let bg = colors.container(is_enabled);
252 let content_color = colors.content(is_enabled);
253 icon_button_render(
254 icon,
255 on_click,
256 &config,
257 colors,
258 sz,
259 Some(bg),
260 None,
261 StateColors {
262 default: Color::TRANSPARENT,
263 hovered: Color::TRANSPARENT,
264 focused: Color::TRANSPARENT,
265 pressed: Color::TRANSPARENT,
266 dragged: content_color.with_alpha_f32(0.12),
267 disabled: th.on_surface.with_alpha_f32(0.12),
268 },
269 true,
270 )
271}
272
273pub fn OutlinedIconButton(
274 icon: View,
275 on_click: impl Fn() + 'static,
276 config: IconButtonConfig,
277) -> View {
278 let th = theme();
279 let colors = resolve_colors(&config, IconButtonDefaults::outlined_colors());
280 let sz = config
281 .container_size
282 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
283 let border_color = if config.enabled {
284 th.outline
285 } else {
286 th.on_surface.with_alpha_f32(0.12)
287 };
288 let cc = colors.content(config.enabled);
289 icon_button_render(
290 icon,
291 on_click,
292 &config,
293 colors,
294 sz,
295 None,
296 Some((1.0, border_color)),
297 StateColors {
298 default: Color::TRANSPARENT,
299 hovered: Color::TRANSPARENT,
300 focused: Color::TRANSPARENT,
301 pressed: Color::TRANSPARENT,
302 dragged: cc.with_alpha_f32(0.12),
303 disabled: Color::TRANSPARENT,
304 },
305 true,
306 )
307}