1#![windows_subsystem = "windows"]
19
20use druid::im::Vector;
21use druid::widget::{
22 Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, RadioGroup, Split, TabInfo,
23 Tabs, TabsEdge, TabsPolicy, TabsTransition, TextBox, ViewSwitcher,
24};
25use druid::{theme, AppLauncher, Color, Data, Env, Lens, Widget, WidgetExt, WindowDesc};
26use instant::Duration;
27
28#[derive(Data, Clone, Lens)]
29struct DynamicTabData {
30 highest_tab: usize,
31 removed_tabs: usize,
32 tab_labels: Vector<usize>,
33}
34
35impl DynamicTabData {
36 fn new(highest_tab: usize) -> Self {
37 DynamicTabData {
38 highest_tab,
39 removed_tabs: 0,
40 tab_labels: (1..=highest_tab).collect(),
41 }
42 }
43
44 fn add_tab(&mut self) {
45 self.highest_tab += 1;
46 self.tab_labels.push_back(self.highest_tab);
47 }
48
49 fn remove_tab(&mut self, idx: usize) {
50 if idx >= self.tab_labels.len() {
51 tracing::warn!("Attempt to remove non existent tab at index {}", idx)
52 } else {
53 self.removed_tabs += 1;
54 self.tab_labels.remove(idx);
55 }
56 }
57
58 fn tabs_key(&self) -> (usize, usize) {
60 (self.highest_tab, self.removed_tabs)
61 }
62}
63
64#[derive(Data, Clone, Lens)]
65struct TabConfig {
66 axis: Axis,
67 edge: TabsEdge,
68 transition: TabsTransition,
69}
70
71#[derive(Data, Clone, Lens)]
72struct AppState {
73 tab_config: TabConfig,
74 advanced: DynamicTabData,
75 first_tab_name: String,
76}
77
78pub fn main() {
79 let main_window = WindowDesc::new(build_root_widget())
81 .title("Tabs")
82 .window_size((700.0, 400.0));
83
84 let initial_state = AppState {
86 tab_config: TabConfig {
87 axis: Axis::Horizontal,
88 edge: TabsEdge::Leading,
89 transition: Default::default(),
90 },
91 first_tab_name: "First tab".into(),
92 advanced: DynamicTabData::new(2),
93 };
94
95 AppLauncher::with_window(main_window)
97 .log_to_console()
98 .launch(initial_state)
99 .expect("Failed to launch application");
100}
101
102fn build_root_widget() -> impl Widget<AppState> {
103 fn group<T: Data, W: Widget<T> + 'static>(text: &str, w: W) -> impl Widget<T> {
104 Flex::column()
105 .cross_axis_alignment(CrossAxisAlignment::Start)
106 .with_child(
107 Label::new(text)
108 .background(theme::PLACEHOLDER_COLOR)
109 .expand_width(),
110 )
111 .with_default_spacer()
112 .with_child(w)
113 .with_default_spacer()
114 .border(Color::WHITE, 0.5)
115 }
116
117 let axis_picker = group(
118 "Tab bar axis",
119 RadioGroup::column(vec![
120 ("Horizontal", Axis::Horizontal),
121 ("Vertical", Axis::Vertical),
122 ])
123 .lens(TabConfig::axis),
124 );
125
126 let cross_picker = group(
127 "Tab bar edge",
128 RadioGroup::column(vec![
129 ("Leading", TabsEdge::Leading),
130 ("Trailing", TabsEdge::Trailing),
131 ])
132 .lens(TabConfig::edge),
133 );
134
135 let transit_picker = group(
136 "Transition",
137 RadioGroup::column(vec![
138 ("Instant", TabsTransition::Instant),
139 (
140 "Slide",
141 TabsTransition::Slide(Duration::from_millis(250).as_nanos() as u64),
142 ),
143 ])
144 .lens(TabConfig::transition),
145 );
146
147 let sidebar = Flex::column()
148 .main_axis_alignment(MainAxisAlignment::Start)
149 .cross_axis_alignment(CrossAxisAlignment::Start)
150 .with_child(axis_picker)
151 .with_default_spacer()
152 .with_child(cross_picker)
153 .with_default_spacer()
154 .with_child(transit_picker)
155 .with_flex_spacer(1.)
156 .fix_width(200.0)
157 .lens(AppState::tab_config);
158
159 let vs = ViewSwitcher::new(
160 |app_s: &AppState, _| app_s.tab_config.clone(),
161 |tc: &TabConfig, _, _| Box::new(build_tab_widget(tc)),
162 );
163 Flex::row().with_child(sidebar).with_flex_child(vs, 1.0)
164}
165
166#[derive(Clone, Data)]
167struct NumberedTabs;
168
169impl TabsPolicy for NumberedTabs {
170 type Key = usize;
171 type Build = ();
172 type Input = DynamicTabData;
173 type LabelWidget = Label<DynamicTabData>;
174 type BodyWidget = Label<DynamicTabData>;
175
176 fn tabs_changed(&self, old_data: &DynamicTabData, data: &DynamicTabData) -> bool {
177 old_data.tabs_key() != data.tabs_key()
178 }
179
180 fn tabs(&self, data: &DynamicTabData) -> Vec<Self::Key> {
181 data.tab_labels.iter().copied().collect()
182 }
183
184 fn tab_info(&self, key: Self::Key, _data: &DynamicTabData) -> TabInfo<DynamicTabData> {
185 TabInfo::new(format!("Tab {key:?}"), true)
186 }
187
188 fn tab_body(&self, key: Self::Key, _data: &DynamicTabData) -> Label<DynamicTabData> {
189 Label::new(format!("Dynamic tab body {key:?}"))
190 }
191
192 fn close_tab(&self, key: Self::Key, data: &mut DynamicTabData) {
193 if let Some(idx) = data.tab_labels.index_of(&key) {
194 data.remove_tab(idx)
195 }
196 }
197
198 fn tab_label(
199 &self,
200 _key: Self::Key,
201 info: TabInfo<Self::Input>,
202 _data: &Self::Input,
203 ) -> Self::LabelWidget {
204 Self::default_make_label(info)
205 }
206}
207
208fn build_tab_widget(tab_config: &TabConfig) -> impl Widget<AppState> {
209 let dyn_tabs = Tabs::for_policy(NumberedTabs)
210 .with_axis(tab_config.axis)
211 .with_edge(tab_config.edge)
212 .with_transition(tab_config.transition)
213 .lens(AppState::advanced);
214
215 let control_dynamic = Flex::column()
216 .cross_axis_alignment(CrossAxisAlignment::Start)
217 .with_child(Label::new("Control dynamic tabs"))
218 .with_child(Button::new("Add a tab").on_click(|_c, d: &mut DynamicTabData, _e| d.add_tab()))
219 .with_child(Label::new(|adv: &DynamicTabData, _e: &Env| {
220 format!("Highest tab number is {}", adv.highest_tab)
221 }))
222 .with_spacer(20.)
223 .lens(AppState::advanced);
224
225 let first_static_tab = Flex::row()
226 .with_child(Label::new("Rename tab:"))
227 .with_child(TextBox::new().lens(AppState::first_tab_name));
228
229 let main_tabs = Tabs::new()
230 .with_axis(tab_config.axis)
231 .with_edge(tab_config.edge)
232 .with_transition(tab_config.transition)
233 .with_tab(
234 |app_state: &AppState, _: &Env| app_state.first_tab_name.to_string(),
235 first_static_tab,
236 )
237 .with_tab("Dynamic", control_dynamic)
238 .with_tab("Page 3", Label::new("Page 3 content"))
239 .with_tab("Page 4", Label::new("Page 4 content"))
240 .with_tab("Page 5", Label::new("Page 5 content"))
241 .with_tab("Page 6", Label::new("Page 6 content"))
242 .with_tab_index(1);
243
244 Split::rows(main_tabs, dyn_tabs).draggable(true)
245}