Skip to main content

egui_material3/
drawer.rs

1use crate::theme::get_global_color;
2use egui::{
3    ecolor::Color32,
4    epaint::{CornerRadius, Stroke},
5    pos2, Area, Id, Order, Rect, Response, Sense, SidePanel, Ui, Vec2, Widget,
6};
7
8/// Material Design navigation drawer variants.
9#[derive(Clone, Copy, Debug, PartialEq)]
10pub enum DrawerVariant {
11    Permanent,
12    Dismissible,
13    Modal,
14}
15
16/// The alignment of a drawer (start or end side).
17#[derive(Clone, Copy, Debug, PartialEq)]
18pub enum DrawerAlignment {
19    /// Drawer at the start side (left in LTR, right in RTL)
20    Start,
21    /// Drawer at the end side (right in LTR, left in RTL)
22    End,
23}
24
25/// Theme data for Material Design drawers.
26#[derive(Clone, Debug)]
27pub struct DrawerThemeData {
28    pub background_color: Option<Color32>,
29    pub scrim_color: Option<Color32>,
30    pub elevation: Option<f32>,
31    pub shadow_color: Option<Color32>,
32    pub surface_tint_color: Option<Color32>,
33    pub shape: Option<CornerRadius>,
34    pub end_shape: Option<CornerRadius>,
35    pub width: Option<f32>,
36    pub clip_behavior: Option<bool>,
37}
38
39impl Default for DrawerThemeData {
40    fn default() -> Self {
41        Self {
42            background_color: None,
43            scrim_color: None,
44            elevation: None,
45            shadow_color: None,
46            surface_tint_color: None,
47            shape: None,
48            end_shape: None,
49            width: None,
50            clip_behavior: None,
51        }
52    }
53}
54
55impl DrawerThemeData {
56    /// Create Material 3 defaults for drawer theming.
57    pub fn material3_defaults() -> Self {
58        Self {
59            background_color: Some(get_global_color("surfaceContainerLow")),
60            scrim_color: Some(Color32::from_rgba_unmultiplied(0, 0, 0, 138)),
61            elevation: Some(1.0),
62            shadow_color: Some(Color32::TRANSPARENT),
63            surface_tint_color: Some(Color32::TRANSPARENT),
64            shape: Some(CornerRadius::same(16)),
65            end_shape: Some(CornerRadius::same(16)),
66            width: Some(360.0),
67            clip_behavior: Some(true),
68        }
69    }
70
71    /// Create Material 2 defaults for drawer theming.
72    pub fn material2_defaults() -> Self {
73        Self {
74            background_color: Some(get_global_color("surface")),
75            scrim_color: Some(Color32::from_rgba_unmultiplied(0, 0, 0, 138)),
76            elevation: Some(16.0),
77            shadow_color: None,
78            surface_tint_color: None,
79            shape: Some(CornerRadius::ZERO),
80            end_shape: Some(CornerRadius::ZERO),
81            width: Some(304.0),
82            clip_behavior: Some(true),
83        }
84    }
85}
86
87/// Material Design drawer header component.
88///
89/// Provides a header area for drawers with customizable decoration and content.
90pub struct DrawerHeader {
91    decoration_color: Option<Color32>,
92    margin: f32,
93    padding: Vec2,
94    height: f32,
95    title: Option<String>,
96    subtitle: Option<String>,
97}
98
99impl Default for DrawerHeader {
100    fn default() -> Self {
101        Self {
102            decoration_color: None,
103            margin: 8.0,
104            padding: Vec2::new(16.0, 16.0),
105            height: 160.0,
106            title: None,
107            subtitle: None,
108        }
109    }
110}
111
112impl DrawerHeader {
113    pub fn new() -> Self {
114        Self::default()
115    }
116
117    pub fn decoration_color(mut self, color: Color32) -> Self {
118        self.decoration_color = Some(color);
119        self
120    }
121
122    pub fn margin(mut self, margin: f32) -> Self {
123        self.margin = margin;
124        self
125    }
126
127    pub fn padding(mut self, padding: Vec2) -> Self {
128        self.padding = padding;
129        self
130    }
131
132    pub fn height(mut self, height: f32) -> Self {
133        self.height = height;
134        self
135    }
136
137    pub fn title(mut self, title: impl Into<String>) -> Self {
138        self.title = Some(title.into());
139        self
140    }
141
142    pub fn subtitle(mut self, subtitle: impl Into<String>) -> Self {
143        self.subtitle = Some(subtitle.into());
144        self
145    }
146
147    pub fn show(self, ui: &mut Ui) -> Response {
148        let rect = ui.allocate_space(Vec2::new(ui.available_width(), self.height + self.margin)).1;
149        
150        let header_rect = Rect::from_min_size(
151            rect.min + Vec2::new(0.0, 0.0),
152            Vec2::new(rect.width(), self.height),
153        );
154
155        // Draw decoration background
156        let bg_color = self.decoration_color.unwrap_or_else(|| get_global_color("surfaceContainerHigh"));
157        ui.painter().rect_filled(header_rect, CornerRadius::ZERO, bg_color);
158
159        // Draw border at bottom
160        let border_y = header_rect.max.y;
161        ui.painter().line_segment(
162            [egui::pos2(header_rect.min.x, border_y), egui::pos2(header_rect.max.x, border_y)],
163            Stroke::new(1.0, get_global_color("outlineVariant")),
164        );
165
166        // Draw content with padding
167        let content_rect = header_rect.shrink2(self.padding);
168        
169        if let Some(title) = &self.title {
170            let title_pos = egui::pos2(content_rect.min.x, content_rect.min.y);
171            ui.painter().text(
172                title_pos,
173                egui::Align2::LEFT_TOP,
174                title,
175                egui::FontId::proportional(22.0),
176                get_global_color("onSurface"),
177            );
178        }
179
180        if let Some(subtitle) = &self.subtitle {
181            let subtitle_pos = egui::pos2(content_rect.min.x, content_rect.min.y + 32.0);
182            ui.painter().text(
183                subtitle_pos,
184                egui::Align2::LEFT_TOP,
185                subtitle,
186                egui::FontId::proportional(14.0),
187                get_global_color("onSurfaceVariant"),
188            );
189        }
190
191        ui.interact(rect, ui.id().with("drawer_header"), Sense::hover())
192    }
193}
194
195/// Material Design navigation drawer component.
196///
197/// Navigation drawers provide access to destinations and app functionality.
198/// They can be permanently on-screen or controlled by navigation triggers.
199///
200/// ```
201/// # egui::__run_test_ui(|ui| {
202/// let mut drawer_open = true;
203///
204/// let drawer = MaterialDrawer::new(DrawerVariant::Permanent, &mut drawer_open)
205///     .header("Mail", Some("email@material.io"))
206///     .item("Inbox", Some("inbox"), true)
207///     .item("Sent", Some("send"), false)
208///     .item("Drafts", Some("drafts"), false);
209///
210/// drawer.show(ui.ctx());
211/// # });
212/// ```
213pub struct MaterialDrawer<'a> {
214    variant: DrawerVariant,
215    open: &'a mut bool,
216    width: f32,
217    alignment: DrawerAlignment,
218    header_title: Option<String>,
219    header_subtitle: Option<String>,
220    items: Vec<DrawerItem>,
221    sections: Vec<DrawerSection>,
222    corner_radius: CornerRadius,
223    elevation: Option<f32>,
224    theme: DrawerThemeData,
225    enable_drag_gesture: bool,
226    edge_drag_width: Option<f32>,
227    barrier_dismissible: bool,
228    semantic_label: Option<String>,
229    id: Id,
230}
231
232/// A section in the navigation drawer with a label and items.
233pub struct DrawerSection {
234    pub label: Option<String>,
235    pub items: Vec<DrawerItem>,
236}
237
238/// A navigation item in a drawer.
239pub struct DrawerItem {
240    pub text: String,
241    pub icon: Option<String>,
242    pub active: bool,
243    pub enabled: bool,
244    pub badge: Option<String>,
245    pub on_click: Option<Box<dyn Fn() + Send + Sync>>,
246}
247
248impl DrawerItem {
249    pub fn new(text: impl Into<String>) -> Self {
250        Self {
251            text: text.into(),
252            icon: None,
253            active: false,
254            enabled: true,
255            badge: None,
256            on_click: None,
257        }
258    }
259
260    pub fn icon(mut self, icon: impl Into<String>) -> Self {
261        self.icon = Some(icon.into());
262        self
263    }
264
265    pub fn active(mut self, active: bool) -> Self {
266        self.active = active;
267        self
268    }
269
270    pub fn enabled(mut self, enabled: bool) -> Self {
271        self.enabled = enabled;
272        self
273    }
274
275    pub fn badge(mut self, badge: impl Into<String>) -> Self {
276        self.badge = Some(badge.into());
277        self
278    }
279
280    pub fn on_click<F>(mut self, callback: F) -> Self
281    where
282        F: Fn() + Send + Sync + 'static,
283    {
284        self.on_click = Some(Box::new(callback));
285        self
286    }
287}
288
289impl<'a> MaterialDrawer<'a> {
290    /// Create a new navigation drawer with Material 3 defaults.
291    pub fn new(variant: DrawerVariant, open: &'a mut bool) -> Self {
292        let id = Id::new(format!("material_drawer_{:?}", variant));
293        let theme = DrawerThemeData::material3_defaults();
294        let width = theme.width.unwrap_or(360.0);
295        let corner_radius = theme.shape.unwrap_or(CornerRadius::same(16));
296        let elevation = theme.elevation;
297        
298        Self {
299            variant,
300            open,
301            width,
302            alignment: DrawerAlignment::Start,
303            header_title: None,
304            header_subtitle: None,
305            items: Vec::new(),
306            sections: Vec::new(),
307            corner_radius,
308            elevation,
309            theme,
310            enable_drag_gesture: true,
311            edge_drag_width: None,
312            barrier_dismissible: true,
313            semantic_label: None,
314            id,
315        }
316    }
317
318    /// Create a new navigation drawer with custom ID.
319    pub fn new_with_id(variant: DrawerVariant, open: &'a mut bool, id: Id) -> Self {
320        let theme = DrawerThemeData::material3_defaults();
321        let width = theme.width.unwrap_or(360.0);
322        let corner_radius = theme.shape.unwrap_or(CornerRadius::same(16));
323        let elevation = theme.elevation;
324        
325        Self {
326            variant,
327            open,
328            width,
329            alignment: DrawerAlignment::Start,
330            header_title: None,
331            header_subtitle: None,
332            items: Vec::new(),
333            sections: Vec::new(),
334            corner_radius,
335            elevation,
336            theme,
337            enable_drag_gesture: true,
338            edge_drag_width: None,
339            barrier_dismissible: true,
340            semantic_label: None,
341            id,
342        }
343    }
344
345    /// Set drawer alignment (start or end).
346    pub fn alignment(mut self, alignment: DrawerAlignment) -> Self {
347        self.alignment = alignment;
348        self
349    }
350
351    /// Set drawer width.
352    pub fn width(mut self, width: f32) -> Self {
353        self.width = width;
354        self
355    }
356
357    /// Set drawer theme.
358    pub fn theme(mut self, theme: DrawerThemeData) -> Self {
359        if let Some(width) = theme.width {
360            self.width = width;
361        }
362        if let Some(shape) = theme.shape {
363            self.corner_radius = shape;
364        }
365        if let Some(elevation) = theme.elevation {
366            self.elevation = Some(elevation);
367        }
368        self.theme = theme;
369        self
370    }
371
372    /// Enable or disable drag gestures.
373    pub fn enable_drag_gesture(mut self, enable: bool) -> Self {
374        self.enable_drag_gesture = enable;
375        self
376    }
377
378    /// Set the edge drag width for opening the drawer.
379    pub fn edge_drag_width(mut self, width: f32) -> Self {
380        self.edge_drag_width = Some(width);
381        self
382    }
383
384    /// Set whether tapping the barrier dismisses the drawer.
385    pub fn barrier_dismissible(mut self, dismissible: bool) -> Self {
386        self.barrier_dismissible = dismissible;
387        self
388    }
389
390    /// Set semantic label for accessibility.
391    pub fn semantic_label(mut self, label: impl Into<String>) -> Self {
392        self.semantic_label = Some(label.into());
393        self
394    }
395
396    /// Add header with title and optional subtitle.
397    pub fn header(mut self, title: impl Into<String>, subtitle: Option<impl Into<String>>) -> Self {
398        self.header_title = Some(title.into());
399        self.header_subtitle = subtitle.map(|s| s.into());
400        self
401    }
402
403    /// Add a navigation item.
404    pub fn item(
405        mut self,
406        text: impl Into<String>,
407        icon: Option<impl Into<String>>,
408        active: bool,
409    ) -> Self {
410        self.items.push(DrawerItem {
411            text: text.into(),
412            icon: icon.map(|i| i.into()),
413            active,
414            enabled: true,
415            badge: None,
416            on_click: None,
417        });
418        self
419    }
420
421    /// Add a navigation item with callback.
422    pub fn item_with_callback<F>(
423        mut self,
424        text: impl Into<String>,
425        icon: Option<impl Into<String>>,
426        active: bool,
427        callback: F,
428    ) -> Self
429    where
430        F: Fn() + Send + Sync + 'static,
431    {
432        self.items.push(DrawerItem {
433            text: text.into(),
434            icon: icon.map(|i| i.into()),
435            active,
436            enabled: true,
437            badge: None,
438            on_click: Some(Box::new(callback)),
439        });
440        self
441    }
442
443    /// Add a drawer item object.
444    pub fn add_item(mut self, item: DrawerItem) -> Self {
445        self.items.push(item);
446        self
447    }
448
449    /// Add a section with label and items.
450    pub fn section(mut self, label: Option<impl Into<String>>, items: Vec<DrawerItem>) -> Self {
451        self.sections.push(DrawerSection {
452            label: label.map(|l| l.into()),
453            items,
454        });
455        self
456    }
457
458    /// Set corner radius.
459    pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
460        self.corner_radius = corner_radius.into();
461        self
462    }
463
464    /// Set elevation value.
465    pub fn elevation(mut self, elevation: f32) -> Self {
466        self.elevation = Some(elevation);
467        self
468    }
469
470    fn get_drawer_style(&self) -> (Color32, Option<Stroke>, f32) {
471        let background_color = self.theme.background_color
472            .unwrap_or_else(|| get_global_color("surfaceContainerLow"));
473        
474        let elevation = self.elevation.unwrap_or(1.0);
475        
476        match self.variant {
477            DrawerVariant::Permanent => {
478                // Permanent drawer: surface with subtle border
479                let border_color = get_global_color("outlineVariant");
480                (background_color, Some(Stroke::new(1.0, border_color)), elevation)
481            }
482            DrawerVariant::Modal => {
483                // Modal drawer: elevated surface, no border
484                (background_color, None, elevation)
485            }
486            DrawerVariant::Dismissible => {
487                // Dismissible drawer: surface with subtle border
488                let border_color = get_global_color("outlineVariant");
489                (background_color, Some(Stroke::new(1.0, border_color)), elevation)
490            }
491        }
492    }
493
494    /// Show the drawer using appropriate egui layout.
495    pub fn show(self, ctx: &egui::Context) -> Response {
496        match self.variant {
497            DrawerVariant::Permanent => self.show_permanent(ctx),
498            DrawerVariant::Dismissible => self.show_dismissible(ctx),
499            DrawerVariant::Modal => self.show_modal(ctx),
500        }
501    }
502
503    fn show_permanent(self, ctx: &egui::Context) -> Response {
504        SidePanel::left(self.id.with("permanent"))
505            .default_width(self.width)
506            .resizable(false)
507            .show(ctx, |ui| self.render_drawer_content(ui))
508            .response
509    }
510
511    fn show_dismissible(self, ctx: &egui::Context) -> Response {
512        if *self.open {
513            SidePanel::left(self.id.with("dismissible"))
514                .default_width(self.width)
515                .resizable(false)
516                .show(ctx, |ui| self.render_drawer_content(ui))
517                .response
518        } else {
519            // Return empty response when closed
520            Area::new(self.id.with("dismissible_dummy"))
521                .fixed_pos(pos2(-1000.0, -1000.0)) // Place offscreen
522                .show(ctx, |ui| ui.allocate_response(Vec2::ZERO, Sense::hover()))
523                .response
524        }
525    }
526
527    fn show_modal(self, ctx: &egui::Context) -> Response {
528        if *self.open {
529            // Draw scrim background
530            let screen_rect = ctx.screen_rect();
531            let scrim_color = self.theme.scrim_color
532                .unwrap_or(Color32::from_rgba_unmultiplied(0, 0, 0, 138));
533            
534            Area::new(self.id.with("modal_scrim"))
535                .order(Order::Background)
536                .show(ctx, |ui| {
537                    let scrim_response = ui.allocate_response(screen_rect.size(), Sense::click());
538                    ui.painter().rect_filled(
539                        screen_rect,
540                        CornerRadius::ZERO,
541                        scrim_color,
542                    );
543
544                    // Close drawer if scrim is clicked and barrier is dismissible
545                    if scrim_response.clicked() && self.barrier_dismissible {
546                        *self.open = false;
547                    }
548                });
549
550            // Draw the actual modal drawer
551            Area::new(self.id.with("modal_drawer"))
552                .order(Order::Foreground)
553                .fixed_pos(pos2(0.0, 0.0))
554                .show(ctx, |ui| {
555                    ui.set_width(self.width);
556                    ui.set_height(screen_rect.height());
557                    self.render_drawer_content(ui)
558                })
559                .response
560        } else {
561            // Return empty response when closed
562            Area::new(self.id.with("modal_dummy"))
563                .fixed_pos(pos2(-1000.0, -1000.0)) // Place offscreen
564                .show(ctx, |ui| ui.allocate_response(Vec2::ZERO, Sense::hover()))
565                .response
566        }
567    }
568
569    fn render_drawer_content(self, ui: &mut Ui) -> Response {
570        let (background_color, border_stroke, _elevation) = self.get_drawer_style();
571
572        // Handle ESC key for dismissible and modal drawers
573        if matches!(
574            self.variant,
575            DrawerVariant::Dismissible | DrawerVariant::Modal
576        ) {
577            if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
578                *self.open = false;
579            }
580        }
581
582        let available_rect = ui.available_rect_before_wrap();
583        let drawer_rect = Rect::from_min_size(
584            available_rect.min,
585            Vec2::new(self.width, available_rect.height()),
586        );
587
588        // Draw drawer background with corner radius
589        ui.painter()
590            .rect_filled(drawer_rect, self.corner_radius, background_color);
591
592        // Draw border if present
593        if let Some(stroke) = border_stroke {
594            ui.painter().rect_stroke(
595                drawer_rect,
596                self.corner_radius,
597                stroke,
598                egui::epaint::StrokeKind::Outside,
599            );
600        }
601
602        let mut current_y = drawer_rect.min.y;
603        let item_height = 56.0; // Material 3 standard item height
604        let section_padding_top = 16.0;
605        let section_padding_bottom = 10.0;
606        let horizontal_padding = 12.0; // Container padding
607
608        // Draw header if present
609        if let Some(title) = &self.header_title {
610            let header_height = 64.0;
611            let header_rect = Rect::from_min_size(
612                egui::pos2(drawer_rect.min.x, current_y),
613                Vec2::new(self.width, header_height),
614            );
615
616            // Header text with proper padding
617            let title_pos = egui::pos2(header_rect.min.x + 28.0, header_rect.min.y + 16.0);
618            ui.painter().text(
619                title_pos,
620                egui::Align2::LEFT_TOP,
621                title,
622                egui::FontId::proportional(22.0),
623                get_global_color("onSurfaceVariant"),
624            );
625
626            if let Some(subtitle) = &self.header_subtitle {
627                let subtitle_pos = egui::pos2(header_rect.min.x + 28.0, header_rect.min.y + 42.0);
628                ui.painter().text(
629                    subtitle_pos,
630                    egui::Align2::LEFT_TOP,
631                    subtitle,
632                    egui::FontId::proportional(14.0),
633                    get_global_color("onSurfaceVariant"),
634                );
635            }
636
637            current_y += header_height;
638        }
639
640        let mut response = ui.allocate_response(drawer_rect.size(), Sense::hover());
641
642        // Render sections if any
643        if !self.sections.is_empty() {
644            for (section_idx, section) in self.sections.iter().enumerate() {
645                // Draw section label if present
646                if let Some(label) = &section.label {
647                    current_y += section_padding_top;
648                    let label_pos = egui::pos2(drawer_rect.min.x + 28.0, current_y);
649                    ui.painter().text(
650                        label_pos,
651                        egui::Align2::LEFT_TOP,
652                        label,
653                        egui::FontId::proportional(14.0),
654                        get_global_color("onSurfaceVariant"),
655                    );
656                    current_y += section_padding_bottom + 10.0;
657                }
658
659                // Draw section items
660                for (index, item) in section.items.iter().enumerate() {
661                    let item_response = self.render_navigation_item(
662                        ui,
663                        item,
664                        drawer_rect,
665                        current_y,
666                        item_height,
667                        horizontal_padding,
668                        self.id.with("section").with(section_idx).with(index),
669                    );
670                    response = response.union(item_response);
671                    current_y += item_height;
672                }
673
674                // Add divider between sections (except after last section)
675                if section_idx < self.sections.len() - 1 {
676                    current_y += 8.0;
677                    let divider_y = current_y;
678                    ui.painter().line_segment(
679                        [
680                            egui::pos2(drawer_rect.min.x + 28.0, divider_y),
681                            egui::pos2(drawer_rect.max.x - 28.0, divider_y),
682                        ],
683                        Stroke::new(1.0, get_global_color("outlineVariant")),
684                    );
685                    current_y += 8.0;
686                }
687            }
688        } else {
689            // Render simple items list if no sections
690            for (index, item) in self.items.iter().enumerate() {
691                let item_response = self.render_navigation_item(
692                    ui,
693                    item,
694                    drawer_rect,
695                    current_y,
696                    item_height,
697                    horizontal_padding,
698                    self.id.with("item").with(index),
699                );
700                response = response.union(item_response);
701                current_y += item_height;
702            }
703        }
704
705        response
706    }
707
708    fn render_navigation_item(
709        &self,
710        ui: &mut Ui,
711        item: &DrawerItem,
712        drawer_rect: Rect,
713        y_pos: f32,
714        item_height: f32,
715        horizontal_padding: f32,
716        item_id: Id,
717    ) -> Response {
718        // Item container with padding
719        let item_outer_rect = Rect::from_min_size(
720            egui::pos2(drawer_rect.min.x + horizontal_padding, y_pos),
721            Vec2::new(self.width - horizontal_padding * 2.0, item_height),
722        );
723
724        let item_response = ui.interact(item_outer_rect, item_id, Sense::click());
725
726        // Active indicator (rounded rectangle on the left)
727        if item.active {
728            let indicator_width = item_outer_rect.width();
729            let indicator_height = 32.0;
730            let indicator_y = y_pos + (item_height - indicator_height) / 2.0;
731            
732            let indicator_rect = Rect::from_min_size(
733                egui::pos2(item_outer_rect.min.x, indicator_y),
734                Vec2::new(indicator_width, indicator_height),
735            );
736
737            let active_color = get_global_color("secondaryContainer");
738            ui.painter().rect_filled(
739                indicator_rect,
740                CornerRadius::same(16),
741                active_color,
742            );
743        } else if item_response.hovered() && item.enabled {
744            let indicator_width = item_outer_rect.width();
745            let indicator_height = 32.0;
746            let indicator_y = y_pos + (item_height - indicator_height) / 2.0;
747            
748            let indicator_rect = Rect::from_min_size(
749                egui::pos2(item_outer_rect.min.x, indicator_y),
750                Vec2::new(indicator_width, indicator_height),
751            );
752
753            let hover_color = get_global_color("onSurface").linear_multiply(0.08);
754            ui.painter().rect_filled(
755                indicator_rect,
756                CornerRadius::same(16),
757                hover_color,
758            );
759        }
760
761        let mut current_x = item_outer_rect.min.x + 16.0;
762
763        // Draw icon if present
764        if let Some(_icon) = &item.icon {
765            let icon_center = egui::pos2(current_x + 12.0, y_pos + item_height / 2.0);
766            let icon_color = if !item.enabled {
767                get_global_color("onSurface").linear_multiply(0.38)
768            } else if item.active {
769                get_global_color("onSecondaryContainer")
770            } else {
771                get_global_color("onSurfaceVariant")
772            };
773
774            ui.painter().circle_filled(icon_center, 12.0, icon_color);
775            current_x += 40.0;
776        }
777
778        // Draw item text
779        let text_color = if !item.enabled {
780            get_global_color("onSurface").linear_multiply(0.38)
781        } else if item.active {
782            get_global_color("onSecondaryContainer")
783        } else {
784            get_global_color("onSurfaceVariant")
785        };
786
787        let text_pos = egui::pos2(
788            current_x,
789            y_pos + (item_height - 20.0) / 2.0,
790        );
791        
792        ui.painter().text(
793            text_pos,
794            egui::Align2::LEFT_CENTER,
795            &item.text,
796            egui::FontId::proportional(14.0),
797            text_color,
798        );
799
800        // Draw badge if present
801        if let Some(badge) = &item.badge {
802            let badge_x = item_outer_rect.max.x - 40.0;
803            let badge_center = egui::pos2(badge_x, y_pos + item_height / 2.0);
804            
805            // Badge background
806            ui.painter().circle_filled(
807                badge_center,
808                10.0,
809                get_global_color("error"),
810            );
811            
812            // Badge text
813            ui.painter().text(
814                badge_center,
815                egui::Align2::CENTER_CENTER,
816                badge,
817                egui::FontId::proportional(10.0),
818                get_global_color("onError"),
819            );
820        }
821
822        // Handle item click
823        if item_response.clicked() && item.enabled {
824            if let Some(callback) = &item.on_click {
825                callback();
826            }
827        }
828
829        item_response
830    }
831}
832
833impl Widget for MaterialDrawer<'_> {
834    fn ui(self, ui: &mut Ui) -> Response {
835        // This implementation is kept for backward compatibility
836        // but the preferred way is to use the show() method
837        self.render_drawer_content(ui)
838    }
839}
840
841/// Convenience function to create a permanent drawer.
842pub fn permanent_drawer(open: &mut bool) -> MaterialDrawer<'_> {
843    MaterialDrawer::new(DrawerVariant::Permanent, open)
844}
845
846/// Convenience function to create a dismissible drawer.
847pub fn dismissible_drawer(open: &mut bool) -> MaterialDrawer<'_> {
848    MaterialDrawer::new(DrawerVariant::Dismissible, open)
849}
850
851/// Convenience function to create a modal drawer.
852pub fn modal_drawer(open: &mut bool) -> MaterialDrawer<'_> {
853    MaterialDrawer::new(DrawerVariant::Modal, open)
854}
855
856// Legacy support - these will be deprecated
857pub fn standard_drawer(open: &mut bool) -> MaterialDrawer<'_> {
858    permanent_drawer(open)
859}