egui_charts/ui/widget_bar/
tabs.rs1use crate::icons::{Icon, icons as embedded_icons};
6use crate::localization as l10n;
7use crate::styles::{icons, stroke};
8use crate::theme::ThemeExt;
9use crate::theme::{Theme, follow_active_theme, theme_derived};
10use crate::tokens::DESIGN_TOKENS;
11use egui::{Color32, Pos2, Rect, Response, Sense, Stroke, Ui, Vec2};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum RightPanelTab {
16 Alerts,
18 ObjectTree,
20}
21
22impl TryFrom<u8> for RightPanelTab {
23 type Error = ();
24
25 fn try_from(value: u8) -> Result<Self, Self::Error> {
26 match value {
27 0 => Ok(RightPanelTab::Alerts),
28 1 => Ok(RightPanelTab::ObjectTree),
29 _ => Err(()),
30 }
31 }
32}
33
34impl RightPanelTab {
35 pub fn icon(&self) -> &'static Icon {
37 match self {
38 RightPanelTab::Alerts => &embedded_icons::BELL,
39 RightPanelTab::ObjectTree => &embedded_icons::OBJECT_TREE,
40 }
41 }
42
43 pub fn label(&self) -> &'static str {
45 match self {
46 RightPanelTab::Alerts => "Alerts",
47 RightPanelTab::ObjectTree => "Object Tree",
48 }
49 }
50}
51
52#[derive(Clone, Debug)]
54pub struct RightPanelTabsConfig {
55 pub tab_bar_width: f32,
57 pub tab_button_size: f32,
59 pub bg_color: Color32,
61 pub tab_bar_bg: Color32,
63 pub active_tab_bg: Color32,
65 pub hover_bg: Color32,
67 pub pressed_bg: Color32,
69 pub icon_color: Color32,
71 pub active_icon_color: Color32,
73 pub border_color: Color32,
75}
76
77impl RightPanelTabsConfig {
78 pub fn from_theme(theme: &Theme) -> Self {
80 let ui = &theme.ui;
81 Self {
82 tab_bar_width: DESIGN_TOKENS.sizing.toolbar.right_icon_width,
83 tab_button_size: DESIGN_TOKENS.sizing.button_lg,
84 bg_color: ui.panel_bg,
85 tab_bar_bg: ui.panel_bg_secondary,
86 active_tab_bg: ui.panel_bg,
87 hover_bg: ui.btn_bg_hover,
88 pressed_bg: ui.btn_bg_active,
89 icon_color: ui.text_secondary,
90 active_icon_color: ui.accent,
91 border_color: ui.border,
92 }
93 }
94}
95
96impl Default for RightPanelTabsConfig {
97 fn default() -> Self {
98 Self::from_theme(&Theme::dark())
99 }
100}
101
102theme_derived!(RightPanelTabsConfig {
103 tab_bar_width,
104 tab_button_size,
105 bg_color,
106 tab_bar_bg,
107 active_tab_bg,
108 hover_bg,
109 pressed_bg,
110 icon_color,
111 active_icon_color,
112 border_color
113});
114
115#[derive(Clone, Debug, PartialEq)]
117#[non_exhaustive]
118pub enum RightPanelAction {
119 None,
121 TabChanged(RightPanelTab),
123 Close,
125}
126
127pub struct RightPanelTabs {
129 pub active_tab: RightPanelTab,
131 pub config: RightPanelTabsConfig,
133 pub available_tabs: Vec<RightPanelTab>,
135 config_theme: Option<(Theme, RightPanelTabsConfig)>,
137}
138
139impl Default for RightPanelTabs {
140 fn default() -> Self {
141 Self {
142 active_tab: RightPanelTab::Alerts,
143 config: RightPanelTabsConfig::default(),
144 available_tabs: vec![RightPanelTab::Alerts, RightPanelTab::ObjectTree],
145 config_theme: Some((Theme::dark(), RightPanelTabsConfig::default())),
146 }
147 }
148}
149
150impl RightPanelTabs {
151 pub fn new() -> Self {
153 Self::default()
154 }
155
156 pub fn with_config(mut self, config: RightPanelTabsConfig) -> Self {
158 self.config = config;
159 self.config_theme = None;
160 self
161 }
162
163 pub fn with_active_tab(mut self, tab: RightPanelTab) -> Self {
165 self.active_tab = tab;
166 self
167 }
168
169 pub fn select_tab(&mut self, tab: RightPanelTab) {
171 self.active_tab = tab;
172 }
173
174 pub fn show_tab_bar(&mut self, ui: &mut Ui) -> RightPanelAction {
177 follow_active_theme(
178 ui.ctx(),
179 &mut self.config_theme,
180 &mut self.config,
181 RightPanelTabsConfig::from_theme,
182 );
183 let mut action = RightPanelAction::None;
184
185 let available_height = ui.available_height();
186 let (response, painter) = ui.allocate_painter(
187 Vec2::new(self.config.tab_bar_width, available_height),
188 Sense::hover(),
189 );
190 let rect = response.rect;
191
192 painter.rect_filled(rect, 0.0, self.config.tab_bar_bg);
194
195 painter.line_segment(
197 [rect.left_top(), rect.left_bottom()],
198 Stroke::new(stroke::HAIRLINE, self.config.border_color),
199 );
200
201 let mut y = rect.min.y + ui.active_theme().spacing.lg;
202
203 for tab in &self.available_tabs {
205 let is_active = *tab == self.active_tab;
206 let btn_rect = Rect::from_min_size(
207 Pos2::new(rect.min.x + ui.active_theme().spacing.sm, y),
208 Vec2::new(self.config.tab_button_size, self.config.tab_button_size),
209 );
210
211 let btn_res = ui.allocate_rect(btn_rect, Sense::click());
213 let translated_label = l10n::text(ui.ctx(), tab.label());
214 btn_res.widget_info(|| {
215 egui::WidgetInfo::labeled(egui::Role::Tab, btn_res.enabled(), &translated_label)
216 });
217 ui.ctx()
218 .accesskit_node_builder(btn_res.id, |node| node.set_selected(is_active));
219 crate::styles::focus::draw_focus_ring(ui, &btn_res, ui.active_theme().radius.md);
220 let is_hovered = btn_res.hovered();
221 let is_pressed = btn_res.is_pointer_button_down_on();
222
223 if is_active {
225 painter.rect_filled(
226 btn_rect,
227 ui.active_theme().radius.md,
228 self.config.active_tab_bg,
229 );
230 painter.rect_filled(
232 Rect::from_min_size(
233 Pos2::new(rect.min.x, btn_rect.min.y),
234 Vec2::new(
235 DESIGN_TOKENS.sizing.settings_dialog.indicator_width,
236 self.config.tab_button_size,
237 ),
238 ),
239 DESIGN_TOKENS.rounding.none,
240 self.config.active_icon_color,
241 );
242 } else if is_pressed {
243 painter.rect_filled(
244 btn_rect,
245 ui.active_theme().radius.md,
246 self.config.pressed_bg,
247 );
248 } else if is_hovered {
249 painter.rect_filled(btn_rect, ui.active_theme().radius.md, self.config.hover_bg);
250 }
251
252 let icon_size = icons::SM;
254 let icon_rect = Rect::from_center_size(btn_rect.center(), Vec2::splat(icon_size));
255
256 let icon_color = if is_active || is_hovered {
258 ui.visuals().widgets.hovered.fg_stroke.color
259 } else {
260 ui.visuals().widgets.noninteractive.fg_stroke.color
261 };
262
263 tab.icon()
264 .as_image_tinted(Vec2::splat(icon_size), icon_color)
265 .paint_at(ui, icon_rect);
266
267 if btn_res.clicked() {
269 action = RightPanelAction::TabChanged(*tab);
270 }
271
272 if is_hovered {
274 btn_res.on_hover_text(l10n::text(ui.ctx(), tab.label()));
275 }
276
277 y += self.config.tab_button_size + ui.active_theme().spacing.sm;
278 }
279
280 action
281 }
282
283 pub fn show_content_header(&self, ui: &mut Ui) -> Response {
285 let (response, painter) = ui.allocate_painter(
286 Vec2::new(
287 ui.available_width(),
288 DESIGN_TOKENS.sizing.context_menu.item_height,
289 ),
290 Sense::hover(),
291 );
292 let rect = response.rect;
293
294 painter.rect_filled(rect, DESIGN_TOKENS.rounding.none, self.config.bg_color);
296
297 painter.line_segment(
299 [rect.left_bottom(), rect.right_bottom()],
300 Stroke::new(stroke::HAIRLINE, self.config.border_color),
301 );
302
303 let title_galley = painter.layout_no_wrap(
305 l10n::text(ui.ctx(), self.active_tab.label()),
306 egui::FontId::proportional(ui.active_theme().text.small),
307 ui.visuals().text_color(),
308 );
309 painter.galley(
310 Pos2::new(
311 rect.min.x + ui.active_theme().spacing.xl,
312 rect.center().y - title_galley.size().y / 2.0,
313 ),
314 title_galley,
315 Color32::TRANSPARENT,
316 );
317
318 response
319 }
320
321 pub fn set_active_tab(&mut self, tab: RightPanelTab) {
323 self.active_tab = tab;
324 }
325
326 pub fn active_tab(&self) -> RightPanelTab {
328 self.active_tab
329 }
330}