1use crate::material::{Glass, GlassDynamics, LiquidModifierExt, LiquidShape};
5use crate::motion::liquid_press_scale;
6use crate::theme::{liquid_colors, liquid_typography};
7use cranpose_macros::composable;
8use cranpose_services::{default_haptics, HapticFeedback};
9use cranpose_ui::rememberMutableInteractionSource;
10use cranpose_ui::text::TextStyle;
11use cranpose_ui::widgets::{Box, BoxSpec, Text};
12use cranpose_ui::{Modifier, Size};
13use cranpose_ui_graphics::{Color, GraphicsLayer};
14use cranpose_ui_layout::Alignment;
15use std::cell::RefCell;
16use std::rc::Rc;
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
20pub enum GlassButtonStyle {
21 #[default]
23 Glass,
24 Prominent,
26 Plain,
28 Destructive,
30}
31
32#[derive(Clone, Debug, Default, PartialEq)]
34pub struct GlassButtonSpec {
35 pub style: GlassButtonStyle,
36 pub glass: Option<Glass>,
38 pub content_color: Option<Color>,
40}
41
42impl GlassButtonSpec {
43 pub fn glass() -> Self {
44 Self::default()
45 }
46
47 pub fn prominent() -> Self {
48 Self {
49 style: GlassButtonStyle::Prominent,
50 ..Self::default()
51 }
52 }
53
54 pub fn plain() -> Self {
55 Self {
56 style: GlassButtonStyle::Plain,
57 ..Self::default()
58 }
59 }
60
61 pub fn destructive() -> Self {
62 Self {
63 style: GlassButtonStyle::Destructive,
64 ..Self::default()
65 }
66 }
67
68 pub fn with_glass(mut self, glass: Glass) -> Self {
69 self.glass = Some(glass);
70 self
71 }
72
73 pub fn with_content_color(mut self, color: Color) -> Self {
74 self.content_color = Some(color);
75 self
76 }
77
78 pub fn content_color(&self, colors: &crate::theme::LiquidColors) -> Color {
80 if let Some(color) = self.content_color {
81 return color;
82 }
83 match self.style {
84 GlassButtonStyle::Glass => colors.accent,
85 GlassButtonStyle::Prominent => colors.on_accent,
86 GlassButtonStyle::Plain => colors.accent,
87 GlassButtonStyle::Destructive => colors.destructive,
88 }
89 }
90
91 pub fn icon_color(&self, colors: &crate::theme::LiquidColors) -> Color {
94 if let Some(color) = self.content_color {
95 return color;
96 }
97 match self.style {
98 GlassButtonStyle::Glass | GlassButtonStyle::Plain => colors.label,
99 GlassButtonStyle::Prominent => colors.on_accent,
100 GlassButtonStyle::Destructive => colors.destructive,
101 }
102 }
103
104 fn resolve_material(&self, colors: &crate::theme::LiquidColors) -> Option<Glass> {
105 if let Some(glass) = &self.glass {
106 return Some(glass.clone());
107 }
108 match self.style {
109 GlassButtonStyle::Glass => Some(Glass::regular()),
110 GlassButtonStyle::Prominent => {
111 Some(Glass::regular().tint(colors.accent.with_alpha(0.75)))
112 }
113 GlassButtonStyle::Plain | GlassButtonStyle::Destructive => None,
114 }
115 }
116}
117
118#[composable]
121#[allow(non_snake_case)]
122pub fn GlassButton(
123 modifier: Modifier,
124 spec: GlassButtonSpec,
125 on_click: impl Fn() + 'static,
126 content: impl FnMut() + 'static,
127) {
128 let colors = liquid_colors();
129 let interaction = rememberMutableInteractionSource();
130 let (pressed_modifier, pressed, content_alpha) =
131 liquid_press_scale(Modifier::empty(), interaction.clone(), 1.08);
132
133 let material = spec.resolve_material(&colors);
134 let mut base = pressed_modifier;
135 if let Some(glass) = material {
136 let pressed_for_glass = pressed;
137 base = base.glass_effect_with(glass, move || GlassDynamics {
138 tilt: (0.0, 0.0),
139 highlight_boost: if pressed_for_glass.get() { 0.85 } else { 0.0 },
140 ..Default::default()
141 });
142 }
143
144 let on_click = Rc::new(RefCell::new(on_click));
145 let base = base
146 .press_interaction_source(interaction)
147 .clickable(move |_point| {
148 default_haptics().perform(HapticFeedback::ImpactLight);
149 (on_click.borrow_mut())();
150 })
151 .padding_symmetric(16.0, 10.0);
152
153 let content_layer = Modifier::empty().graphics_layer(move || GraphicsLayer {
155 alpha: content_alpha.get().clamp(0.0, 1.0),
156 ..Default::default()
157 });
158 let content = Rc::new(RefCell::new(content));
159 Box(
160 base.then(modifier),
161 BoxSpec::default().content_alignment(Alignment::CENTER),
162 move || {
163 let content = Rc::clone(&content);
164 Box(
165 content_layer.clone(),
166 BoxSpec::default().content_alignment(Alignment::CENTER),
167 move || (content.borrow_mut())(),
168 );
169 },
170 );
171}
172
173#[composable]
175#[allow(non_snake_case)]
176pub fn GlassButtonLabel(text: impl Into<String>, spec: GlassButtonSpec) {
177 let typography = liquid_typography();
178 let color = spec.content_color(&liquid_colors());
179 let style = TextStyle {
180 span_style: cranpose_ui::text::SpanStyle {
181 color: Some(color),
182 ..typography.headline.span_style.clone()
183 },
184 ..typography.headline.clone()
185 };
186 Text(text.into(), Modifier::empty(), style);
187}
188
189#[composable]
191#[allow(non_snake_case)]
192pub fn GlassIconButton(
193 modifier: Modifier,
194 spec: GlassButtonSpec,
195 diameter: f32,
196 on_click: impl Fn() + 'static,
197 icon_path: &'static str,
198) {
199 let colors = liquid_colors();
200 let interaction = rememberMutableInteractionSource();
201 let (pressed_modifier, pressed, content_alpha) =
202 liquid_press_scale(Modifier::empty(), interaction.clone(), 1.12);
203
204 let material = spec
205 .resolve_material(&colors)
206 .map(|glass| glass.shape(LiquidShape::Circle));
207 let mut base = pressed_modifier;
208 if let Some(glass) = material {
209 let pressed_for_glass = pressed;
210 base = base.glass_effect_with(glass, move || GlassDynamics {
211 tilt: (0.0, 0.0),
212 highlight_boost: if pressed_for_glass.get() { 0.85 } else { 0.0 },
213 ..Default::default()
214 });
215 }
216
217 let icon_color = spec.icon_color(&colors);
218 let on_click = Rc::new(RefCell::new(on_click));
219 let base = base
220 .press_interaction_source(interaction)
221 .clickable(move |_point| {
222 default_haptics().perform(HapticFeedback::ImpactLight);
223 (on_click.borrow_mut())();
224 })
225 .size(Size::new(diameter, diameter));
226
227 let content_layer = Modifier::empty().graphics_layer(move || GraphicsLayer {
229 alpha: content_alpha.get().clamp(0.0, 1.0),
230 ..Default::default()
231 });
232 Box(
233 base.then(modifier),
234 BoxSpec::default().content_alignment(Alignment::CENTER),
235 move || {
236 Box(
237 content_layer.clone(),
238 BoxSpec::default().content_alignment(Alignment::CENTER),
239 move || {
240 crate::icons::Icon(icon_path, diameter * 0.5, icon_color);
241 },
242 );
243 },
244 );
245}