egui_material3/
tabs.rs

1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
2use crate::get_global_color;
3
4/// Material Design tabs component.
5///
6/// Tabs organize content across different screens, data sets, and other interactions.
7/// They allow users to navigate between related groups of content.
8///
9/// # Example
10/// ```rust
11/// # egui::__run_test_ui(|ui| {
12/// let mut selected_tab = 0;
13/// 
14/// ui.add(MaterialTabs::primary(&mut selected_tab)
15///     .tab("Home")
16///     .tab("Profile")
17///     .tab("Settings"));
18/// # });
19/// ```
20#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
21pub struct MaterialTabs<'a> {
22    /// Reference to the currently selected tab index
23    selected: &'a mut usize,
24    /// List of tab items
25    tabs: Vec<TabItem>,
26    /// Whether the tabs are enabled for interaction
27    enabled: bool,
28    /// Visual variant of the tabs (primary or secondary)
29    variant: TabVariant,
30    /// Optional salt for generating unique IDs
31    id_salt: Option<String>,
32}
33
34/// Individual tab item data.
35pub struct TabItem {
36    /// Display label for the tab
37    label: String,
38    /// Optional icon for the tab
39    icon: Option<String>,
40}
41
42/// Visual variants for tabs component.
43#[derive(Clone, Copy, PartialEq)]
44pub enum TabVariant {
45    /// Primary tabs (filled background, more prominent)
46    Primary,
47    /// Secondary tabs (outlined style, less prominent)
48    Secondary,
49}
50
51impl<'a> MaterialTabs<'a> {
52    /// Create a new tabs component.
53    ///
54    /// # Arguments
55    /// * `selected` - Mutable reference to the currently selected tab index
56    /// * `variant` - Visual variant (Primary or Secondary)
57    ///
58    /// # Example
59    /// ```rust
60    /// # egui::__run_test_ui(|ui| {
61    /// let mut tab_index = 0;
62    /// let tabs = MaterialTabs::new(&mut tab_index, TabVariant::Primary);
63    /// # });
64    /// ```
65    pub fn new(selected: &'a mut usize, variant: TabVariant) -> Self {
66        Self {
67            selected,
68            tabs: Vec::new(),
69            enabled: true,
70            variant,
71            id_salt: None,
72        }
73    }
74
75    /// Create a primary tabs component.
76    ///
77    /// Primary tabs have a filled background and are more prominent.
78    ///
79    /// # Arguments
80    /// * `selected` - Mutable reference to the currently selected tab index
81    ///
82    /// # Example
83    /// ```rust
84    /// # egui::__run_test_ui(|ui| {
85    /// let mut tab_index = 0;
86    /// let tabs = MaterialTabs::primary(&mut tab_index);
87    /// # });
88    /// ```
89    pub fn primary(selected: &'a mut usize) -> Self {
90        Self::new(selected, TabVariant::Primary)
91    }
92
93    /// Create a secondary tabs component.
94    ///
95    /// Secondary tabs have an outlined style and are less prominent than primary tabs.
96    ///
97    /// # Arguments
98    /// * `selected` - Mutable reference to the currently selected tab index
99    ///
100    /// # Example
101    /// ```rust
102    /// # egui::__run_test_ui(|ui| {
103    /// let mut tab_index = 0;
104    /// let tabs = MaterialTabs::secondary(&mut tab_index);
105    /// # });
106    /// ```
107    pub fn secondary(selected: &'a mut usize) -> Self {
108        Self::new(selected, TabVariant::Secondary)
109    }
110
111    /// Add a tab with just a text label.
112    ///
113    /// # Arguments
114    /// * `label` - Text label for the tab
115    ///
116    /// # Example
117    /// ```rust
118    /// # egui::__run_test_ui(|ui| {
119    /// let mut tab_index = 0;
120    /// ui.add(MaterialTabs::primary(&mut tab_index)
121    ///     .tab("Home")
122    ///     .tab("About"));
123    /// # });
124    /// ```
125    pub fn tab(mut self, label: impl Into<String>) -> Self {
126        self.tabs.push(TabItem {
127            label: label.into(),
128            icon: None,
129        });
130        self
131    }
132
133    /// Add a tab with both an icon and text label.
134    ///
135    /// # Arguments
136    /// * `label` - Text label for the tab
137    /// * `icon` - Icon identifier (e.g., "home", "person", "settings")
138    ///
139    /// # Example
140    /// ```rust
141    /// # egui::__run_test_ui(|ui| {
142    /// let mut tab_index = 0;
143    /// ui.add(MaterialTabs::primary(&mut tab_index)
144    ///     .tab_with_icon("Home", "home")
145    ///     .tab_with_icon("Profile", "person"));
146    /// # });
147    /// ```
148    pub fn tab_with_icon(mut self, label: impl Into<String>, icon: impl Into<String>) -> Self {
149        self.tabs.push(TabItem {
150            label: label.into(),
151            icon: Some(icon.into()),
152        });
153        self
154    }
155
156    /// Set whether the tabs are enabled for interaction.
157    ///
158    /// # Arguments
159    /// * `enabled` - `true` to enable tabs, `false` to disable
160    ///
161    /// # Example
162    /// ```rust
163    /// # egui::__run_test_ui(|ui| {
164    /// let mut tab_index = 0;
165    /// ui.add(MaterialTabs::primary(&mut tab_index)
166    ///     .tab("Home")
167    ///     .tab("Profile")
168    ///     .tab("Settings")
169    ///     .enabled(false));
170    /// # });
171    /// ```
172    pub fn enabled(mut self, enabled: bool) -> Self {
173        self.enabled = enabled;
174        self
175    }
176
177    /// Set an optional salt for generating unique IDs for the tabs.
178    ///
179    /// This is useful if you have multiple instances of tabs and want to avoid ID collisions.
180    ///
181    /// # Arguments
182    /// * `salt` - Salt string to use in ID generation
183    ///
184    /// # Example
185    /// ```rust
186    /// # egui::__run_test_ui(|ui| {
187    /// let mut tab_index = 0;
188    /// ui.add(MaterialTabs::primary(&mut tab_index)
189    ///     .tab("Home")
190    ///     .tab("Profile")
191    ///     .tab("Settings")
192    ///     .id_salt("unique_salt"));
193    /// # });
194    /// ```
195    pub fn id_salt(mut self, salt: impl Into<String>) -> Self {
196        self.id_salt = Some(salt.into());
197        self
198    }
199}
200
201impl<'a> Widget for MaterialTabs<'a> {
202    fn ui(self, ui: &mut Ui) -> Response {
203        let tab_height = 48.0;
204        let tab_width = ui.available_width() / self.tabs.len().max(1) as f32;
205        
206        let desired_size = Vec2::new(ui.available_width(), tab_height);
207        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::hover());
208
209        // Material Design colors
210        let primary_color = get_global_color("primary");
211        let surface = get_global_color("surface");
212        let on_surface = get_global_color("onSurface");
213        let on_surface_variant = get_global_color("onSurfaceVariant");
214        let outline_variant = get_global_color("outlineVariant");
215
216        // Draw tab bar background
217        ui.painter().rect_filled(
218            rect,
219            0.0,
220            surface,
221        );
222
223        // Draw tabs
224        let mut any_clicked = false;
225        for (index, tab) in self.tabs.iter().enumerate() {
226            let tab_rect = Rect::from_min_size(
227                Pos2::new(rect.min.x + index as f32 * tab_width, rect.min.y),
228                Vec2::new(tab_width, tab_height),
229            );
230
231            // Create unique ID for each tab using optional salt
232            let tab_id = if let Some(ref salt) = self.id_salt {
233                egui::Id::new((salt, "tab", index))
234            } else {
235                egui::Id::new(("tab", index))
236            };
237            
238            let tab_response = ui.interact(
239                tab_rect,
240                tab_id,
241                Sense::click(),
242            );
243
244            let is_selected = *self.selected == index;
245            let is_hovered = tab_response.hovered();
246
247            // Determine colors
248            let (text_color, indicator_color) = match self.variant {
249                TabVariant::Primary => {
250                    if is_selected {
251                        (primary_color, primary_color)
252                    } else if is_hovered && self.enabled {
253                        (on_surface, on_surface_variant)
254                    } else {
255                        (on_surface_variant, Color32::TRANSPARENT)
256                    }
257                }
258                TabVariant::Secondary => {
259                    if is_selected {
260                        (on_surface, outline_variant)
261                    } else if is_hovered && self.enabled {
262                        (on_surface, Color32::TRANSPARENT)
263                    } else {
264                        (on_surface_variant, Color32::TRANSPARENT)
265                    }
266                }
267            };
268
269            // Draw hover background
270            if is_hovered && self.enabled {
271                let hover_color = Color32::from_rgba_premultiplied(
272                    text_color.r(), text_color.g(), text_color.b(), 20
273                );
274                ui.painter().rect_filled(
275                    tab_rect,
276                    0.0,
277                    hover_color,
278                );
279            }
280
281            // Handle click
282            if tab_response.clicked() && self.enabled {
283                *self.selected = index;
284                any_clicked = true;
285            }
286
287            // Layout tab content
288            let mut content_y = tab_rect.center().y;
289            
290            // Draw icon if present
291            if let Some(_icon) = &tab.icon {
292                let icon_rect = Rect::from_min_size(
293                    Pos2::new(tab_rect.center().x - 12.0, content_y - 20.0),
294                    Vec2::splat(24.0),
295                );
296                
297                ui.painter().circle_filled(icon_rect.center(), 8.0, text_color);
298                content_y += 12.0;
299            }
300
301            // Draw tab text
302            let text_pos = Pos2::new(tab_rect.center().x, content_y);
303            let font_size = if tab.icon.is_some() {
304                egui::FontId::proportional(12.0)
305            } else {
306                egui::FontId::default()
307            };
308
309            ui.painter().text(
310                text_pos,
311                egui::Align2::CENTER_CENTER,
312                &tab.label,
313                font_size,
314                text_color,
315            );
316
317            // Draw indicator
318            match self.variant {
319                TabVariant::Primary => {
320                    if is_selected && indicator_color != Color32::TRANSPARENT {
321                        let indicator_rect = Rect::from_min_size(
322                            Pos2::new(tab_rect.min.x + 8.0, tab_rect.max.y - 3.0),
323                            Vec2::new(tab_width - 16.0, 3.0),
324                        );
325                        ui.painter().rect_filled(
326                            indicator_rect,
327                            1.5,
328                            indicator_color,
329                        );
330                    }
331                }
332                TabVariant::Secondary => {
333                    if is_selected && indicator_color != Color32::TRANSPARENT {
334                        ui.painter().rect_stroke(
335                            tab_rect,
336                            0.0,
337                            Stroke::new(1.0, indicator_color),
338                            egui::epaint::StrokeKind::Outside,
339                        );
340                    }
341                }
342            }
343        }
344
345        // Draw bottom border for secondary variant
346        if self.variant == TabVariant::Secondary {
347            let border_rect = Rect::from_min_size(
348                Pos2::new(rect.min.x, rect.max.y - 1.0),
349                Vec2::new(rect.width(), 1.0),
350            );
351            ui.painter().rect_filled(
352                border_rect,
353                0.0,
354                outline_variant,
355            );
356        }
357
358        if any_clicked {
359            response.mark_changed();
360        }
361        response
362    }
363}
364
365/// Convenience function to create primary tabs.
366///
367/// Shorthand for `MaterialTabs::primary()`.
368///
369/// # Arguments
370/// * `selected` - Mutable reference to the currently selected tab index
371///
372/// # Example
373/// ```rust
374/// # egui::__run_test_ui(|ui| {
375/// let mut tab_index = 0;
376/// ui.add(tabs_primary(&mut tab_index)
377///     .tab("Tab 1")
378///     .tab("Tab 2"));
379/// # });
380/// ```
381pub fn tabs_primary<'a>(selected: &'a mut usize) -> MaterialTabs<'a> {
382    MaterialTabs::primary(selected)
383}
384
385/// Convenience function to create secondary tabs.
386///
387/// Shorthand for `MaterialTabs::secondary()`.
388///
389/// # Arguments
390/// * `selected` - Mutable reference to the currently selected tab index
391///
392/// # Example
393/// ```rust
394/// # egui::__run_test_ui(|ui| {
395/// let mut tab_index = 0;
396/// ui.add(tabs_secondary(&mut tab_index)
397///     .tab("Tab 1")
398///     .tab("Tab 2"));
399/// # });
400/// ```
401pub fn tabs_secondary<'a>(selected: &'a mut usize) -> MaterialTabs<'a> {
402    MaterialTabs::secondary(selected)
403}