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
//! Tab bar with a sky-coloured underline on the active tab.
use egui::{vec2, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType};
use crate::theme::Theme;
/// A horizontal tab bar. The active tab is indicated by a sky-coloured
/// underline and an inactive tab lights up on hover.
///
/// ```no_run
/// # use elegance::TabBar;
/// # egui::__run_test_ui(|ui| {
/// let mut selected = 0usize;
/// ui.add(TabBar::new(&mut selected, ["Overview", "Settings", "Activity"]));
/// # });
/// ```
#[must_use = "Add with `ui.add(...)`."]
pub struct TabBar<'a> {
selected: &'a mut usize,
tabs: Vec<WidgetText>,
}
impl<'a> std::fmt::Debug for TabBar<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TabBar")
.field("selected", &*self.selected)
.field(
"tabs",
&self
.tabs
.iter()
.map(|t| t.text().to_string())
.collect::<Vec<_>>(),
)
.finish()
}
}
impl<'a> TabBar<'a> {
/// Build a tab bar. Accepts any iterator of items that convert into
/// [`WidgetText`], so both static arrays and dynamic collections work:
///
/// ```no_run
/// # use elegance::TabBar;
/// # egui::__run_test_ui(|ui| {
/// let mut selected = 0usize;
/// ui.add(TabBar::new(&mut selected, ["Overview", "Settings"]));
/// let dynamic: Vec<String> = vec!["A".into(), "B".into()];
/// ui.add(TabBar::new(&mut selected, dynamic));
/// # });
/// ```
pub fn new<I, S>(selected: &'a mut usize, tabs: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<WidgetText>,
{
Self {
selected,
tabs: tabs.into_iter().map(Into::into).collect(),
}
}
}
impl<'a> Widget for TabBar<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let theme = Theme::current(ui.ctx());
let p = &theme.palette;
let t = &theme.typography;
let response = ui
.horizontal(|ui| {
ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
for (idx, name) in self.tabs.iter().enumerate() {
let is_selected = idx == *self.selected;
let galley = crate::theme::placeholder_galley(
ui,
name.text(),
t.button,
true,
f32::INFINITY,
);
let pad_x = theme.control_padding_x;
// Intentionally taller than `theme.control_padding_y` — tabs
// read as chunkier than standard controls.
let pad_y = 10.0;
let size =
Vec2::new(galley.size().x + 2.0 * pad_x, galley.size().y + 2.0 * pad_y);
let (rect, resp) = ui.allocate_exact_size(size, Sense::click());
if resp.clicked() {
*self.selected = idx;
}
let text_color = if is_selected {
p.sky
} else if resp.hovered() {
p.text
} else {
p.text_faint
};
let text_pos =
egui::pos2(rect.min.x + pad_x, rect.center().y - galley.size().y * 0.5);
ui.painter().galley(text_pos, galley, text_color);
let bottom = rect.bottom();
if is_selected {
let a = egui::pos2(rect.min.x + 4.0, bottom - 1.0);
let b = egui::pos2(rect.max.x - 4.0, bottom - 1.0);
ui.painter().line_segment([a, b], Stroke::new(2.0, p.sky));
}
}
})
.response;
if ui.is_rect_visible(response.rect) {
let bottom = response.rect.bottom();
let a = egui::pos2(response.rect.min.x, bottom - 0.5);
let b = egui::pos2(response.rect.right(), bottom - 0.5);
ui.painter()
.line_segment([a, b], Stroke::new(1.0, p.border));
}
response.widget_info(|| WidgetInfo::labeled(WidgetType::Other, true, "tab bar"));
response
}
}