widgetry/widgets/
tabs.rs

1use crate::widgets::DEFAULT_CORNER_RADIUS;
2use crate::{ButtonBuilder, EventCtx, Panel, Widget};
3use geom::CornerRadii;
4
5struct Tab {
6    tab_id: String,
7    bar_item: ButtonBuilder<'static, 'static>,
8    content: Widget,
9}
10
11impl Tab {
12    fn new(tab_id: String, bar_item: ButtonBuilder<'static, 'static>, content: Widget) -> Self {
13        Self {
14            tab_id,
15            bar_item,
16            content,
17        }
18    }
19
20    fn build_bar_item_widget(&self, ctx: &EventCtx, active: bool) -> Widget {
21        self.bar_item
22            .clone()
23            .corner_rounding(CornerRadii {
24                top_left: DEFAULT_CORNER_RADIUS,
25                top_right: DEFAULT_CORNER_RADIUS,
26                bottom_left: 0.0,
27                bottom_right: 0.0,
28            })
29            .disabled(active)
30            .build_widget(ctx, &self.tab_id)
31    }
32}
33
34pub struct TabController {
35    id: String,
36    tabs: Vec<Tab>,
37    active_tab_idx: usize,
38}
39
40impl TabController {
41    pub fn new(id: impl Into<String>) -> Self {
42        Self {
43            id: id.into(),
44            tabs: vec![],
45            active_tab_idx: 0,
46        }
47    }
48
49    /// Add a new tab.
50    ///
51    /// `bar_item`: The button shown in the tab bar
52    /// `content`: The content shown when this tab's `bar_item` is clicked
53    pub fn push_tab(&mut self, bar_item: ButtonBuilder<'static, 'static>, content: Widget) {
54        let tab_id = self.tab_id(self.tabs.len() + 1);
55        let tab = Tab::new(tab_id, bar_item, content);
56        self.tabs.push(tab);
57    }
58
59    /// A widget containing the tab bar and a content pane with the currently active tab.
60    // TODO: Clarify that this can only be called once - maybe `TabBuilder.into_tab_controller()`
61    pub fn build_widget(&mut self, ctx: &EventCtx) -> Widget {
62        Widget::custom_col(vec![
63            self.build_bar_items(ctx),
64            self.pop_active_content()
65                .container()
66                .tab_body(ctx)
67                .named(self.active_content_id()),
68        ])
69    }
70
71    pub fn handle_action(&mut self, ctx: &EventCtx, action: &str, panel: &mut Panel) -> bool {
72        if !action.starts_with(&self.id) {
73            return false;
74        }
75
76        let tab_idx = self
77            .tabs
78            .iter()
79            .enumerate()
80            .find(|(_idx, tab)| tab.tab_id == action)
81            .unwrap_or_else(|| panic!("invalid tab id: {}", action))
82            .0;
83        self.activate_tab(ctx, tab_idx, panel);
84        true
85    }
86
87    pub fn active_tab_idx(&self) -> usize {
88        self.active_tab_idx
89    }
90
91    fn active_content_id(&self) -> String {
92        format!("{}_active_content", self.id)
93    }
94
95    fn bar_items_id(&self) -> String {
96        format!("{}_bar_items", self.id)
97    }
98
99    fn tab_id(&self, tab_index: usize) -> String {
100        format!("{}_tab_{}", self.id, tab_index)
101    }
102
103    fn pop_active_content(&mut self) -> Widget {
104        let mut tmp = Widget::nothing();
105        assert!(
106            self.tabs.get(self.active_tab_idx).is_some(),
107            "must add at least one tab before rendering"
108        );
109        std::mem::swap(&mut self.tabs[self.active_tab_idx].content, &mut tmp);
110        tmp
111    }
112
113    fn build_bar_items(&self, ctx: &EventCtx) -> Widget {
114        let bar_items = self
115            .tabs
116            .iter()
117            .enumerate()
118            .map(|(idx, tab)| tab.build_bar_item_widget(ctx, idx == self.active_tab_idx))
119            .collect();
120
121        Widget::row(bar_items)
122            .container()
123            .named(self.bar_items_id())
124    }
125
126    fn activate_tab(&mut self, ctx: &EventCtx, tab_idx: usize, panel: &mut Panel) {
127        let old_idx = self.active_tab_idx;
128        self.active_tab_idx = tab_idx;
129
130        let mut bar_items = self.build_bar_items(ctx);
131        panel.swap_inner_content(ctx, &self.bar_items_id(), &mut bar_items);
132
133        let mut content = self.pop_active_content();
134        panel.swap_inner_content(ctx, &self.active_content_id(), &mut content);
135        self.tabs[old_idx].content = content;
136    }
137}