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}