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 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 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}