Skip to main content

guise/data/
tabs.rs

1//! `Tabs` — a tab bar with switchable panels (gpui entity).
2
3use gpui::prelude::*;
4use gpui::{div, px, transparent_black, App, Context, IntoElement, SharedString, Window};
5
6use super::Content;
7use crate::theme::{theme, Size};
8
9struct TabItem {
10    label: SharedString,
11    content: Content,
12}
13
14/// A tabbed view. Create with `cx.new(|cx| Tabs::new(cx).tab("One", |_, _| ...))`.
15pub struct Tabs {
16    tabs: Vec<TabItem>,
17    active: usize,
18}
19
20impl Tabs {
21    pub fn new(_cx: &mut Context<Self>) -> Self {
22        Tabs {
23            tabs: Vec::new(),
24            active: 0,
25        }
26    }
27
28    /// Add a tab. `content` is rebuilt each render so it can show live data.
29    pub fn tab<E>(
30        mut self,
31        label: impl Into<SharedString>,
32        content: impl Fn(&mut Window, &mut App) -> E + 'static,
33    ) -> Self
34    where
35        E: IntoElement,
36    {
37        self.tabs.push(TabItem {
38            label: label.into(),
39            content: Box::new(move |window, cx| content(window, cx).into_any_element()),
40        });
41        self
42    }
43
44    pub fn active(mut self, index: usize) -> Self {
45        self.active = index;
46        self
47    }
48
49    /// The index of the active tab.
50    pub fn active_index(&self) -> usize {
51        self.active
52    }
53}
54
55impl Render for Tabs {
56    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
57        let t = theme(cx);
58        let accent = t.primary().hsla();
59        let dimmed = t.dimmed().hsla();
60        let text = t.text().hsla();
61        let line = t.border().hsla();
62        let font = t.font_size(Size::Sm);
63
64        let count = self.tabs.len();
65        let active = if count == 0 {
66            0
67        } else {
68            self.active.min(count - 1)
69        };
70
71        let mut bar = div().flex().border_b_1().border_color(line);
72        for (i, tab) in self.tabs.iter().enumerate() {
73            let is_active = i == active;
74            bar = bar.child(
75                div()
76                    .id(("guise-tab", i))
77                    .px(px(16.0))
78                    .py(px(8.0))
79                    .border_b_2()
80                    .border_color(if is_active {
81                        accent
82                    } else {
83                        transparent_black()
84                    })
85                    .text_size(px(font))
86                    .text_color(if is_active { accent } else { dimmed })
87                    .hover(move |s| s.text_color(text))
88                    .child(tab.label.clone())
89                    .on_click(cx.listener(move |this, _ev, _window, cx| {
90                        this.active = i;
91                        cx.notify();
92                    })),
93            );
94        }
95
96        let mut root = div().flex().flex_col().gap(px(12.0)).child(bar);
97        if count > 0 {
98            let panel = (self.tabs[active].content)(window, cx);
99            root = root.child(panel);
100        }
101        root
102    }
103}