Skip to main content

elegance/
tabs.rs

1//! Tab bar with a sky-coloured underline on the active tab.
2
3use egui::{vec2, Response, Sense, Stroke, Ui, Vec2, Widget, WidgetInfo, WidgetText, WidgetType};
4
5use crate::theme::Theme;
6
7/// A horizontal tab bar. The active tab is indicated by a sky-coloured
8/// underline and an inactive tab lights up on hover.
9///
10/// ```no_run
11/// # use elegance::TabBar;
12/// # egui::__run_test_ui(|ui| {
13/// let mut selected = 0usize;
14/// ui.add(TabBar::new(&mut selected, ["Overview", "Settings", "Activity"]));
15/// # });
16/// ```
17#[must_use = "Add with `ui.add(...)`."]
18pub struct TabBar<'a> {
19    selected: &'a mut usize,
20    tabs: Vec<WidgetText>,
21}
22
23impl<'a> std::fmt::Debug for TabBar<'a> {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        f.debug_struct("TabBar")
26            .field("selected", &*self.selected)
27            .field(
28                "tabs",
29                &self
30                    .tabs
31                    .iter()
32                    .map(|t| t.text().to_string())
33                    .collect::<Vec<_>>(),
34            )
35            .finish()
36    }
37}
38
39impl<'a> TabBar<'a> {
40    /// Build a tab bar. Accepts any iterator of items that convert into
41    /// [`WidgetText`], so both static arrays and dynamic collections work:
42    ///
43    /// ```no_run
44    /// # use elegance::TabBar;
45    /// # egui::__run_test_ui(|ui| {
46    /// let mut selected = 0usize;
47    /// ui.add(TabBar::new(&mut selected, ["Overview", "Settings"]));
48    /// let dynamic: Vec<String> = vec!["A".into(), "B".into()];
49    /// ui.add(TabBar::new(&mut selected, dynamic));
50    /// # });
51    /// ```
52    pub fn new<I, S>(selected: &'a mut usize, tabs: I) -> Self
53    where
54        I: IntoIterator<Item = S>,
55        S: Into<WidgetText>,
56    {
57        Self {
58            selected,
59            tabs: tabs.into_iter().map(Into::into).collect(),
60        }
61    }
62}
63
64impl<'a> Widget for TabBar<'a> {
65    fn ui(self, ui: &mut Ui) -> Response {
66        let theme = Theme::current(ui.ctx());
67        let p = &theme.palette;
68        let t = &theme.typography;
69
70        let response = ui
71            .horizontal(|ui| {
72                ui.spacing_mut().item_spacing = vec2(0.0, 0.0);
73                for (idx, name) in self.tabs.iter().enumerate() {
74                    let is_selected = idx == *self.selected;
75                    let galley = crate::theme::placeholder_galley(
76                        ui,
77                        name.text(),
78                        t.button,
79                        true,
80                        f32::INFINITY,
81                    );
82
83                    let pad_x = theme.control_padding_x;
84                    // Intentionally taller than `theme.control_padding_y` — tabs
85                    // read as chunkier than standard controls.
86                    let pad_y = 10.0;
87                    let size =
88                        Vec2::new(galley.size().x + 2.0 * pad_x, galley.size().y + 2.0 * pad_y);
89                    let (rect, resp) = ui.allocate_exact_size(size, Sense::click());
90
91                    if resp.clicked() {
92                        *self.selected = idx;
93                    }
94
95                    let text_color = if is_selected {
96                        p.sky
97                    } else if resp.hovered() {
98                        p.text
99                    } else {
100                        p.text_faint
101                    };
102                    let text_pos =
103                        egui::pos2(rect.min.x + pad_x, rect.center().y - galley.size().y * 0.5);
104                    ui.painter().galley(text_pos, galley, text_color);
105
106                    let bottom = rect.bottom();
107                    if is_selected {
108                        let a = egui::pos2(rect.min.x + 4.0, bottom - 1.0);
109                        let b = egui::pos2(rect.max.x - 4.0, bottom - 1.0);
110                        ui.painter().line_segment([a, b], Stroke::new(2.0, p.sky));
111                    }
112                }
113            })
114            .response;
115
116        if ui.is_rect_visible(response.rect) {
117            let bottom = response.rect.bottom();
118            let a = egui::pos2(response.rect.min.x, bottom - 0.5);
119            let b = egui::pos2(response.rect.right(), bottom - 0.5);
120            ui.painter()
121                .line_segment([a, b], Stroke::new(1.0, p.border));
122        }
123
124        response.widget_info(|| WidgetInfo::labeled(WidgetType::Other, true, "tab bar"));
125        response
126    }
127}