1use crate::theme::get_global_color;
2use egui::{
3 ecolor::Color32,
4 epaint::{Stroke, CornerRadius, Shadow},
5 Rect, Response, Sense, Ui, Vec2, Widget, Id, Area, SidePanel, Order, pos2,
6};
7
8#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum DrawerVariant {
11 Permanent,
12 Dismissible,
13 Modal,
14}
15
16pub struct MaterialDrawer<'a> {
35 variant: DrawerVariant,
36 open: &'a mut bool,
37 width: f32,
38 header_title: Option<String>,
39 header_subtitle: Option<String>,
40 items: Vec<DrawerItem>,
41 corner_radius: CornerRadius,
42 elevation: Option<Shadow>,
43 id: Id,
44}
45
46pub struct DrawerItem {
47 pub text: String,
48 pub icon: Option<String>,
49 pub active: bool,
50 pub on_click: Option<Box<dyn Fn() + Send + Sync>>,
51}
52
53impl DrawerItem {
54 pub fn new(text: impl Into<String>) -> Self {
55 Self {
56 text: text.into(),
57 icon: None,
58 active: false,
59 on_click: None,
60 }
61 }
62
63 pub fn icon(mut self, icon: impl Into<String>) -> Self {
64 self.icon = Some(icon.into());
65 self
66 }
67
68 pub fn active(mut self, active: bool) -> Self {
69 self.active = active;
70 self
71 }
72
73 pub fn on_click<F>(mut self, callback: F) -> Self
74 where
75 F: Fn() + Send + Sync + 'static,
76 {
77 self.on_click = Some(Box::new(callback));
78 self
79 }
80}
81
82impl<'a> MaterialDrawer<'a> {
83 pub fn new(variant: DrawerVariant, open: &'a mut bool) -> Self {
85 let id = Id::new(format!("material_drawer_{:?}", variant));
86 Self {
87 variant,
88 open,
89 width: 256.0, header_title: None,
91 header_subtitle: None,
92 items: Vec::new(),
93 corner_radius: CornerRadius::ZERO,
94 elevation: None,
95 id,
96 }
97 }
98
99 pub fn new_with_id(variant: DrawerVariant, open: &'a mut bool, id: Id) -> Self {
101 Self {
102 variant,
103 open,
104 width: 256.0,
105 header_title: None,
106 header_subtitle: None,
107 items: Vec::new(),
108 corner_radius: CornerRadius::ZERO,
109 elevation: None,
110 id,
111 }
112 }
113
114 pub fn width(mut self, width: f32) -> Self {
116 self.width = width;
117 self
118 }
119
120 pub fn header(mut self, title: impl Into<String>, subtitle: Option<impl Into<String>>) -> Self {
122 self.header_title = Some(title.into());
123 self.header_subtitle = subtitle.map(|s| s.into());
124 self
125 }
126
127 pub fn item(mut self, text: impl Into<String>, icon: Option<impl Into<String>>, active: bool) -> Self {
129 self.items.push(DrawerItem {
130 text: text.into(),
131 icon: icon.map(|i| i.into()),
132 active,
133 on_click: None,
134 });
135 self
136 }
137
138 pub fn item_with_callback<F>(mut self, text: impl Into<String>, icon: Option<impl Into<String>>, active: bool, callback: F) -> Self
140 where
141 F: Fn() + Send + Sync + 'static,
142 {
143 self.items.push(DrawerItem {
144 text: text.into(),
145 icon: icon.map(|i| i.into()),
146 active,
147 on_click: Some(Box::new(callback)),
148 });
149 self
150 }
151
152 pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
154 self.corner_radius = corner_radius.into();
155 self
156 }
157
158 pub fn elevation(mut self, elevation: impl Into<Shadow>) -> Self {
160 self.elevation = Some(elevation.into());
161 self
162 }
163
164 fn get_drawer_style(&self) -> (Color32, Option<Stroke>, bool) {
165 let md_surface = get_global_color("surface");
166 let md_outline = get_global_color("outline");
167
168 match self.variant {
169 DrawerVariant::Permanent => {
170 (md_surface, Some(Stroke::new(1.0, md_outline)), false)
172 },
173 DrawerVariant::Modal => {
174 (md_surface, None, true)
176 },
177 DrawerVariant::Dismissible => {
178 (md_surface, Some(Stroke::new(1.0, md_outline)), false)
180 },
181 }
182 }
183
184 pub fn show(self, ctx: &egui::Context) -> Response {
186 match self.variant {
187 DrawerVariant::Permanent => self.show_permanent(ctx),
188 DrawerVariant::Dismissible => self.show_dismissible(ctx),
189 DrawerVariant::Modal => self.show_modal(ctx),
190 }
191 }
192
193 fn show_permanent(self, ctx: &egui::Context) -> Response {
194 SidePanel::left(self.id.with("permanent"))
195 .default_width(self.width)
196 .resizable(false)
197 .show(ctx, |ui| {
198 self.render_drawer_content(ui)
199 })
200 .response
201 }
202
203 fn show_dismissible(self, ctx: &egui::Context) -> Response {
204 if *self.open {
205 SidePanel::left(self.id.with("dismissible"))
206 .default_width(self.width)
207 .resizable(false)
208 .show(ctx, |ui| {
209 self.render_drawer_content(ui)
210 })
211 .response
212 } else {
213 Area::new(self.id.with("dismissible_dummy"))
215 .fixed_pos(pos2(-1000.0, -1000.0)) .show(ctx, |ui| {
217 ui.allocate_response(Vec2::ZERO, Sense::hover())
218 })
219 .response
220 }
221 }
222
223 fn show_modal(self, ctx: &egui::Context) -> Response {
224 if *self.open {
225 let screen_rect = ctx.screen_rect();
227 Area::new(self.id.with("modal_scrim"))
228 .order(Order::Background)
229 .show(ctx, |ui| {
230 let scrim_response = ui.allocate_response(screen_rect.size(), Sense::click());
231 ui.painter().rect_filled(
232 screen_rect,
233 CornerRadius::ZERO,
234 Color32::from_rgba_unmultiplied(0, 0, 0, 128), );
236
237 if scrim_response.clicked() {
239 *self.open = false;
240 }
241 });
242
243 Area::new(self.id.with("modal_drawer"))
245 .order(Order::Foreground)
246 .fixed_pos(pos2(0.0, 0.0))
247 .show(ctx, |ui| {
248 ui.set_width(self.width);
249 ui.set_height(screen_rect.height());
250 self.render_drawer_content(ui)
251 })
252 .response
253 } else {
254 Area::new(self.id.with("modal_dummy"))
256 .fixed_pos(pos2(-1000.0, -1000.0)) .show(ctx, |ui| {
258 ui.allocate_response(Vec2::ZERO, Sense::hover())
259 })
260 .response
261 }
262 }
263
264 fn render_drawer_content(self, ui: &mut Ui) -> Response {
265 let (background_color, border_stroke, has_elevation) = self.get_drawer_style();
266
267 if matches!(self.variant, DrawerVariant::Dismissible | DrawerVariant::Modal) {
269 if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
270 *self.open = false;
271 }
272 }
273
274 let header_height = if self.header_title.is_some() { 64.0 } else { 0.0 };
276 let item_height = 48.0;
277 let items_height = self.items.len() as f32 * item_height;
278 let _total_height = header_height + items_height;
279
280 let available_rect = ui.available_rect_before_wrap();
281 let drawer_rect = Rect::from_min_size(available_rect.min, Vec2::new(self.width, available_rect.height()));
282
283 ui.painter().rect_filled(drawer_rect, self.corner_radius, background_color);
285
286 if let Some(stroke) = border_stroke {
288 ui.painter().rect_stroke(drawer_rect, self.corner_radius, stroke, egui::epaint::StrokeKind::Outside);
289 }
290
291 if has_elevation {
293 let shadow_offset = Vec2::new(0.0, 4.0);
295 let shadow_color = Color32::from_rgba_unmultiplied(0, 0, 0, 20);
296
297 for i in 1..=3 {
299 let shadow_rect = drawer_rect.translate(shadow_offset * i as f32 * 0.5);
300 ui.painter().rect_filled(shadow_rect, self.corner_radius, shadow_color);
301 }
302 }
303
304 let mut current_y = drawer_rect.min.y;
305
306 if let Some(title) = &self.header_title {
308 let header_rect = Rect::from_min_size(
309 egui::pos2(drawer_rect.min.x, current_y),
310 Vec2::new(self.width, header_height)
311 );
312
313 let header_color = background_color.linear_multiply(0.95);
315 ui.painter().rect_filled(header_rect, CornerRadius::ZERO, header_color);
316
317 let title_pos = egui::pos2(
319 header_rect.min.x + 16.0,
320 header_rect.min.y + 16.0
321 );
322 ui.painter().text(
323 title_pos,
324 egui::Align2::LEFT_TOP,
325 title,
326 egui::TextStyle::Heading.resolve(ui.style()),
327 get_global_color("onSurface")
328 );
329
330 if let Some(subtitle) = &self.header_subtitle {
331 let subtitle_pos = egui::pos2(
332 header_rect.min.x + 16.0,
333 header_rect.min.y + 36.0
334 );
335 ui.painter().text(
336 subtitle_pos,
337 egui::Align2::LEFT_TOP,
338 subtitle,
339 egui::TextStyle::Body.resolve(ui.style()),
340 get_global_color("onSurfaceVariant")
341 );
342 }
343
344 current_y += header_height;
345 }
346
347 let mut response = ui.allocate_response(drawer_rect.size(), Sense::hover());
348
349 for (index, item) in self.items.iter().enumerate() {
351 let item_rect = Rect::from_min_size(
352 egui::pos2(drawer_rect.min.x, current_y),
353 Vec2::new(self.width, item_height)
354 );
355
356 let item_id = self.id.with("item").with(index);
358 let item_response = ui.interact(item_rect, item_id, Sense::click());
359
360 if item.active {
362 let active_color = get_global_color("primary").linear_multiply(0.12);
363 ui.painter().rect_filled(item_rect, CornerRadius::ZERO, active_color);
364 } else if item_response.hovered() {
365 let hover_color = get_global_color("onSurface").linear_multiply(0.08);
366 ui.painter().rect_filled(item_rect, CornerRadius::ZERO, hover_color);
367 }
368
369 let mut current_x = item_rect.min.x + 16.0;
370
371 if let Some(_icon) = &item.icon {
373 let icon_center = egui::pos2(current_x + 12.0, current_y + item_height / 2.0);
375 let icon_color = if item.active {
376 get_global_color("primary")
377 } else {
378 get_global_color("onSurfaceVariant")
379 };
380
381 ui.painter().circle_filled(icon_center, 10.0, icon_color);
382 current_x += 40.0; }
384
385 let text_color = if item.active {
387 get_global_color("primary")
388 } else {
389 get_global_color("onSurface")
390 };
391
392 let text_pos = egui::pos2(current_x, current_y + (item_height - ui.text_style_height(&egui::TextStyle::Body)) / 2.0);
393 ui.painter().text(
394 text_pos,
395 egui::Align2::LEFT_TOP,
396 &item.text,
397 egui::TextStyle::Body.resolve(ui.style()),
398 text_color
399 );
400
401 if item_response.clicked() {
403 if let Some(callback) = &item.on_click {
404 callback();
405 }
406 }
407
408 response = response.union(item_response);
409 current_y += item_height;
410 }
411
412 response
413 }
414}
415
416impl Widget for MaterialDrawer<'_> {
417 fn ui(self, ui: &mut Ui) -> Response {
418 self.render_drawer_content(ui)
421 }
422}
423
424pub fn permanent_drawer(open: &mut bool) -> MaterialDrawer<'_> {
426 MaterialDrawer::new(DrawerVariant::Permanent, open)
427}
428
429pub fn dismissible_drawer(open: &mut bool) -> MaterialDrawer<'_> {
431 MaterialDrawer::new(DrawerVariant::Dismissible, open)
432}
433
434pub fn modal_drawer(open: &mut bool) -> MaterialDrawer<'_> {
436 MaterialDrawer::new(DrawerVariant::Modal, open)
437}
438
439pub fn standard_drawer(open: &mut bool) -> MaterialDrawer<'_> {
441 permanent_drawer(open)
442}