1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
use crate::get_global_color;
use egui::{
Align, Color32, Layout, Response, Rounding, Shadow, Stroke, Ui, Vec2, Widget,
};
/// Material Design toolbar component.
///
/// A fixed area at the bottom (or top) of a screen that contains navigation elements.
/// The toolbar serves as a container for navigational links, buttons, and icon buttons.
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::{toolbar, MaterialButton, icon_button_standard};
///
/// ui.add(toolbar()
/// .item(MaterialButton::text("Home"))
/// .item_fn(|ui| ui.add(icon_button_standard("search")))
/// .item(MaterialButton::text("Settings")));
/// # });
/// ```
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct MaterialToolbar<'a> {
/// Items to display in the toolbar (buttons, icon buttons, etc.)
items: Vec<ToolbarItem<'a>>,
/// Position the toolbar at the top of the screen
top: bool,
/// Enable tabbar mode with equal-width items
tabbar: bool,
/// Show icons in tabbar mode
tabbar_icons: bool,
/// Show labels in tabbar mode
tabbar_labels: bool,
/// Show outline/border
outline: bool,
/// Custom background color
bg_color: Option<Color32>,
/// Minimum height of the toolbar
min_height: f32,
/// Spacing between items
item_spacing: f32,
/// Inner padding
padding: Vec2,
}
/// Represents an item in the toolbar
enum ToolbarItem<'a> {
/// A regular widget (button, icon button, etc.)
Widget(Box<dyn FnOnce(&mut Ui) -> Response + 'a>),
/// A spacer to push items apart
Spacer,
}
impl<'a> Default for MaterialToolbar<'a> {
fn default() -> Self {
Self {
items: Vec::new(),
top: false,
tabbar: false,
tabbar_icons: false,
tabbar_labels: false,
outline: true,
bg_color: None,
min_height: 56.0, // Material Design standard toolbar height
item_spacing: 8.0,
padding: Vec2::new(16.0, 8.0),
}
}
}
impl<'a> MaterialToolbar<'a> {
/// Create a new toolbar
pub fn new() -> Self {
Self::default()
}
/// Add a widget item to the toolbar
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::{toolbar, MaterialButton};
///
/// ui.add(toolbar()
/// .item(MaterialButton::text("Home"))
/// .item(MaterialButton::text("Profile")));
/// # });
/// ```
pub fn item<W>(mut self, widget: W) -> Self
where
W: Widget + 'a,
{
self.items.push(ToolbarItem::Widget(Box::new(move |ui| {
ui.add(widget)
})));
self
}
/// Add a custom closure as an item
///
/// Use this for icon buttons and other widgets that need special handling.
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::{toolbar, icon_button_standard};
///
/// ui.add(toolbar()
/// .item_fn(|ui| ui.add(icon_button_standard("home")))
/// .item_fn(|ui| ui.add(icon_button_standard("search"))));
/// # });
/// ```
pub fn item_fn<F>(mut self, f: F) -> Self
where
F: FnOnce(&mut Ui) -> Response + 'a,
{
self.items.push(ToolbarItem::Widget(Box::new(f)));
self
}
/// Add a spacer to push subsequent items to the right
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::{toolbar, MaterialButton};
///
/// ui.add(toolbar()
/// .item(MaterialButton::text("Left"))
/// .spacer()
/// .item(MaterialButton::text("Right")));
/// # });
/// ```
pub fn spacer(mut self) -> Self {
self.items.push(ToolbarItem::Spacer);
self
}
/// Position the toolbar at the top of the screen
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::toolbar;
///
/// ui.add(toolbar()
/// .top(true));
/// # });
/// ```
pub fn top(mut self, top: bool) -> Self {
self.top = top;
self
}
/// Enable tabbar mode with equal-width items
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::toolbar;
///
/// ui.add(toolbar()
/// .tabbar(true));
/// # });
/// ```
pub fn tabbar(mut self, tabbar: bool) -> Self {
self.tabbar = tabbar;
self
}
/// Show icons in tabbar mode
pub fn tabbar_icons(mut self, show: bool) -> Self {
self.tabbar_icons = show;
self
}
/// Show labels in tabbar mode
pub fn tabbar_labels(mut self, show: bool) -> Self {
self.tabbar_labels = show;
self
}
/// Show outline/border
pub fn outline(mut self, outline: bool) -> Self {
self.outline = outline;
self
}
/// Set custom background color
pub fn bg_color(mut self, color: Color32) -> Self {
self.bg_color = Some(color);
self
}
/// Set minimum height of the toolbar
pub fn min_height(mut self, height: f32) -> Self {
self.min_height = height;
self
}
/// Set spacing between items
pub fn item_spacing(mut self, spacing: f32) -> Self {
self.item_spacing = spacing;
self
}
/// Set inner padding
pub fn padding(mut self, padding: Vec2) -> Self {
self.padding = padding;
self
}
}
impl<'a> Widget for MaterialToolbar<'a> {
fn ui(self, ui: &mut Ui) -> Response {
// Material Design colors for toolbar/bottom navigation
let surface = get_global_color("surface");
let surface_container = get_global_color("surfaceContainer");
let outline_variant = get_global_color("outlineVariant");
let surface_tint = get_global_color("surfaceTint");
// Determine background color based on position and state
let bg_color = self.bg_color.unwrap_or_else(|| {
if self.top {
// Top toolbar uses surface with elevation
surface
} else {
// Bottom navigation uses surface container
surface_container
}
});
// Calculate available width
let available_width = ui.available_width();
// Allocate space for the toolbar
let (rect, response) = ui.allocate_exact_size(
Vec2::new(available_width, self.min_height),
egui::Sense::hover(),
);
if ui.is_rect_visible(rect) {
// Draw elevation shadow for top toolbar (behind background)
if self.top {
// Material Design 3 elevation level 2
let shadow = Shadow {
offset: [0, 2],
blur: 6,
spread: 0,
color: Color32::from_black_alpha(16),
};
let shadow_offset = Vec2::new(shadow.offset[0] as f32, shadow.offset[1] as f32);
let shadow_rect = rect.translate(shadow_offset);
// Draw shadow with blur simulation
for i in 0..3 {
let blur_offset = i as f32 * 1.5;
let alpha = (16 / (i + 1)) as u8;
ui.painter().rect_filled(
shadow_rect.expand(blur_offset),
Rounding::ZERO,
Color32::from_black_alpha(alpha),
);
}
}
// Draw background with surface tint for elevation
if self.top {
// Top toolbar with elevation tint
ui.painter().rect_filled(
rect,
Rounding::ZERO,
bg_color,
);
// Apply surface tint overlay for elevated appearance
let tint_overlay = Color32::from_rgba_unmultiplied(
surface_tint.r(),
surface_tint.g(),
surface_tint.b(),
8, // 3% opacity for elevation level 2
);
ui.painter().rect_filled(
rect,
Rounding::ZERO,
tint_overlay,
);
} else {
// Bottom navigation - flat surface
ui.painter().rect_filled(
rect,
Rounding::ZERO,
bg_color,
);
}
// Draw outline/border with proper Material Design colors
if self.outline {
let border_color = if self.top {
outline_variant // Subtle for top
} else {
outline_variant // Subtle for bottom
};
let border_y = if self.top {
rect.max.y
} else {
rect.min.y
};
ui.painter().line_segment(
[
egui::pos2(rect.min.x, border_y),
egui::pos2(rect.max.x, border_y),
],
Stroke::new(1.0, border_color),
);
}
// Create a child UI for content
let mut child_ui = ui.child_ui(
rect.shrink2(self.padding),
Layout::left_to_right(Align::Center),
None,
);
child_ui.spacing_mut().item_spacing.x = self.item_spacing;
if self.tabbar {
// Tabbar mode: equal-width items
let item_count = self.items.len();
if item_count > 0 {
let item_width = (rect.width() - self.padding.x * 2.0) / item_count as f32;
for item in self.items {
match item {
ToolbarItem::Widget(widget_fn) => {
child_ui.allocate_ui_with_layout(
Vec2::new(item_width, rect.height() - self.padding.y * 2.0),
Layout::centered_and_justified(egui::Direction::TopDown),
|ui| {
widget_fn(ui);
},
);
}
ToolbarItem::Spacer => {
// Spacers don't make sense in tabbar mode
}
}
}
}
} else {
// Normal mode: flexible layout
for item in self.items {
match item {
ToolbarItem::Widget(widget_fn) => {
widget_fn(&mut child_ui);
}
ToolbarItem::Spacer => {
child_ui.with_layout(
Layout::right_to_left(Align::Center),
|_ui| {},
);
}
}
}
}
}
response
}
}
/// Convenience function to create a toolbar
///
/// # Example
/// ```rust
/// # egui::__run_test_ui(|ui| {
/// use egui_material3::{toolbar, MaterialButton, icon_button_standard};
///
/// ui.add(toolbar()
/// .item(MaterialButton::text("Home"))
/// .item_fn(|ui| ui.add(icon_button_standard("settings"))));
/// # });
/// ```
pub fn toolbar<'a>() -> MaterialToolbar<'a> {
MaterialToolbar::new()
}