tabs/
tabs.rs

1// Copyright 2020 The Druid Authors.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Example of tabs
16
17// On Windows platform, don't show a console when opening the app.
18#![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    // This provides a key that will monotonically increase as interactions occur.
59    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    // describe the main window
80    let main_window = WindowDesc::new(build_root_widget())
81        .title("Tabs")
82        .window_size((700.0, 400.0));
83
84    // create the initial app state
85    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    // start the application
96    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}