Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tabs widget and the Rotated widget that it uses. #1160

Merged
merged 14 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ You can find its changes [documented below](#060---2020-06-01).
- WIDGET_PADDING items added to theme and `Flex::with_default_spacer`/`Flex::add_default_spacer` ([#1220] by [@cmyr])
- CONFIGURE_WINDOW command to allow reconfiguration of an existing window. ([#1235] by [@rjwittams])
- `RawLabel` widget displays text `Data`. ([#1252] by [@cmyr])
- 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1160] by [@rjwittams])

### Changed

Expand Down Expand Up @@ -452,6 +453,7 @@ Last release without a changelog :(
[#1152]: https://github.com/linebender/druid/pull/1152
[#1155]: https://github.com/linebender/druid/pull/1155
[#1157]: https://github.com/linebender/druid/pull/1157
[#1160]: https://github.com/linebender/druid/pull/1160
[#1171]: https://github.com/linebender/druid/pull/1171
[#1172]: https://github.com/linebender/druid/pull/1172
[#1173]: https://github.com/linebender/druid/pull/1173
Expand Down
4 changes: 4 additions & 0 deletions druid/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,10 @@ required-features = ["im"]
name = "svg"
required-features = ["svg"]

[[example]]
name = "tabs"
required-features = ["im"]

[[example]]
name = "widget_gallery"
required-features = ["svg", "im", "image"]
233 changes: 233 additions & 0 deletions druid/examples/tabs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
// Copyright 2020 The Druid Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use druid::im::Vector;
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
use druid::widget::{
Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup,
SizedBox, Split, TabInfo, Tabs, TabsPolicy, TabsTransition, TextBox, ViewSwitcher,
};
use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc};
use instant::Duration;

#[derive(Data, Clone, Lens)]
rjwittams marked this conversation as resolved.
Show resolved Hide resolved
struct DynamicTabData {
highest_tab: usize,
removed_tabs: usize,
tab_labels: Vector<usize>,
}

impl DynamicTabData {
fn new(highest_tab: usize) -> Self {
DynamicTabData {
highest_tab,
removed_tabs: 0,
tab_labels: (1..=highest_tab).collect(),
}
}

fn add_tab(&mut self) {
self.highest_tab += 1;
self.tab_labels.push_back(self.highest_tab);
}

fn remove_tab(&mut self, idx: usize) {
if idx >= self.tab_labels.len() {
log::warn!("Attempt to remove non existent tab at index {}", idx)
} else {
self.removed_tabs += 1;
self.tab_labels.remove(idx);
}
}

// This provides a key that will monotonically increase as interactions occur.
fn tabs_key(&self) -> (usize, usize) {
(self.highest_tab, self.removed_tabs)
}
}

#[derive(Data, Clone, Lens)]
struct TabConfig {
axis: Axis,
cross: CrossAxisAlignment,
transition: TabsTransition,
}

#[derive(Data, Clone, Lens)]
struct AppState {
tab_config: TabConfig,
advanced: DynamicTabData,
first_tab_name: String,
}

pub fn main() {
// describe the main window
let main_window = WindowDesc::new(build_root_widget)
.title("Tabs")
.window_size((700.0, 400.0));

// create the initial app state
let initial_state = AppState {
tab_config: TabConfig {
axis: Axis::Horizontal,
cross: CrossAxisAlignment::Start,
transition: Default::default(),
},
first_tab_name: "First tab".into(),
advanced: DynamicTabData::new(2),
};

// start the application
AppLauncher::with_window(main_window)
.use_simple_logger()
.launch(initial_state)
.expect("Failed to launch application");
}

fn build_root_widget() -> impl Widget<AppState> {
fn decor<T: Data>(label: Label<T>) -> SizedBox<T> {
label
.padding(5.)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when grouping items in Flex containers, I would like to discourage having explicit padding on each item and instead use the with_default_spacer method to insert space between items. Playing around I would add that between each with_child, and also after each child that contains a RadioGroup.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll check that, not sure I knew about it when I wrote this

Copy link
Collaborator Author

@rjwittams rjwittams Sep 28, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying it, this seems to increase the code noise of layout noise quite a bit, because factoring it out is harder (multiple calls onto a flex instead of a returned value ), meaning cut and paste is more likely.

Why is the spacer better than padding, or why does it matter? Also it isn't doing the same thing is it? Padding is on all edges, and the spacer is only in the direction of the flex.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is ensuring widgets look correct when used beside one another; this is especially an issue when using a RadioGroup. If you use the default_spacer methods, you will have the same space between widgets as is used in RadioGroup (and other built-in components) wheras if you pick your own padding values you'll end up with items that don't line up correctly.

This isn't currently a practical issue in this example, but given that examples in general tend to be copied by new users, I want to discourage people from using arbitrary padding values, and encourage them to use these other mechanisms that will create more consistent layouts.

I agree about the increased code noise, and don't have a good answer there. Perhaps in the future we will apply auto-padding to items in collection by default or something? But this is where we are for the time being. 😒

.background(theme::PLACEHOLDER_COLOR)
.expand_width()
}

fn group<T: Data, W: Widget<T> + 'static>(w: W) -> Padding<T> {
w.border(Color::WHITE, 0.5).padding(5.)
}

let axis_picker = Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(decor(Label::new("Tab bar axis")))
.with_child(RadioGroup::new(vec![
("Horizontal", Axis::Horizontal),
("Vertical", Axis::Vertical),
]))
.lens(AppState::tab_config.then(TabConfig::axis));

let cross_picker = Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(decor(Label::new("Tab bar alignment")))
.with_child(RadioGroup::new(vec![
("Start", CrossAxisAlignment::Start),
("End", CrossAxisAlignment::End),
]))
.lens(AppState::tab_config.then(TabConfig::cross));

let transit_picker = Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(decor(Label::new("Transition")))
.with_child(RadioGroup::new(vec![
("Instant", TabsTransition::Instant),
(
"Slide",
TabsTransition::Slide(Duration::from_millis(250).as_nanos() as u64),
),
]))
.lens(AppState::tab_config.then(TabConfig::transition));

let sidebar = Flex::column()
.main_axis_alignment(MainAxisAlignment::Start)
.cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(group(axis_picker))
.with_child(group(cross_picker))
.with_child(group(transit_picker))
.with_flex_spacer(1.)
.fix_width(200.0);

let vs = ViewSwitcher::new(
|app_s: &AppState, _| app_s.tab_config.clone(),
|tc: &TabConfig, _, _| Box::new(build_tab_widget(tc)),
);
Flex::row().with_child(sidebar).with_flex_child(vs, 1.0)
}

#[derive(Clone, Data)]
struct NumberedTabs;

impl TabsPolicy for NumberedTabs {
type Key = usize;
type Build = ();
type Input = DynamicTabData;
type LabelWidget = Label<DynamicTabData>;
type BodyWidget = Label<DynamicTabData>;

fn tabs_changed(&self, old_data: &DynamicTabData, data: &DynamicTabData) -> bool {
old_data.tabs_key() != data.tabs_key()
}

fn tabs(&self, data: &DynamicTabData) -> Vec<Self::Key> {
data.tab_labels.iter().copied().collect()
}

fn tab_info(&self, key: Self::Key, _data: &DynamicTabData) -> TabInfo<DynamicTabData> {
TabInfo::new(format!("Tab {:?}", key), true)
}

fn tab_body(&self, key: Self::Key, _data: &DynamicTabData) -> Label<DynamicTabData> {
Label::new(format!("Dynamic tab body {:?}", key))
}

fn close_tab(&self, key: Self::Key, data: &mut DynamicTabData) {
if let Some(idx) = data.tab_labels.index_of(&key) {
data.remove_tab(idx)
}
}

fn tab_label(
&self,
_key: Self::Key,
info: TabInfo<Self::Input>,
_data: &Self::Input,
) -> Self::LabelWidget {
Self::default_make_label(info)
}
}

fn build_tab_widget(tab_config: &TabConfig) -> impl Widget<AppState> {
let dyn_tabs = Tabs::for_policy(NumberedTabs)
.with_axis(tab_config.axis)
.with_cross_axis_alignment(tab_config.cross)
.with_transition(tab_config.transition)
.lens(AppState::advanced);

let control_dynamic = Flex::column()
.cross_axis_alignment(CrossAxisAlignment::Start)
.with_child(Label::new("Control dynamic tabs"))
.with_child(Button::new("Add a tab").on_click(|_c, d: &mut DynamicTabData, _e| d.add_tab()))
.with_child(Label::new(|adv: &DynamicTabData, _e: &Env| {
format!("Highest tab number is {}", adv.highest_tab)
}))
.with_spacer(20.)
.lens(AppState::advanced);

let first_static_tab = Flex::row()
.with_child(Label::new("Rename tab:"))
.with_child(TextBox::new().lens(AppState::first_tab_name));

let main_tabs = Tabs::new()
.with_axis(tab_config.axis)
.with_cross_axis_alignment(tab_config.cross)
.with_transition(tab_config.transition)
.with_tab(
|app_state: &AppState, _: &Env| app_state.first_tab_name.to_string(),
first_static_tab,
)
.with_tab("Dynamic", control_dynamic)
.with_tab("Page 3", Label::new("Page 3 content"))
.with_tab("Page 4", Label::new("Page 4 content"))
.with_tab("Page 5", Label::new("Page 5 content"))
.with_tab("Page 6", Label::new("Page 6 content"));

Split::rows(main_tabs, dyn_tabs).draggable(true)
}
1 change: 1 addition & 0 deletions druid/examples/web/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ impl_example!(split_demo);
impl_example!(styled_text.unwrap());
impl_example!(switches);
impl_example!(timer);
impl_example!(tabs);
impl_example!(view_switcher);
impl_example!(widget_gallery);
impl_example!(text);
1 change: 1 addition & 0 deletions druid/src/lens/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
//! ```

#[allow(clippy::module_inception)]
#[macro_use]
mod lens;
pub use lens::{Deref, Field, Id, InArc, Index, Map, Ref, Then, Unit};
#[doc(hidden)]
Expand Down
4 changes: 3 additions & 1 deletion druid/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ pub use druid_shell::{kurbo, piet};
#[doc(inline)]
pub use im;

#[macro_use]
pub mod lens;
cmyr marked this conversation as resolved.
Show resolved Hide resolved

mod app;
mod app_delegate;
mod bloom;
Expand All @@ -153,7 +156,6 @@ mod data;
mod env;
mod event;
mod ext_event;
pub mod lens;
mod localization;
mod menu;
mod mouse;
Expand Down
Loading