1use crate::theme::get_global_color;
2use egui::{
3 ecolor::Color32,
4 epaint::{CornerRadius, Stroke},
5 pos2, Area, Id, Order, Rect, Response, Sense, Ui, Vec2,
6};
7
8#[must_use = "You should call .show() to display the action sheet"]
34pub struct MaterialActionSheet<'a> {
35 id: Id,
37 open: &'a mut bool,
39 backdrop: bool,
41 backdrop_dismissible: bool,
43 groups: Vec<ActionGroup<'a>>,
45 current_group: ActionGroup<'a>,
47 max_width: f32,
49}
50
51pub struct ActionGroup<'a> {
53 pub label: Option<String>,
55 pub buttons: Vec<ActionButton<'a>>,
57}
58
59pub struct ActionButton<'a> {
61 pub text: String,
63 pub bold: bool,
65 pub on_click: Option<Box<dyn FnOnce() + 'a>>,
67 pub enabled: bool,
69}
70
71impl<'a> MaterialActionSheet<'a> {
72 pub fn new(id: impl Into<Id>, open: &'a mut bool) -> Self {
78 Self {
79 id: id.into(),
80 open,
81 backdrop: true,
82 backdrop_dismissible: true,
83 groups: Vec::new(),
84 current_group: ActionGroup {
85 label: None,
86 buttons: Vec::new(),
87 },
88 max_width: 448.0, }
90 }
91
92 pub fn backdrop(mut self, show: bool) -> Self {
94 self.backdrop = show;
95 self
96 }
97
98 pub fn backdrop_dismissible(mut self, dismissible: bool) -> Self {
100 self.backdrop_dismissible = dismissible;
101 self
102 }
103
104 pub fn max_width(mut self, width: f32) -> Self {
106 self.max_width = width;
107 self
108 }
109
110 pub fn label(mut self, text: impl Into<String>) -> Self {
112 self.current_group.label = Some(text.into());
113 self
114 }
115
116 pub fn button<F>(mut self, text: impl Into<String>, callback: F) -> Self
118 where
119 F: FnOnce() + 'a,
120 {
121 self.current_group.buttons.push(ActionButton {
122 text: text.into(),
123 bold: false,
124 on_click: Some(Box::new(callback)),
125 enabled: true,
126 });
127 self
128 }
129
130 pub fn bold_button<F>(mut self, text: impl Into<String>, callback: F) -> Self
132 where
133 F: FnOnce() + 'a,
134 {
135 self.current_group.buttons.push(ActionButton {
136 text: text.into(),
137 bold: true,
138 on_click: Some(Box::new(callback)),
139 enabled: true,
140 });
141 self
142 }
143
144 pub fn simple_button(mut self, text: impl Into<String>) -> Self {
146 self.current_group.buttons.push(ActionButton {
147 text: text.into(),
148 bold: false,
149 on_click: None,
150 enabled: true,
151 });
152 self
153 }
154
155 pub fn new_group(mut self) -> Self {
157 if !self.current_group.buttons.is_empty() || self.current_group.label.is_some() {
158 let group = std::mem::replace(
159 &mut self.current_group,
160 ActionGroup {
161 label: None,
162 buttons: Vec::new(),
163 },
164 );
165 self.groups.push(group);
166 }
167 self
168 }
169
170 pub fn show(mut self, ctx: &egui::Context) -> Response {
172 if !self.current_group.buttons.is_empty() || self.current_group.label.is_some() {
174 let group = std::mem::replace(
175 &mut self.current_group,
176 ActionGroup {
177 label: None,
178 buttons: Vec::new(),
179 },
180 );
181 self.groups.push(group);
182 }
183
184 if !*self.open {
185 return Area::new(self.id.with("dummy"))
187 .fixed_pos(pos2(-1000.0, -1000.0))
188 .show(ctx, |ui| ui.allocate_response(Vec2::ZERO, Sense::hover()))
189 .response;
190 }
191
192 if ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
194 *self.open = false;
195 }
196
197 let screen_rect = ctx.viewport_rect();
198 let backdrop_dismissible = self.backdrop_dismissible;
199
200 if self.backdrop {
202 let scrim_color = Color32::from_rgba_unmultiplied(0, 0, 0, 128);
203
204 Area::new(self.id.with("backdrop"))
205 .order(Order::Middle)
206 .fixed_pos(pos2(0.0, 0.0))
207 .show(ctx, |ui| {
208 let scrim_response = ui.allocate_response(screen_rect.size(), Sense::click());
209 ui.painter()
210 .rect_filled(screen_rect, CornerRadius::ZERO, scrim_color);
211
212 if scrim_response.clicked() && backdrop_dismissible {
213 *self.open = false;
214 }
215 });
216 }
217
218 let open_ptr = self.open as *mut bool;
220 Area::new(self.id.with("sheet"))
221 .order(Order::Foreground)
222 .anchor(egui::Align2::CENTER_BOTTOM, Vec2::new(0.0, 0.0))
223 .show(ctx, |ui| {
224 let width = self.max_width.min(screen_rect.width());
225 ui.set_width(width);
226
227 unsafe { self.render_content(ui, &mut *open_ptr) }
230 })
231 .response
232 }
233
234 fn render_content(self, ui: &mut Ui, open: &'a mut bool) -> Response {
235 let background_color = get_global_color("surfaceContainer");
236 let corner_radius = CornerRadius {
237 nw: 16,
238 ne: 16,
239 sw: 0,
240 se: 0,
241 };
242
243 let mut total_height = 0.0;
244 let button_height = 48.0; for group in &self.groups {
248 if group.label.is_some() {
249 total_height += button_height;
250 }
251 total_height += group.buttons.len() as f32 * button_height;
252 }
253
254 total_height += 8.0;
256
257 let available_rect = ui.available_rect_before_wrap();
258 let sheet_size = Vec2::new(available_rect.width(), total_height);
259 let sheet_rect = Rect::from_min_size(
260 pos2(available_rect.min.x, available_rect.max.y - total_height),
261 sheet_size,
262 );
263
264 ui.painter()
266 .rect_filled(sheet_rect, corner_radius, background_color);
267
268 let mut current_y = sheet_rect.min.y;
269 let mut response = ui.allocate_response(sheet_size, Sense::hover());
270
271 let mut callbacks_to_execute: Vec<Box<dyn FnOnce()>> = Vec::new();
273
274 let groups_len = self.groups.len();
276
277 for (group_idx, group) in self.groups.into_iter().enumerate() {
279 if let Some(label) = &group.label {
281 let label_rect = Rect::from_min_size(
282 pos2(sheet_rect.min.x, current_y),
283 Vec2::new(sheet_rect.width(), button_height),
284 );
285
286 let text_color = get_global_color("primary");
287 let text_pos = pos2(label_rect.min.x + 16.0, current_y + button_height / 2.0);
288
289 ui.painter().text(
290 text_pos,
291 egui::Align2::LEFT_CENTER,
292 label,
293 egui::FontId::proportional(14.0),
294 text_color,
295 );
296
297 current_y += button_height;
298 }
299
300 for (button_idx, mut button) in group.buttons.into_iter().enumerate() {
302 let button_id = self.id.with("group").with(group_idx).with(button_idx);
303 let button_rect = Rect::from_min_size(
304 pos2(sheet_rect.min.x, current_y),
305 Vec2::new(sheet_rect.width(), button_height),
306 );
307
308 let button_response = ui.interact(button_rect, button_id, Sense::click());
309
310 if button_response.hovered() && button.enabled {
312 let hover_color = get_global_color("onSurface").linear_multiply(0.08);
313 ui.painter()
314 .rect_filled(button_rect, CornerRadius::ZERO, hover_color);
315 }
316
317 let text_color = if !button.enabled {
319 get_global_color("onSurface").linear_multiply(0.38)
320 } else {
321 get_global_color("onSurface")
322 };
323
324 let font_id = if button.bold {
325 egui::FontId::proportional(16.0)
326 } else {
327 egui::FontId::proportional(16.0)
328 };
329
330 let text_pos = pos2(button_rect.min.x + 16.0, current_y + button_height / 2.0);
331
332 ui.painter().text(
333 text_pos,
334 egui::Align2::LEFT_CENTER,
335 &button.text,
336 font_id,
337 text_color,
338 );
339
340 if button_response.clicked() && button.enabled {
342 *open = false;
343 if let Some(callback) = button.on_click.take() {
344 callbacks_to_execute.push(callback);
345 }
346 }
347
348 response = response.union(button_response);
349 current_y += button_height;
350 }
351
352 if group_idx < groups_len - 1 {
354 let divider_y = current_y;
355 ui.painter().line_segment(
356 [
357 pos2(sheet_rect.min.x, divider_y),
358 pos2(sheet_rect.max.x, divider_y),
359 ],
360 Stroke::new(1.0, get_global_color("outlineVariant")),
361 );
362 }
363 }
364
365 for callback in callbacks_to_execute {
367 callback();
368 }
369
370 response
371 }
372}
373
374pub fn action_sheet<'a>(id: impl Into<Id>, open: &'a mut bool) -> MaterialActionSheet<'a> {
387 MaterialActionSheet::new(id, open)
388}