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