From fe395dbc8d8349b7c7a4bfc9d3c27429a066c67b Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 26 Aug 2020 22:48:06 +0100 Subject: [PATCH 01/12] Tab widget and the Rotated widget that it uses. --- CHANGELOG.md | 3 + druid-shell/src/region.rs | 8 +- druid/Cargo.toml | 4 + druid/examples/tabs.rs | 211 +++++++ druid/examples/web/src/lib.rs | 1 + druid/examples/widget_gallery.rs | 8 +- druid/src/lens/mod.rs | 1 + druid/src/lib.rs | 1 + druid/src/widget/flex.rs | 56 +- druid/src/widget/mod.rs | 6 +- druid/src/widget/rotated.rs | 111 ++++ druid/src/widget/tabs.rs | 923 +++++++++++++++++++++++++++++++ druid/src/widget/widget_ext.rs | 7 + 13 files changed, 1320 insertions(+), 20 deletions(-) create mode 100644 druid/examples/tabs.rs create mode 100644 druid/src/widget/rotated.rs create mode 100644 druid/src/widget/tabs.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7032fd26fc..36ff4746b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ You can find its changes [documented below](#060---2020-06-01). - `request_update` in `EventCtx`. ([#1128] by [@raphlinus]) - `ExtEventSink`s can now be obtained from widget methods. ([#1152] by [@jneem]) - 'Scope' widget to allow encapsulation of reactive state. ([#1151] by [@rjwittams]) +- 'Rotated' widget to allow widgets to be rotated by an integral number of quarter turns ([#1159] by [@rjwittams]) +- 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1159] by [@rjwittams]) ### Changed @@ -403,6 +405,7 @@ Last release without a changelog :( [#1151]: https://github.com/linebender/druid/pull/1151 [#1152]: https://github.com/linebender/druid/pull/1152 [#1157]: https://github.com/linebender/druid/pull/1157 +[#1158]: https://github.com/linebender/druid/pull/1158 [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 diff --git a/druid-shell/src/region.rs b/druid-shell/src/region.rs index 99fb24f0cf..56eea7c627 100644 --- a/druid-shell/src/region.rs +++ b/druid-shell/src/region.rs @@ -1,4 +1,4 @@ -use kurbo::{BezPath, Rect, Shape, Vec2}; +use kurbo::{Affine, BezPath, Rect, Shape, Vec2}; /// A union of rectangles, useful for describing an area that needs to be repainted. #[derive(Clone, Debug)] @@ -81,6 +81,12 @@ impl Region { } self.rects.retain(|r| r.area() > 0.0) } + + pub fn transform_by(&mut self, transform: Affine) { + for rect in &mut self.rects { + *rect = transform.transform_rect_bbox(*rect) + } + } } impl std::ops::AddAssign for Region { diff --git a/druid/Cargo.toml b/druid/Cargo.toml index 7e7470171a..b26b6355b9 100644 --- a/druid/Cargo.toml +++ b/druid/Cargo.toml @@ -70,6 +70,10 @@ required-features = ["im"] name = "svg" required-features = ["svg"] +[[example]] +name = "tabs" +required-features = ["im"] + [[example]] name = "widget_gallery" required-features = ["svg", "im", "image"] diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs new file mode 100644 index 0000000000..373f9226b5 --- /dev/null +++ b/druid/examples/tabs.rs @@ -0,0 +1,211 @@ +use druid::im::Vector; +use druid::widget::{ + Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, + SizedBox, Split, TabInfo, TabOrientation, Tabs, TabsPolicy, ViewSwitcher, +}; +use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc}; + +#[derive(Data, Clone)] +struct Basic {} + +#[derive(Data, Clone, Lens)] +struct Advanced { + highest_tab: usize, + removed_tabs: usize, + tab_labels: Vector, +} + +impl Advanced { + fn new(highest_tab: usize) -> Self { + Advanced { + 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); + } + } + + fn tabs_key(&self) -> (usize, usize) { + (self.highest_tab, self.removed_tabs) + } +} + +#[derive(Data, Clone, Lens)] +struct TabConfig { + axis: Axis, + cross: CrossAxisAlignment, + rotation: TabOrientation, +} + +#[derive(Data, Clone, Lens)] +struct AppState { + tab_config: TabConfig, + basic: Basic, + advanced: Advanced, +} + +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, + rotation: TabOrientation::Standard, + }, + basic: Basic {}, + advanced: Advanced::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 { + fn decor(label: Label) -> SizedBox { + label + .padding(5.) + .background(theme::PLACEHOLDER_COLOR) + .expand_width() + } + + fn group + 'static>(w: W) -> Padding { + 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 rot_picker = Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child(decor(Label::new("Tab rotation"))) + .with_child(RadioGroup::new(vec![ + ("Standard", TabOrientation::Standard), + ("None", TabOrientation::Turns(0)), + ("Up", TabOrientation::Turns(3)), + ("Down", TabOrientation::Turns(1)), + ("Aussie", TabOrientation::Turns(2)), + ])) + .lens(AppState::tab_config.then(TabConfig::rotation)); + + 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(rot_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 = Advanced; + type LabelWidget = Label; + type BodyWidget = Label; + + fn tabs_changed(&self, old_data: &Advanced, data: &Advanced) -> bool { + old_data.tabs_key() != data.tabs_key() + } + + fn tabs(&self, data: &Advanced) -> Vec { + data.tab_labels.iter().copied().collect() + } + + fn tab_info(&self, key: Self::Key, _data: &Advanced) -> TabInfo { + TabInfo::new(format!("Tab {:?}", key), true) + } + + fn tab_body(&self, key: Self::Key, _data: &Advanced) -> Option> { + Some(Label::new(format!("Dynamic tab body {:?}", key))) + } + + fn close_tab(&self, key: Self::Key, data: &mut Advanced) { + if let Some(idx) = data.tab_labels.index_of(&key) { + data.remove_tab(idx) + } + } + + fn tab_label(&self, _key: Self::Key, info: &TabInfo, _data: &Self::Input) -> Self::LabelWidget { + Self::default_make_label(info) + } +} + +fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { + let dyn_tabs = Tabs::for_policy(NumberedTabs) + .with_axis(tab_config.axis) + .with_cross_axis_alignment(tab_config.cross) + .with_rotation(tab_config.rotation) + .lens(AppState::advanced); + + let adv = 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 Advanced, _e| d.add_tab())) + .with_child(Label::new(|adv: &Advanced, _e: &Env| { + format!("Highest tab number is {}", adv.highest_tab) + })) + .with_spacer(20.) + .lens(AppState::advanced); + + let main_tabs = Tabs::new() + .with_axis(tab_config.axis) + .with_cross_axis_alignment(tab_config.cross) + .with_rotation(tab_config.rotation) + .with_tab("Basic", Label::new("Basic kind of stuff")) + .with_tab("Advanced", adv) + .with_tab("Page 3", Label::new("Basic kind of stuff")) + .with_tab("Page 4", Label::new("Basic kind of stuff")) + .with_tab("Page 5", Label::new("Basic kind of stuff")) + .with_tab("Page 6", Label::new("Basic kind of stuff")) + .with_tab("Page 7", Label::new("Basic kind of stuff")); + + let col = Split::rows(main_tabs, dyn_tabs).draggable(true); + + col +} diff --git a/druid/examples/web/src/lib.rs b/druid/examples/web/src/lib.rs index 78fc69f620..61bdde4b26 100644 --- a/druid/examples/web/src/lib.rs +++ b/druid/examples/web/src/lib.rs @@ -77,5 +77,6 @@ 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); diff --git a/druid/examples/widget_gallery.rs b/druid/examples/widget_gallery.rs index 4cd4e0c119..9cd92593da 100644 --- a/druid/examples/widget_gallery.rs +++ b/druid/examples/widget_gallery.rs @@ -23,7 +23,7 @@ use druid::{ AppLauncher, Color, Data, Lens, Rect, Widget, WidgetExt, WidgetPod, WindowDesc, }; -#[cfg(not(target_arch = "wasm32"))] +#[cfg(feature = "svg")] use druid::widget::{Svg, SvgData}; const XI_IMAGE: &[u8] = include_bytes!("assets/xi.image"); @@ -67,7 +67,7 @@ pub fn main() { } fn ui_builder() -> impl Widget { - #[cfg(not(target_arch = "wasm32"))] + #[cfg(feature = "svg")] let svg_example = label_widget( Svg::new( include_str!("./assets/tiger.svg") @@ -77,8 +77,8 @@ fn ui_builder() -> impl Widget { "Svg", ); - #[cfg(target_arch = "wasm32")] - let svg_example = label_widget(Label::new("no SVG on wasm (yet)").center(), "Svg"); + #[cfg(not(feature = "svg"))] + let svg_example = label_widget(Label::new("SVG not supported (yet)").center(), "Svg"); Scroll::new( SquaresGrid::new() diff --git a/druid/src/lens/mod.rs b/druid/src/lens/mod.rs index 2a62ade9cf..e3adc066eb 100644 --- a/druid/src/lens/mod.rs +++ b/druid/src/lens/mod.rs @@ -47,6 +47,7 @@ //! ``` #[allow(clippy::module_inception)] +#[macro_use] mod lens; pub use lens::{Deref, Field, Id, InArc, Index, Map, Then}; #[doc(hidden)] diff --git a/druid/src/lib.rs b/druid/src/lib.rs index efa267c738..05f721966f 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -149,6 +149,7 @@ mod data; mod env; mod event; mod ext_event; +#[macro_use] pub mod lens; mod localization; mod menu; diff --git a/druid/src/widget/flex.rs b/druid/src/widget/flex.rs index 7e923c46ab..7548edcd5d 100644 --- a/druid/src/widget/flex.rs +++ b/druid/src/widget/flex.rs @@ -195,8 +195,8 @@ pub struct FlexParams { alignment: Option, } -#[derive(Clone, Copy)] -pub(crate) enum Axis { +#[derive(Data, Debug, Clone, Copy, PartialEq)] +pub enum Axis { Horizontal, Vertical, } @@ -279,6 +279,13 @@ impl ChildWidget { } impl Axis { + pub fn cross(self) -> Axis { + match self { + Axis::Horizontal => Axis::Vertical, + Axis::Vertical => Axis::Horizontal, + } + } + pub(crate) fn major(self, coords: Size) -> f64 { match self { Axis::Horizontal => coords.width, @@ -286,6 +293,28 @@ impl Axis { } } + pub fn major_span(self, rect: &Rect) -> (f64, f64) { + match self { + Axis::Horizontal => (rect.x0, rect.x1), + Axis::Vertical => (rect.y0, rect.y1), + } + } + + pub fn minor_span(self, rect: &Rect) -> (f64, f64) { + self.cross().major_span(rect) + } + + pub fn major_pos(self, pos: Point) -> f64 { + match self { + Axis::Horizontal => pos.x, + Axis::Vertical => pos.y, + } + } + + pub fn minor_pos(self, pos: Point) -> f64 { + self.cross().major_pos(pos) + } + pub(crate) fn minor(self, coords: Size) -> f64 { match self { Axis::Horizontal => coords.height, @@ -316,12 +345,9 @@ impl Axis { } impl Flex { - /// Create a new horizontal stack. - /// - /// The child widgets are laid out horizontally, from left to right. - pub fn row() -> Self { + pub fn for_axis(axis: Axis) -> Self { Flex { - direction: Axis::Horizontal, + direction: axis, children: Vec::new(), cross_alignment: CrossAxisAlignment::Center, main_alignment: MainAxisAlignment::Start, @@ -329,17 +355,19 @@ impl Flex { } } + /// Create a new horizontal stack. + /// + /// The child widgets are laid out horizontally, from left to right. + /// + pub fn row() -> Self { + Self::for_axis(Axis::Horizontal) + } + /// Create a new vertical stack. /// /// The child widgets are laid out vertically, from top to bottom. pub fn column() -> Self { - Flex { - direction: Axis::Vertical, - children: Vec::new(), - cross_alignment: CrossAxisAlignment::Center, - main_alignment: MainAxisAlignment::Start, - fill_major_axis: false, - } + Self::for_axis(Axis::Vertical) } /// Builder-style method for specifying the childrens' [`CrossAxisAlignment`]. diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 5af57a32f0..8d2995d75c 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -34,6 +34,7 @@ mod painter; mod parse; mod progress_bar; mod radio; +mod rotated; mod scope; mod scroll; mod sized_box; @@ -45,6 +46,7 @@ mod stepper; #[cfg_attr(docsrs, doc(cfg(feature = "svg")))] mod svg; mod switch; +mod tabs; mod textbox; mod view_switcher; #[allow(clippy::module_inception)] @@ -61,7 +63,7 @@ pub use container::Container; pub use controller::{Controller, ControllerHost}; pub use either::Either; pub use env_scope::EnvScope; -pub use flex::{CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; +pub use flex::{Axis, CrossAxisAlignment, Flex, FlexParams, MainAxisAlignment}; pub use identity_wrapper::IdentityWrapper; pub use label::{Label, LabelText}; pub use list::{List, ListIter}; @@ -70,6 +72,7 @@ pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; pub use progress_bar::ProgressBar; pub use radio::{Radio, RadioGroup}; +pub use rotated::Rotated; pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; pub use scroll::Scroll; pub use sized_box::SizedBox; @@ -80,6 +83,7 @@ pub use stepper::Stepper; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; +pub use tabs::{TabInfo, TabOrientation, Tabs, TabsPolicy, TabsState}; pub use textbox::TextBox; pub use view_switcher::ViewSwitcher; #[doc(hidden)] diff --git a/druid/src/widget/rotated.rs b/druid/src/widget/rotated.rs new file mode 100644 index 0000000000..d4b666ade2 --- /dev/null +++ b/druid/src/widget/rotated.rs @@ -0,0 +1,111 @@ +use crate::event::Event::{MouseDown, MouseMove, MouseUp, Wheel}; +use crate::{ + Affine, BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, MouseEvent, + PaintCtx, RenderContext, Size, UpdateCtx, Vec2, Widget, +}; +use std::f64::consts::PI; + +pub struct Rotated { + child: W, + quarter_turns: u8, + transforms: Option<(Affine, Affine)>, +} + +impl Rotated { + pub fn new(child: W, quarter_turns: u8) -> Self { + Rotated { + child, + quarter_turns, + transforms: None, + } + } +} + +impl Rotated { + fn flip(&self, size: Size) -> Size { + if self.quarter_turns % 2 == 0 { + size + } else { + Size::new(size.height, size.width) + } + } + + fn affine(&self, child_size: Size, my_size: Size) -> Affine { + let a = ((self.quarter_turns % 4) as f64) * PI / 2.0; + + Affine::translate(Vec2::new(my_size.width / 2., my_size.height / 2.)) + * Affine::rotate(a) + * Affine::translate(Vec2::new(-child_size.width / 2., -child_size.height / 2.)) + } + + fn translate_mouse_event(&self, inverse: Affine, me: &MouseEvent) -> MouseEvent { + let mut me = me.clone(); + me.pos = inverse * me.pos; + me + } +} + +impl> Widget for Rotated { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { + if let Some((transform, inverse)) = self.transforms { + ctx.widget_state.invalid.transform_by(inverse); + match event { + MouseMove(me) => self.child.event( + ctx, + &Event::MouseMove(self.translate_mouse_event(inverse, me)), + data, + env, + ), + MouseDown(me) => self.child.event( + ctx, + &Event::MouseDown(self.translate_mouse_event(inverse, me)), + data, + env, + ), + MouseUp(me) => self.child.event( + ctx, + &Event::MouseUp(self.translate_mouse_event(inverse, me)), + data, + env, + ), + Wheel(me) => self.child.event( + ctx, + &Event::Wheel(self.translate_mouse_event(inverse, me)), + data, + env, + ), + _ => self.child.event(ctx, event, data, env), + } + ctx.widget_state.invalid.transform_by(transform); + } + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { + self.child.lifecycle(ctx, event, data, env) + } + + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { + self.child.update(ctx, old_data, data, env) + } + + fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { + let bc = BoxConstraints::new(self.flip(bc.min()), self.flip(bc.max())); + + let child_size = self.child.layout(ctx, &bc, data, env); + let flipped_size = self.flip(child_size); + let transform = self.affine(child_size, flipped_size); + self.transforms = Some((transform, transform.inverse())); + flipped_size + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { + if let Some((transform, inverse)) = self.transforms { + ctx.region.transform_by(inverse); + ctx.with_save(|ctx| { + ctx.transform(transform); + self.child.paint(ctx, data, env) + }); + ctx.region.transform_by(transform); + } + } +} diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs new file mode 100644 index 0000000000..ca6d6687d8 --- /dev/null +++ b/druid/src/widget/tabs.rs @@ -0,0 +1,923 @@ +// 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. + +//! A widget that can switch between one of many views, hiding the inactive ones. +//! + +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; +use std::marker::PhantomData; +use std::rc::Rc; + +use crate::kurbo::Line; +use crate::piet::RenderContext; + +use crate::widget::{Axis, CrossAxisAlignment, Flex, Label, LensScopeTransfer, Scope, ScopePolicy}; +use crate::{ + theme, Affine, BoxConstraints, Data, Env, Event, EventCtx, Insets, LayoutCtx, Lens, LifeCycle, + LifeCycleCtx, PaintCtx, Point, Rect, SingleUse, Size, UpdateCtx, Widget, WidgetExt, WidgetPod, +}; + +type TabsScope = Scope, Box>>>; +type TabBodyPod = WidgetPod<::Input, ::BodyWidget>; +type TabBarPod = WidgetPod, Box>>>; +type TabIndex = usize; + +const MILLIS: u64 = 1_000_000; // Number of nanos + +pub struct TabInfo { + pub name: String, + pub can_close: bool, +} + +impl TabInfo { + pub fn new(name: String, can_close: bool) -> Self { + TabInfo { name, can_close } + } +} + +/// A policy that determines how a Tabs instance derives its tabs from its app data +pub trait TabsPolicy: Data { + /// The identity of a tab. + type Key: Hash + Eq + Clone + Debug; + + /// The input data that will a) be used to derive the tab and b) also be the input data of all the child widgets. + type Input: Data; + + /// The common type for all body widgets in this set of tabs. + /// A flexible default is Box> + type BodyWidget: Widget; + + /// The common type for all label widgets in this set of tabs + /// Usually this would be Label + type LabelWidget: Widget; + + /// This policy whilst it is being built. + /// This is only be useful for implementations supporting AddTab, such as StaticTabs. + /// It can be filled in with () by other implementations until associated type defaults are stable + type Build; + + /// Have the tabs changed. Expected to be cheap, eg pointer or numeric comparison. + fn tabs_changed(&self, old_data: &Self::Input, data: &Self::Input) -> bool; + + /// What are the current tabs set in order. + fn tabs(&self, data: &Self::Input) -> Vec; + + /// Presentation information for the tab + fn tab_info(&self, key: Self::Key, data: &Self::Input) -> TabInfo; + + /// Body widget for the tab + fn tab_body(&self, key: Self::Key, data: &Self::Input) -> Option; + + /// Label widget for the tab. + /// Usually implemented with a call to default_make_label ( can't default here because Self::LabelWidget isn't determined) + fn tab_label(&self, key: Self::Key, info: &TabInfo, data: &Self::Input) -> Self::LabelWidget; + + /// Change the data to reflect the user requesting to close a tab. + #[allow(unused_variables)] + fn close_tab(&self, key: Self::Key, data: &mut Self::Input) {} + + #[allow(unused_variables)] + /// Construct an instance of this TabsFromData from its Build type. + /// This should only be implemented if supporting AddTab - possibly only StaticTabs needs to. + fn build(build: Self::Build) -> Self { + unimplemented!() + } + + fn default_make_label(info: &TabInfo) -> Label { + Label::new(info.name.clone()).with_text_color(theme::FOREGROUND_LIGHT) + } +} + +#[derive(Clone)] +pub struct StaticTabs { + // This needs be able to avoid cloning the widgets we are given - + // as such it is Rc + tabs: Rc>>, +} + +impl StaticTabs { + pub fn new() -> Self { + StaticTabs { + tabs: Rc::new(Vec::new()), + } + } +} + +impl Data for StaticTabs { + fn same(&self, _other: &Self) -> bool { + // Changing the tabs after construction shouldn't be possible for static tabs + // It seems pointless to compare them + true + } +} + +impl TabsPolicy for StaticTabs { + type Key = usize; + type Input = T; + type BodyWidget = Box>; + type LabelWidget = Label; + type Build = Vec>; + + fn tabs_changed(&self, _old_data: &T, _data: &T) -> bool { + false + } + + fn tabs(&self, _data: &T) -> Vec { + (0..self.tabs.len()).collect() + } + + fn tab_info(&self, key: Self::Key, _data: &T) -> TabInfo { + TabInfo::new(self.tabs[key].name.clone(), false) + } + + fn tab_body(&self, key: Self::Key, _data: &T) -> Option { + self.tabs[key].child.take() + } + + fn tab_label(&self, _key: Self::Key, info: &TabInfo, _data: &Self::Input) -> Self::LabelWidget { + Self::default_make_label(info) + } + + fn build(build: Self::Build) -> Self { + StaticTabs { + tabs: Rc::new(build), + } + } +} + +pub trait AddTab: TabsPolicy { + fn add_tab( + tabs: &mut Self::Build, + name: impl Into, + child: impl Widget + 'static, + ); +} + +impl AddTab for StaticTabs { + fn add_tab(tabs: &mut Self::Build, name: impl Into, child: impl Widget + 'static) { + tabs.push(InitialTab::new(name, child)) + } +} + +#[derive(Clone, Lens, Data)] +pub struct TabsState { + pub inner: TP::Input, + pub selected: TabIndex, + pub policy: TP, +} + +impl TabsState { + pub fn new(inner: TP::Input, selected: usize, policy: TP) -> Self { + TabsState { + inner, + selected, + policy, + } + } +} + +pub struct TabBar { + axis: Axis, + cross: CrossAxisAlignment, + orientation: TabOrientation, + tabs: Vec<(TP::Key, TabBarPod)>, + hot: Option, + phantom_tp: PhantomData, +} + +impl TabBar { + pub fn new(axis: Axis, cross: CrossAxisAlignment, orientation: TabOrientation) -> Self { + TabBar { + axis, + cross, + orientation, + tabs: vec![], + hot: None, + phantom_tp: Default::default(), + } + } + + fn find_idx(&self, pos: Point) -> Option { + let major_pix = self.axis.major_pos(pos); + let axis = self.axis; + let res = self + .tabs + .binary_search_by_key(&((major_pix * 10.) as i64), |(_, tab)| { + let rect = tab.layout_rect(); + let far_pix = axis.major_pos(rect.origin()) + axis.major(rect.size()); + (far_pix * 10.) as i64 + }); + match res { + Ok(idx) => Some(idx), + Err(idx) if idx < self.tabs.len() => Some(idx), + _ => None, + } + } + + fn ensure_tabs(&mut self, data: &TabsState) { + // Borrow checker/ type inference fun + let (orientation, axis, cross) = (self.orientation, self.axis, self.cross); + let finish_row = |w| WidgetPod::new(orientation.rotate_and_box(w, axis, cross)); + let finish_label = |w| WidgetPod::new(orientation.rotate_and_box(w, axis, cross)); + + ensure_for_tabs(&mut self.tabs, &data.policy, &data.inner, |policy, key| { + let info = policy.tab_info(key.clone(), &data.inner); + + let label = data + .policy + .tab_label(key.clone(), &info, &data.inner) + // TODO: Type inference fails here because both sides of the lens are dependent on + // associated types of the policy. Needs changes to lens derivation to embed PhantomData of the (relevant?) type params) + // of the lensed types into the lens, to type inference has something to grab hold of + .lens::, tabs_state_derived_lenses::inner>(TabsState::::inner) + .padding(Insets::uniform_xy(9., 5.)); + + if info.can_close { + let row = Flex::row() + .with_child(label) + .with_child(Label::new("ⓧ").on_click( + move |_ctx, data: &mut TabsState, _env| { + data.policy.close_tab(key.clone(), &mut data.inner); + }, + )); + finish_row(row) + } else { + finish_label(label) + } + }); + } +} + +impl Widget> for TabBar { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut TabsState, env: &Env) { + match event { + Event::MouseDown(e) => { + if let Some(idx) = self.find_idx(e.pos) { + data.selected = idx; + } + } + Event::MouseMove(e) => { + let new_hot = if ctx.is_hot() { + self.find_idx(e.pos) + } else { + None + }; + if new_hot != self.hot { + self.hot = new_hot; + ctx.request_paint(); + } + } + _ => {} + } + + for (_, tab) in self.tabs.iter_mut() { + tab.event(ctx, event, data, env); + } + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &TabsState, + env: &Env, + ) { + if let LifeCycle::WidgetAdded = event { + self.ensure_tabs(data); + ctx.children_changed(); + ctx.request_layout(); + } + + for (_, tab) in self.tabs.iter_mut() { + tab.lifecycle(ctx, event, data, env); + } + } + + fn update( + &mut self, + ctx: &mut UpdateCtx, + old_data: &TabsState, + data: &TabsState, + _env: &Env, + ) { + if data.policy.tabs_changed(&old_data.inner, &data.inner) { + self.ensure_tabs(data); + ctx.children_changed(); + ctx.request_layout(); + } else if old_data.selected != data.selected { + ctx.request_paint(); + } + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &TabsState, + env: &Env, + ) -> Size { + let (mut major, mut minor) = (0., 0.); + for (_, tab) in self.tabs.iter_mut() { + let size = tab.layout(ctx, bc, data, env); + tab.set_layout_rect( + ctx, + data, + env, + Rect::from_origin_size(self.axis.pack(major, 0.), size), + ); + major += self.axis.major(size); + minor = f64::max(minor, self.axis.minor(size)); + } + // Now go back through to reset the minors + for (_, tab) in self.tabs.iter_mut() { + let rect = tab.layout_rect(); + let rect = rect.with_size(self.axis.pack(self.axis.major(rect.size()), minor)); + tab.set_layout_rect(ctx, data, env, rect); + } + + let wanted = self + .axis + .pack(f64::max(major, self.axis.major(bc.max())), minor); + bc.constrain(wanted) + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &TabsState, env: &Env) { + let hl_thickness = 2.; + let highlight = env.get(theme::PRIMARY_LIGHT); + // TODO: allow reversing tab order (makes more sense in some rotations) + for (idx, (_, tab)) in self.tabs.iter_mut().enumerate() { + let rect = tab.layout_rect(); + let rect = Rect::from_origin_size(rect.origin(), rect.size()); + let bg = match (idx == data.selected, Some(idx) == self.hot) { + (_, true) => env.get(theme::BUTTON_DARK), + (true, false) => env.get(theme::BACKGROUND_LIGHT), + _ => env.get(theme::BACKGROUND_DARK), + }; + ctx.fill(rect, &bg); + + tab.paint(ctx, data, env); + if idx == data.selected { + let (maj_near, maj_far) = self.axis.major_span(&rect); + let (min_near, min_far) = self.axis.minor_span(&rect); + let minor_pos = if let CrossAxisAlignment::End = self.cross { + min_near + (hl_thickness / 2.) + } else { + min_far - (hl_thickness / 2.) + }; + + ctx.stroke( + Line::new( + self.axis.pack(maj_near, minor_pos), + self.axis.pack(maj_far, minor_pos), + ), + &highlight, + hl_thickness, + ) + } + } + } +} + +pub struct TabsTransition { + previous_idx: TabIndex, + current_time: u64, + length: u64, + increasing: bool, +} + +impl TabsTransition { + pub fn new(previous_idx: TabIndex, length: u64, increasing: bool) -> Self { + TabsTransition { + previous_idx, + current_time: 0, + length, + increasing, + } + } + + pub fn live(&self) -> bool { + self.current_time < self.length + } + + pub fn fraction(&self) -> f64 { + (self.current_time as f64) / (self.length as f64) + } + + pub fn previous_transform(&self, axis: Axis, main: f64) -> Affine { + let x = if self.increasing { + -main * self.fraction() + } else { + main * self.fraction() + }; + Affine::translate(axis.pack(x, 0.)) + } + + pub fn selected_transform(&self, axis: Axis, main: f64) -> Affine { + let x = if self.increasing { + main * (1.0 - self.fraction()) + } else { + -main * (1.0 - self.fraction()) + }; + Affine::translate(axis.pack(x, 0.)) + } +} + +fn ensure_for_tabs( + contents: &mut Vec<(TP::Key, Content)>, + policy: &TP, + data: &TP::Input, + f: impl Fn(&TP, TP::Key) -> Content, +) -> Vec { + let mut existing_by_key: HashMap = contents.drain(..).collect(); + + let mut existing_idx = Vec::new(); + for key in policy.tabs(data).into_iter() { + let next = if let Some(child) = existing_by_key.remove(&key) { + existing_idx.push(contents.len()); + child + } else { + f(&policy, key.clone()) + }; + contents.push((key.clone(), next)) + } + existing_idx +} + +pub struct TabsBody { + children: Vec<(TP::Key, Option>)>, + transition: Option, + axis: Axis, + phantom_tp: PhantomData, +} + +impl TabsBody { + pub fn new(axis: Axis) -> TabsBody { + TabsBody { + children: vec![], + transition: None, + axis, + phantom_tp: Default::default(), + } + } + + fn make_tabs(&mut self, data: &TabsState) -> Vec { + ensure_for_tabs( + &mut self.children, + &data.policy, + &data.inner, + |policy, key| { + policy + .tab_body(key.clone(), &data.inner) + .map(WidgetPod::new) + }, + ) + } + + fn active_child(&mut self, state: &TabsState) -> Option<&mut TabBodyPod> { + Self::child(&mut self.children, state.selected) + } + + // Doesn't take self to allow separate borrowing + fn child( + children: &mut Vec<(TP::Key, Option>)>, + idx: usize, + ) -> Option<&mut TabBodyPod> { + children.get_mut(idx).and_then(|x| x.1.as_mut()) + } + + fn child_pods(&mut self) -> impl Iterator> { + self.children.iter_mut().flat_map(|x| x.1.as_mut()) + } +} + +fn hidden_should_receive_event(evt: &Event) -> bool { + match evt { + Event::WindowConnected + | Event::WindowSize(_) + | Event::Timer(_) + | Event::Command(_) + | Event::Internal(_) => true, + Event::MouseDown(_) + | Event::MouseUp(_) + | Event::MouseMove(_) + | Event::Wheel(_) + | Event::KeyDown(_) + | Event::KeyUp(_) + | Event::Paste(_) + | Event::Zoom(_) => false, + } +} + +fn hidden_should_receive_lifecycle(lc: &LifeCycle) -> bool { + match lc { + LifeCycle::WidgetAdded | LifeCycle::Internal(_) => true, + LifeCycle::Size(_) + | LifeCycle::AnimFrame(_) + | LifeCycle::HotChanged(_) + | LifeCycle::FocusChanged(_) => false, + } +} + +impl Widget> for TabsBody { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut TabsState, env: &Env) { + if hidden_should_receive_event(event) { + for child in self.child_pods() { + child.event(ctx, event, &mut data.inner, env); + } + } else if let Some(child) = self.active_child(data) { + child.event(ctx, event, &mut data.inner, env); + } + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &TabsState, + env: &Env, + ) { + if let LifeCycle::WidgetAdded = event { + self.make_tabs(data); + ctx.children_changed(); + ctx.request_layout(); + } + + if hidden_should_receive_lifecycle(event) { + for child in self.child_pods() { + child.lifecycle(ctx, event, &data.inner, env); + } + } else if let Some(child) = self.active_child(data) { + // Pick which events go to all and which just to active + child.lifecycle(ctx, event, &data.inner, env); + } + + if let (Some(trans), LifeCycle::AnimFrame(interval)) = (&mut self.transition, event) { + trans.current_time += *interval; + if trans.live() { + ctx.request_anim_frame(); + } else { + self.transition = None; + } + } + } + + fn update( + &mut self, + ctx: &mut UpdateCtx, + old_data: &TabsState, + data: &TabsState, + env: &Env, + ) { + let init = if data.policy.tabs_changed(&old_data.inner, &data.inner) { + ctx.children_changed(); + ctx.request_layout(); + Some(self.make_tabs(data)) + } else { + None + }; + + if old_data.selected != data.selected { + self.transition = Some(TabsTransition::new( + old_data.selected, + 250 * MILLIS, + old_data.selected < data.selected, + )); + ctx.request_layout(); + ctx.request_anim_frame(); + } + + // Make sure to only pass events to initialised children + if let Some(init) = init { + for idx in init { + if let Some(child) = Self::child(&mut self.children, idx) { + child.update(ctx, &data.inner, env) + } + } + } else { + for child in self.child_pods() { + child.update(ctx, &data.inner, env); + } + } + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &TabsState, + env: &Env, + ) -> Size { + if let Some(ref mut child) = self.active_child(data) { + let inner = &data.inner; + let size = child.layout(ctx, bc, inner, env); + child.set_layout_rect(ctx, inner, env, Rect::from_origin_size(Point::ORIGIN, size)); + size + } else { + bc.max() + } + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &TabsState, env: &Env) { + if let Some(trans) = &self.transition { + let axis = self.axis; + let size = ctx.size(); + let major = axis.major(size); + ctx.clip(Rect::from_origin_size(Point::ZERO, size)); + + let children = &mut self.children; + if let Some(ref mut prev) = Self::child(children, trans.previous_idx) { + ctx.with_save(|ctx| { + ctx.transform(trans.previous_transform(axis, major)); + prev.paint_raw(ctx, &data.inner, env); + }) + } + if let Some(ref mut child) = Self::child(children, data.selected) { + ctx.with_save(|ctx| { + ctx.transform(trans.selected_transform(axis, major)); + child.paint_raw(ctx, &data.inner, env); + }) + } + } else { + if let Some(ref mut child) = Self::child(&mut self.children, data.selected) { + child.paint_raw(ctx, &data.inner, env); + } + } + } +} + +// This only needs to exist to be able to give a reasonable type to the TabScope +pub struct TabsScopePolicy { + tabs_from_data: TP, + selected: TabIndex, +} + +impl TabsScopePolicy { + pub fn new(tabs_from_data: TP, selected: TabIndex) -> Self { + Self { + tabs_from_data, + selected, + } + } +} + +impl ScopePolicy for TabsScopePolicy { + type In = TP::Input; + type State = TabsState; + type Transfer = LensScopeTransfer; + + fn create(self, inner: &Self::In) -> (Self::State, Self::Transfer) { + ( + TabsState::new(inner.clone(), self.selected, self.tabs_from_data), + LensScopeTransfer::new(Self::State::inner), + ) + } +} + +#[derive(Data, Copy, Clone, Debug, PartialOrd, PartialEq)] +pub enum TabOrientation { + Standard, + Turns(u8), // These represent 90 degree rotations clockwise. +} + +impl TabOrientation { + pub fn rotate_and_box + 'static, T: Data>( + self, + widget: W, + axis: Axis, + cross: CrossAxisAlignment, + ) -> Box> { + let turns = match self { + Self::Standard => match (axis, cross) { + (Axis::Horizontal, _) => 0, + (Axis::Vertical, CrossAxisAlignment::Start) => 3, + (Axis::Vertical, _) => 1, + }, + Self::Turns(turns) => turns, + }; + + if turns == 0 { + Box::new(widget) + } else { + Box::new(widget.rotate(turns)) + } + } +} + +pub struct InitialTab { + name: String, + child: SingleUse>>, // This is to avoid cloning provided tabs +} + +impl InitialTab { + pub fn new(name: impl Into, child: impl Widget + 'static) -> Self { + InitialTab { + name: name.into(), + child: SingleUse::new(Box::new(child)), + } + } +} + +enum TabsContent { + Building { + tabs: TP::Build, + }, + Complete { + tabs: TP, + }, + Running { + scope: WidgetPod>, + }, + Swapping, +} + +pub struct Tabs { + axis: Axis, + cross: CrossAxisAlignment, // Not sure if this should have another enum. Middle means nothing here + rotation: TabOrientation, + content: TabsContent, +} + +impl Tabs> { + pub fn new() -> Self { + Tabs::building(Vec::new()) + } +} + +impl Tabs { + fn of_content(content: TabsContent) -> Self { + Tabs { + axis: Axis::Horizontal, + cross: CrossAxisAlignment::Start, + rotation: TabOrientation::Standard, + content, + } + } + + pub fn for_policy(tabs: TP) -> Self { + Self::of_content(TabsContent::Complete { tabs }) + } + + pub fn building(tabs_from_data: TP::Build) -> Self + where + TP: AddTab, + { + Self::of_content(TabsContent::Building { + tabs: tabs_from_data, + }) + } + + pub fn with_axis(mut self, axis: Axis) -> Self { + self.axis = axis; + self + } + + pub fn with_rotation(mut self, rotation: TabOrientation) -> Self { + self.rotation = rotation; + self + } + + pub fn with_cross_axis_alignment(mut self, cross: CrossAxisAlignment) -> Self { + self.cross = cross; + self + } + + pub fn with_tab( + mut self, + name: impl Into, + child: impl Widget + 'static, + ) -> Tabs + where + TP: AddTab, + { + self.add_tab(name, child); + self + } + + pub fn add_tab(&mut self, name: impl Into, child: impl Widget + 'static) + where + TP: AddTab, + { + if let TabsContent::Building { tabs } = &mut self.content { + TP::add_tab(tabs, name, child) + } else { + log::warn!("Can't add static tabs to a running or complete tabs instance!") + } + } + + pub fn with_tabs(self, tabs: TabsFromD) -> Tabs { + Tabs { + axis: self.axis, + cross: self.cross, + rotation: self.rotation, + content: TabsContent::Complete { tabs }, + } + } + + pub fn make_scope(&self, tabs_from_data: TP) -> WidgetPod> { + let (bar, body) = ( + (TabBar::new(self.axis, self.cross, self.rotation), 0.0), + ( + TabsBody::new(self.axis) + .padding(5.) + .border(theme::BORDER_DARK, 0.5) + .expand(), + 1.0, + ), + ); + let mut layout: Flex> = Flex::for_axis(self.axis.cross()); + + if let CrossAxisAlignment::End = self.cross { + layout.add_flex_child(body.0, body.1); + layout.add_flex_child(bar.0, bar.1); + } else { + layout.add_flex_child(bar.0, bar.1); + layout.add_flex_child(body.0, body.1); + }; + + WidgetPod::new(Scope::new( + TabsScopePolicy::new(tabs_from_data, 0), + Box::new(layout), + )) + } +} + +impl Widget for Tabs { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut TP::Input, env: &Env) { + if let TabsContent::Running { scope } = &mut self.content { + scope.event(ctx, event, data, env); + } + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &TP::Input, + env: &Env, + ) { + if let LifeCycle::WidgetAdded = event { + let mut temp = TabsContent::Swapping; + std::mem::swap(&mut self.content, &mut temp); + + self.content = match temp { + TabsContent::Building { tabs } => { + ctx.children_changed(); + TabsContent::Running { + scope: self.make_scope(TP::build(tabs)), + } + } + TabsContent::Complete { tabs } => { + ctx.children_changed(); + TabsContent::Running { + scope: self.make_scope(tabs), + } + } + _ => temp, + }; + } + if let TabsContent::Running { scope } = &mut self.content { + scope.lifecycle(ctx, event, data, env) + } + } + + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &TP::Input, data: &TP::Input, env: &Env) { + if let TabsContent::Running { scope } = &mut self.content { + scope.update(ctx, data, env); + } + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &TP::Input, + env: &Env, + ) -> Size { + if let TabsContent::Running { scope } = &mut self.content { + let size = scope.layout(ctx, bc, data, env); + scope.set_layout_rect(ctx, data, env, Rect::from_origin_size(Point::ORIGIN, size)); + size + } else { + bc.min() + } + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &TP::Input, env: &Env) { + if let TabsContent::Running { scope } = &mut self.content { + scope.paint(ctx, data, env) + } + } +} diff --git a/druid/src/widget/widget_ext.rs b/druid/src/widget/widget_ext.rs index 0df71851ec..13c09a47a6 100644 --- a/druid/src/widget/widget_ext.rs +++ b/druid/src/widget/widget_ext.rs @@ -19,6 +19,7 @@ use super::{ Align, BackgroundBrush, Click, Container, Controller, ControllerHost, EnvScope, IdentityWrapper, Padding, Parse, SizedBox, WidgetId, }; +use crate::widget::Rotated; use crate::{Color, Data, Env, EventCtx, Insets, KeyOrValue, Lens, LensWrap, UnitPoint, Widget}; /// A trait that provides extra methods for combining `Widget`s. @@ -239,6 +240,12 @@ pub trait WidgetExt: Widget + Sized + 'static { IdentityWrapper::wrap(self, id) } + /// Rotate a widget by a number of quarter turns clockwise. + /// Mainly useful for text decorations. + fn rotate(self, quarter_turns: u8) -> Rotated { + Rotated::new(self, quarter_turns) + } + /// Wrap this widget in a `Box`. fn boxed(self) -> Box> { Box::new(self) From bfccbfa6fca0d88cd6dc6a3a02cf24bde830f39e Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 26 Aug 2020 23:08:39 +0100 Subject: [PATCH 02/12] Clippy and CHANGELOG.md fixes for tabs --- CHANGELOG.md | 6 +++--- druid/src/widget/tabs.rs | 24 +++++++++++++++--------- 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 36ff4746b8..a91d35aa46 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,8 @@ You can find its changes [documented below](#060---2020-06-01). - `request_update` in `EventCtx`. ([#1128] by [@raphlinus]) - `ExtEventSink`s can now be obtained from widget methods. ([#1152] by [@jneem]) - 'Scope' widget to allow encapsulation of reactive state. ([#1151] by [@rjwittams]) -- 'Rotated' widget to allow widgets to be rotated by an integral number of quarter turns ([#1159] by [@rjwittams]) -- 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1159] by [@rjwittams]) +- 'Rotated' widget to allow widgets to be rotated by an integral number of quarter turns ([#1160] by [@rjwittams]) +- 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1160] by [@rjwittams]) ### Changed @@ -405,7 +405,7 @@ Last release without a changelog :( [#1151]: https://github.com/linebender/druid/pull/1151 [#1152]: https://github.com/linebender/druid/pull/1152 [#1157]: https://github.com/linebender/druid/pull/1157 -[#1158]: https://github.com/linebender/druid/pull/1158 +[#1160]: https://github.com/linebender/druid/pull/1160 [Unreleased]: https://github.com/linebender/druid/compare/v0.6.0...master [0.6.0]: https://github.com/linebender/druid/compare/v0.5.0...v0.6.0 diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index ca6d6687d8..4ef1ad2392 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -116,6 +116,12 @@ impl StaticTabs { } } +impl Default for StaticTabs { + fn default() -> Self { + Self::new() + } +} + impl Data for StaticTabs { fn same(&self, _other: &Self) -> bool { // Changing the tabs after construction shouldn't be possible for static tabs @@ -478,11 +484,7 @@ impl TabsBody { &mut self.children, &data.policy, &data.inner, - |policy, key| { - policy - .tab_body(key.clone(), &data.inner) - .map(WidgetPod::new) - }, + |policy, key| policy.tab_body(key, &data.inner).map(WidgetPod::new), ) } @@ -650,10 +652,8 @@ impl Widget> for TabsBody { child.paint_raw(ctx, &data.inner, env); }) } - } else { - if let Some(ref mut child) = Self::child(&mut self.children, data.selected) { - child.paint_raw(ctx, &data.inner, env); - } + } else if let Some(ref mut child) = Self::child(&mut self.children, data.selected) { + child.paint_raw(ctx, &data.inner, env); } } } @@ -756,6 +756,12 @@ impl Tabs> { } } +impl Default for Tabs> { + fn default() -> Self { + Self::new() + } +} + impl Tabs { fn of_content(content: TabsContent) -> Self { Tabs { From 7967e055b3b382f0c395fc84f64d2ea7118174c9 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 26 Aug 2020 23:46:03 +0100 Subject: [PATCH 03/12] Extra level of Clippy fixes... --- druid/examples/tabs.rs | 4 +--- druid/src/widget/tabs.rs | 10 +++++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index 373f9226b5..30312a8a13 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -205,7 +205,5 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { .with_tab("Page 6", Label::new("Basic kind of stuff")) .with_tab("Page 7", Label::new("Basic kind of stuff")); - let col = Split::rows(main_tabs, dyn_tabs).draggable(true); - - col + Split::rows(main_tabs, dyn_tabs).draggable(true) } diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index 4ef1ad2392..10be5120de 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -833,7 +833,7 @@ impl Tabs { } pub fn make_scope(&self, tabs_from_data: TP) -> WidgetPod> { - let (bar, body) = ( + let (tabs_bar, tabs_body) = ( (TabBar::new(self.axis, self.cross, self.rotation), 0.0), ( TabsBody::new(self.axis) @@ -846,11 +846,11 @@ impl Tabs { let mut layout: Flex> = Flex::for_axis(self.axis.cross()); if let CrossAxisAlignment::End = self.cross { - layout.add_flex_child(body.0, body.1); - layout.add_flex_child(bar.0, bar.1); + layout.add_flex_child(tabs_body.0, tabs_body.1); + layout.add_flex_child(tabs_bar.0, tabs_bar.1); } else { - layout.add_flex_child(bar.0, bar.1); - layout.add_flex_child(body.0, body.1); + layout.add_flex_child(tabs_bar.0, tabs_bar.1); + layout.add_flex_child(tabs_body.0, tabs_body.1); }; WidgetPod::new(Scope::new( From 86ddd3217e80d0141ad3817b34f97456af2e61f1 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Fri, 28 Aug 2020 13:03:23 +0100 Subject: [PATCH 04/12] Add option to control the tabs transition Add in a call to request_paint to make the tabs transition animation work on Linux (and hopefully Windows) --- druid/examples/tabs.rs | 38 ++++++++++----- druid/src/widget/mod.rs | 2 +- druid/src/widget/tabs.rs | 99 +++++++++++++++++++++++++++------------- 3 files changed, 94 insertions(+), 45 deletions(-) diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index 30312a8a13..b02f34815c 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -1,9 +1,7 @@ use druid::im::Vector; -use druid::widget::{ - Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, - SizedBox, Split, TabInfo, TabOrientation, Tabs, TabsPolicy, ViewSwitcher, -}; +use druid::widget::{Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, SizedBox, Split, TabInfo, TabsOrientation, Tabs, TabsPolicy, ViewSwitcher, TabsTransition, TextBox}; use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc}; +use instant::Duration; #[derive(Data, Clone)] struct Basic {} @@ -47,7 +45,8 @@ impl Advanced { struct TabConfig { axis: Axis, cross: CrossAxisAlignment, - rotation: TabOrientation, + rotation: TabsOrientation, + transition: TabsTransition, } #[derive(Data, Clone, Lens)] @@ -55,6 +54,7 @@ struct AppState { tab_config: TabConfig, basic: Basic, advanced: Advanced, + text: String } pub fn main() { @@ -68,10 +68,12 @@ pub fn main() { tab_config: TabConfig { axis: Axis::Horizontal, cross: CrossAxisAlignment::Start, - rotation: TabOrientation::Standard, + rotation: TabsOrientation::Standard, + transition: Default::default(), }, basic: Basic {}, advanced: Advanced::new(2), + text: "Interesting placeholder".into() }; // start the application @@ -115,20 +117,30 @@ fn build_root_widget() -> impl Widget { .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(decor(Label::new("Tab rotation"))) .with_child(RadioGroup::new(vec![ - ("Standard", TabOrientation::Standard), - ("None", TabOrientation::Turns(0)), - ("Up", TabOrientation::Turns(3)), - ("Down", TabOrientation::Turns(1)), - ("Aussie", TabOrientation::Turns(2)), + ("Standard", TabsOrientation::Standard), + ("None", TabsOrientation::Turns(0)), + ("Up", TabsOrientation::Turns(3)), + ("Down", TabsOrientation::Turns(1)), + ("Aussie", TabsOrientation::Turns(2)), ])) .lens(AppState::tab_config.then(TabConfig::rotation)); + 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(rot_picker)) + .with_child( group(transit_picker) ) .with_flex_spacer(1.) .fix_width(200.0); @@ -181,6 +193,7 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { .with_axis(tab_config.axis) .with_cross_axis_alignment(tab_config.cross) .with_rotation(tab_config.rotation) + .with_transition(tab_config.transition) .lens(AppState::advanced); let adv = Flex::column() @@ -197,13 +210,14 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { .with_axis(tab_config.axis) .with_cross_axis_alignment(tab_config.cross) .with_rotation(tab_config.rotation) + .with_transition(tab_config.transition) .with_tab("Basic", Label::new("Basic kind of stuff")) .with_tab("Advanced", adv) .with_tab("Page 3", Label::new("Basic kind of stuff")) .with_tab("Page 4", Label::new("Basic kind of stuff")) .with_tab("Page 5", Label::new("Basic kind of stuff")) .with_tab("Page 6", Label::new("Basic kind of stuff")) - .with_tab("Page 7", Label::new("Basic kind of stuff")); + .with_tab("Page 7", TextBox::new().lens(AppState::text) ); Split::rows(main_tabs, dyn_tabs).draggable(true) } diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 8d2995d75c..9e102e4d59 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -83,7 +83,7 @@ pub use stepper::Stepper; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; -pub use tabs::{TabInfo, TabOrientation, Tabs, TabsPolicy, TabsState}; +pub use tabs::{TabInfo, TabsOrientation, TabsTransition, Tabs, TabsPolicy, TabsState}; pub use textbox::TextBox; pub use view_switcher::ViewSwitcher; #[doc(hidden)] diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index 10be5120de..e6515db4f3 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -29,13 +29,13 @@ use crate::{ theme, Affine, BoxConstraints, Data, Env, Event, EventCtx, Insets, LayoutCtx, Lens, LifeCycle, LifeCycleCtx, PaintCtx, Point, Rect, SingleUse, Size, UpdateCtx, Widget, WidgetExt, WidgetPod, }; +use instant::Duration; type TabsScope = Scope, Box>>>; type TabBodyPod = WidgetPod<::Input, ::BodyWidget>; type TabBarPod = WidgetPod, Box>>>; type TabIndex = usize; - -const MILLIS: u64 = 1_000_000; // Number of nanos +type Nanos = u64; // TODO: Make Duration Data? pub struct TabInfo { pub name: String, @@ -198,14 +198,14 @@ impl TabsState { pub struct TabBar { axis: Axis, cross: CrossAxisAlignment, - orientation: TabOrientation, + orientation: TabsOrientation, tabs: Vec<(TP::Key, TabBarPod)>, hot: Option, phantom_tp: PhantomData, } impl TabBar { - pub fn new(axis: Axis, cross: CrossAxisAlignment, orientation: TabOrientation) -> Self { + pub fn new(axis: Axis, cross: CrossAxisAlignment, orientation: TabsOrientation) -> Self { TabBar { axis, cross, @@ -397,29 +397,29 @@ impl Widget> for TabBar { } } -pub struct TabsTransition { +pub struct TabsTransitionState { previous_idx: TabIndex, current_time: u64, - length: u64, + duration: Nanos, increasing: bool, } -impl TabsTransition { - pub fn new(previous_idx: TabIndex, length: u64, increasing: bool) -> Self { - TabsTransition { +impl TabsTransitionState { + pub fn new(previous_idx: TabIndex, duration: Nanos, increasing: bool) -> Self { + TabsTransitionState { previous_idx, current_time: 0, - length, + duration, increasing, } } pub fn live(&self) -> bool { - self.current_time < self.length + self.current_time < self.duration } pub fn fraction(&self) -> f64 { - (self.current_time as f64) / (self.length as f64) + (self.current_time as f64) / (self.duration as f64) } pub fn previous_transform(&self, axis: Axis, main: f64) -> Affine { @@ -464,17 +464,19 @@ fn ensure_for_tabs( pub struct TabsBody { children: Vec<(TP::Key, Option>)>, - transition: Option, axis: Axis, + transition: TabsTransition, + transition_state: Option, phantom_tp: PhantomData, } impl TabsBody { - pub fn new(axis: Axis) -> TabsBody { + pub fn new(axis: Axis, transition: TabsTransition) -> TabsBody { TabsBody { children: vec![], - transition: None, axis, + transition, + transition_state: None, phantom_tp: Default::default(), } } @@ -566,13 +568,14 @@ impl Widget> for TabsBody { child.lifecycle(ctx, event, &data.inner, env); } - if let (Some(trans), LifeCycle::AnimFrame(interval)) = (&mut self.transition, event) { - trans.current_time += *interval; - if trans.live() { + if let (Some(t_state), LifeCycle::AnimFrame(interval)) = (&mut self.transition_state, event) { + t_state.current_time += *interval; + if t_state.live() { ctx.request_anim_frame(); } else { - self.transition = None; + self.transition_state = None; } + ctx.request_paint(); } } @@ -592,13 +595,12 @@ impl Widget> for TabsBody { }; if old_data.selected != data.selected { - self.transition = Some(TabsTransition::new( - old_data.selected, - 250 * MILLIS, - old_data.selected < data.selected, - )); + self.transition_state = self.transition.tab_changed(old_data.selected, data.selected); ctx.request_layout(); - ctx.request_anim_frame(); + + if self.transition_state.is_some() { + ctx.request_anim_frame(); + } } // Make sure to only pass events to initialised children @@ -633,7 +635,7 @@ impl Widget> for TabsBody { } fn paint(&mut self, ctx: &mut PaintCtx, data: &TabsState, env: &Env) { - if let Some(trans) = &self.transition { + if let Some(trans) = &self.transition_state { let axis = self.axis; let size = ctx.size(); let major = axis.major(size); @@ -687,12 +689,37 @@ impl ScopePolicy for TabsScopePolicy { } #[derive(Data, Copy, Clone, Debug, PartialOrd, PartialEq)] -pub enum TabOrientation { +pub enum TabsOrientation { Standard, Turns(u8), // These represent 90 degree rotations clockwise. } -impl TabOrientation { +#[derive(Data, Copy, Clone, Debug, PartialOrd, PartialEq)] +pub enum TabsTransition { + Instant, + Slide(Nanos) +} + +impl Default for TabsTransition{ + fn default() -> Self { + TabsTransition::Slide(Duration::from_millis(250).as_nanos() as Nanos) + } +} + +impl TabsTransition{ + fn tab_changed(&self, old: TabIndex, new: TabIndex)->Option{ + match self{ + TabsTransition::Instant=>None, + TabsTransition::Slide(dur)=>Some(TabsTransitionState::new( + old, + *dur, + old < new, + )) + } + } +} + +impl TabsOrientation { pub fn rotate_and_box + 'static, T: Data>( self, widget: W, @@ -746,7 +773,8 @@ enum TabsContent { pub struct Tabs { axis: Axis, cross: CrossAxisAlignment, // Not sure if this should have another enum. Middle means nothing here - rotation: TabOrientation, + rotation: TabsOrientation, + transition: TabsTransition, content: TabsContent, } @@ -767,7 +795,8 @@ impl Tabs { Tabs { axis: Axis::Horizontal, cross: CrossAxisAlignment::Start, - rotation: TabOrientation::Standard, + rotation: TabsOrientation::Standard, + transition: Default::default(), content, } } @@ -790,7 +819,7 @@ impl Tabs { self } - pub fn with_rotation(mut self, rotation: TabOrientation) -> Self { + pub fn with_rotation(mut self, rotation: TabsOrientation) -> Self { self.rotation = rotation; self } @@ -800,6 +829,11 @@ impl Tabs { self } + pub fn with_transition(mut self, transition: TabsTransition) -> Self { + self.transition = transition; + self + } + pub fn with_tab( mut self, name: impl Into, @@ -828,6 +862,7 @@ impl Tabs { axis: self.axis, cross: self.cross, rotation: self.rotation, + transition: self.transition, content: TabsContent::Complete { tabs }, } } @@ -836,7 +871,7 @@ impl Tabs { let (tabs_bar, tabs_body) = ( (TabBar::new(self.axis, self.cross, self.rotation), 0.0), ( - TabsBody::new(self.axis) + TabsBody::new(self.axis, self.transition) .padding(5.) .border(theme::BORDER_DARK, 0.5) .expand(), From 6ccedde451351b0c8f93044566f70910a613b462 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Fri, 28 Aug 2020 13:06:59 +0100 Subject: [PATCH 05/12] Don't expand the tab body, instead make the tab body always expand itself, and just draw its child in the origin. Seems to work ok with the animations. --- druid/src/widget/tabs.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index e6515db4f3..5756d67890 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -628,10 +628,8 @@ impl Widget> for TabsBody { let inner = &data.inner; let size = child.layout(ctx, bc, inner, env); child.set_layout_rect(ctx, inner, env, Rect::from_origin_size(Point::ORIGIN, size)); - size - } else { - bc.max() } + bc.max() } fn paint(&mut self, ctx: &mut PaintCtx, data: &TabsState, env: &Env) { @@ -873,8 +871,7 @@ impl Tabs { ( TabsBody::new(self.axis, self.transition) .padding(5.) - .border(theme::BORDER_DARK, 0.5) - .expand(), + .border(theme::BORDER_DARK, 0.5), 1.0, ), ); From ab52e7b8f6068e259077b027a04934e924c224f9 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Fri, 28 Aug 2020 13:09:35 +0100 Subject: [PATCH 06/12] Rustfmt and Clippy... --- druid/examples/tabs.rs | 27 +++++++++++++++++---------- druid/src/widget/mod.rs | 2 +- druid/src/widget/tabs.rs | 25 ++++++++++++------------- 3 files changed, 30 insertions(+), 24 deletions(-) diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index b02f34815c..89ab0ad36a 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -1,5 +1,9 @@ use druid::im::Vector; -use druid::widget::{Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, SizedBox, Split, TabInfo, TabsOrientation, Tabs, TabsPolicy, ViewSwitcher, TabsTransition, TextBox}; +use druid::widget::{ + Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, + SizedBox, Split, TabInfo, Tabs, TabsOrientation, TabsPolicy, TabsTransition, TextBox, + ViewSwitcher, +}; use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc}; use instant::Duration; @@ -54,7 +58,7 @@ struct AppState { tab_config: TabConfig, basic: Basic, advanced: Advanced, - text: String + text: String, } pub fn main() { @@ -73,7 +77,7 @@ pub fn main() { }, basic: Basic {}, advanced: Advanced::new(2), - text: "Interesting placeholder".into() + text: "Interesting placeholder".into(), }; // start the application @@ -126,13 +130,16 @@ fn build_root_widget() -> impl Widget { .lens(AppState::tab_config.then(TabConfig::rotation)); let transit_picker = Flex::column() - .cross_axis_alignment( CrossAxisAlignment::Start) - .with_child( decor(Label::new("Transition"))) - .with_child( RadioGroup::new( vec![ + .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)) + ( + "Slide", + TabsTransition::Slide(Duration::from_millis(250).as_nanos() as u64), + ), ])) - .lens(AppState::tab_config.then(TabConfig::transition)) ; + .lens(AppState::tab_config.then(TabConfig::transition)); let sidebar = Flex::column() .main_axis_alignment(MainAxisAlignment::Start) @@ -140,7 +147,7 @@ fn build_root_widget() -> impl Widget { .with_child(group(axis_picker)) .with_child(group(cross_picker)) .with_child(group(rot_picker)) - .with_child( group(transit_picker) ) + .with_child(group(transit_picker)) .with_flex_spacer(1.) .fix_width(200.0); @@ -217,7 +224,7 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { .with_tab("Page 4", Label::new("Basic kind of stuff")) .with_tab("Page 5", Label::new("Basic kind of stuff")) .with_tab("Page 6", Label::new("Basic kind of stuff")) - .with_tab("Page 7", TextBox::new().lens(AppState::text) ); + .with_tab("Page 7", TextBox::new().lens(AppState::text)); Split::rows(main_tabs, dyn_tabs).draggable(true) } diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 9e102e4d59..63d12e5e02 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -83,7 +83,7 @@ pub use stepper::Stepper; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; -pub use tabs::{TabInfo, TabsOrientation, TabsTransition, Tabs, TabsPolicy, TabsState}; +pub use tabs::{TabInfo, Tabs, TabsOrientation, TabsPolicy, TabsState, TabsTransition}; pub use textbox::TextBox; pub use view_switcher::ViewSwitcher; #[doc(hidden)] diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index 5756d67890..3eaa73cdd8 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -568,7 +568,8 @@ impl Widget> for TabsBody { child.lifecycle(ctx, event, &data.inner, env); } - if let (Some(t_state), LifeCycle::AnimFrame(interval)) = (&mut self.transition_state, event) { + if let (Some(t_state), LifeCycle::AnimFrame(interval)) = (&mut self.transition_state, event) + { t_state.current_time += *interval; if t_state.live() { ctx.request_anim_frame(); @@ -595,7 +596,9 @@ impl Widget> for TabsBody { }; if old_data.selected != data.selected { - self.transition_state = self.transition.tab_changed(old_data.selected, data.selected); + self.transition_state = self + .transition + .tab_changed(old_data.selected, data.selected); ctx.request_layout(); if self.transition_state.is_some() { @@ -695,24 +698,20 @@ pub enum TabsOrientation { #[derive(Data, Copy, Clone, Debug, PartialOrd, PartialEq)] pub enum TabsTransition { Instant, - Slide(Nanos) + Slide(Nanos), } -impl Default for TabsTransition{ +impl Default for TabsTransition { fn default() -> Self { TabsTransition::Slide(Duration::from_millis(250).as_nanos() as Nanos) } } -impl TabsTransition{ - fn tab_changed(&self, old: TabIndex, new: TabIndex)->Option{ - match self{ - TabsTransition::Instant=>None, - TabsTransition::Slide(dur)=>Some(TabsTransitionState::new( - old, - *dur, - old < new, - )) +impl TabsTransition { + fn tab_changed(self, old: TabIndex, new: TabIndex) -> Option { + match self { + TabsTransition::Instant => None, + TabsTransition::Slide(dur) => Some(TabsTransitionState::new(old, dur, old < new)), } } } From 5aad9cfe934fd41577e27820d2fabbc55e5db98c Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Wed, 9 Sep 2020 22:30:22 +0100 Subject: [PATCH 07/12] Fixup AnimationFrame move --- druid/src/widget/tabs.rs | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index 3eaa73cdd8..c7dc1ef02b 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -512,6 +512,7 @@ fn hidden_should_receive_event(evt: &Event) -> bool { Event::WindowConnected | Event::WindowSize(_) | Event::Timer(_) + | Event::AnimFrame(_) | Event::Command(_) | Event::Internal(_) => true, Event::MouseDown(_) @@ -529,7 +530,6 @@ fn hidden_should_receive_lifecycle(lc: &LifeCycle) -> bool { match lc { LifeCycle::WidgetAdded | LifeCycle::Internal(_) => true, LifeCycle::Size(_) - | LifeCycle::AnimFrame(_) | LifeCycle::HotChanged(_) | LifeCycle::FocusChanged(_) => false, } @@ -544,6 +544,17 @@ impl Widget> for TabsBody { } else if let Some(child) = self.active_child(data) { child.event(ctx, event, &mut data.inner, env); } + + if let (Some(t_state), Event::AnimFrame(interval)) = (&mut self.transition_state, event) + { + t_state.current_time += *interval; + if t_state.live() { + ctx.request_anim_frame(); + } else { + self.transition_state = None; + } + ctx.request_paint(); + } } fn lifecycle( @@ -568,16 +579,7 @@ impl Widget> for TabsBody { child.lifecycle(ctx, event, &data.inner, env); } - if let (Some(t_state), LifeCycle::AnimFrame(interval)) = (&mut self.transition_state, event) - { - t_state.current_time += *interval; - if t_state.live() { - ctx.request_anim_frame(); - } else { - self.transition_state = None; - } - ctx.request_paint(); - } + } fn update( From 028f1aba13c161b1f48de42b6c49881359d70e97 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Thu, 10 Sep 2020 15:50:04 +0100 Subject: [PATCH 08/12] Remove rotation and add docs. --- druid/examples/tabs.rs | 20 +--- druid/src/widget/flex.rs | 144 ++++++++++++---------- druid/src/widget/mod.rs | 4 +- druid/src/widget/rotated.rs | 111 ----------------- druid/src/widget/tabs.rs | 212 +++++++++++++++++---------------- druid/src/widget/widget_ext.rs | 7 -- 6 files changed, 190 insertions(+), 308 deletions(-) delete mode 100644 druid/src/widget/rotated.rs diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index 89ab0ad36a..ef4965fb60 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -1,8 +1,7 @@ use druid::im::Vector; use druid::widget::{ Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, - SizedBox, Split, TabInfo, Tabs, TabsOrientation, TabsPolicy, TabsTransition, TextBox, - ViewSwitcher, + SizedBox, Split, TabInfo, Tabs, TabsPolicy, TabsTransition, TextBox, ViewSwitcher, }; use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc}; use instant::Duration; @@ -49,7 +48,6 @@ impl Advanced { struct TabConfig { axis: Axis, cross: CrossAxisAlignment, - rotation: TabsOrientation, transition: TabsTransition, } @@ -72,7 +70,6 @@ pub fn main() { tab_config: TabConfig { axis: Axis::Horizontal, cross: CrossAxisAlignment::Start, - rotation: TabsOrientation::Standard, transition: Default::default(), }, basic: Basic {}, @@ -117,18 +114,6 @@ fn build_root_widget() -> impl Widget { ])) .lens(AppState::tab_config.then(TabConfig::cross)); - let rot_picker = Flex::column() - .cross_axis_alignment(CrossAxisAlignment::Start) - .with_child(decor(Label::new("Tab rotation"))) - .with_child(RadioGroup::new(vec![ - ("Standard", TabsOrientation::Standard), - ("None", TabsOrientation::Turns(0)), - ("Up", TabsOrientation::Turns(3)), - ("Down", TabsOrientation::Turns(1)), - ("Aussie", TabsOrientation::Turns(2)), - ])) - .lens(AppState::tab_config.then(TabConfig::rotation)); - let transit_picker = Flex::column() .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(decor(Label::new("Transition"))) @@ -146,7 +131,6 @@ fn build_root_widget() -> impl Widget { .cross_axis_alignment(CrossAxisAlignment::Start) .with_child(group(axis_picker)) .with_child(group(cross_picker)) - .with_child(group(rot_picker)) .with_child(group(transit_picker)) .with_flex_spacer(1.) .fix_width(200.0); @@ -199,7 +183,6 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { let dyn_tabs = Tabs::for_policy(NumberedTabs) .with_axis(tab_config.axis) .with_cross_axis_alignment(tab_config.cross) - .with_rotation(tab_config.rotation) .with_transition(tab_config.transition) .lens(AppState::advanced); @@ -216,7 +199,6 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { let main_tabs = Tabs::new() .with_axis(tab_config.axis) .with_cross_axis_alignment(tab_config.cross) - .with_rotation(tab_config.rotation) .with_transition(tab_config.transition) .with_tab("Basic", Label::new("Basic kind of stuff")) .with_tab("Advanced", adv) diff --git a/druid/src/widget/flex.rs b/druid/src/widget/flex.rs index 7548edcd5d..d25d85ce6b 100644 --- a/druid/src/widget/flex.rs +++ b/druid/src/widget/flex.rs @@ -195,12 +195,89 @@ pub struct FlexParams { alignment: Option, } +/// An axis in visual space. Most often used by widgets to describe +/// the direction in which they grow as their number of children increases. +/// Has some methods for manipulating geometry with respect to the axis. #[derive(Data, Debug, Clone, Copy, PartialEq)] pub enum Axis { + /// The x axis Horizontal, + /// The y axis Vertical, } +impl Axis { + /// Get the axis perpendicular to this one. + pub fn cross(self) -> Axis { + match self { + Axis::Horizontal => Axis::Vertical, + Axis::Vertical => Axis::Horizontal, + } + } + + /// Extract from the argument the magnitude along this axis + pub fn major(self, coords: Size) -> f64 { + match self { + Axis::Horizontal => coords.width, + Axis::Vertical => coords.height, + } + } + + /// Extract from the argument the magnitude along the perpendicular axis + pub fn minor(self, coords: Size) -> f64 { + self.cross().major(coords) + } + + /// Extract the extent of the argument in this axis as a pair. + pub fn major_span(self, rect: Rect) -> (f64, f64) { + match self { + Axis::Horizontal => (rect.x0, rect.x1), + Axis::Vertical => (rect.y0, rect.y1), + } + } + + /// Extract the extent of the argument in the minor axis as a pair. + pub fn minor_span(self, rect: Rect) -> (f64, f64) { + self.cross().major_span(rect) + } + + /// Extract the coordinate locating the argument with respect to this axis. + pub fn major_pos(self, pos: Point) -> f64 { + match self { + Axis::Horizontal => pos.x, + Axis::Vertical => pos.y, + } + } + + /// Extract the coordinate locating the argument with respect to the perpendicular axis. + pub fn minor_pos(self, pos: Point) -> f64 { + self.cross().major_pos(pos) + } + + /// Arrange the major and minor measurements with respect to this axis such that it forms + /// an (x, y) pair. + pub fn pack(self, major: f64, minor: f64) -> (f64, f64) { + match self { + Axis::Horizontal => (major, minor), + Axis::Vertical => (minor, major), + } + } + + /// Generate constraints with new values on the major axis. + fn constraints(self, bc: &BoxConstraints, min_major: f64, major: f64) -> BoxConstraints { + match self { + Axis::Horizontal => BoxConstraints::new( + Size::new(min_major, bc.min().height), + Size::new(major, bc.max().height), + ), + Axis::Vertical => BoxConstraints::new( + Size::new(bc.min().width, min_major), + Size::new(bc.max().width, major), + ), + } + } +} + /// The alignment of the widgets on the container's cross (or minor) axis. /// /// If a widget is smaller than the container on the minor axis, this determines @@ -278,73 +355,8 @@ impl ChildWidget { } } -impl Axis { - pub fn cross(self) -> Axis { - match self { - Axis::Horizontal => Axis::Vertical, - Axis::Vertical => Axis::Horizontal, - } - } - - pub(crate) fn major(self, coords: Size) -> f64 { - match self { - Axis::Horizontal => coords.width, - Axis::Vertical => coords.height, - } - } - - pub fn major_span(self, rect: &Rect) -> (f64, f64) { - match self { - Axis::Horizontal => (rect.x0, rect.x1), - Axis::Vertical => (rect.y0, rect.y1), - } - } - - pub fn minor_span(self, rect: &Rect) -> (f64, f64) { - self.cross().major_span(rect) - } - - pub fn major_pos(self, pos: Point) -> f64 { - match self { - Axis::Horizontal => pos.x, - Axis::Vertical => pos.y, - } - } - - pub fn minor_pos(self, pos: Point) -> f64 { - self.cross().major_pos(pos) - } - - pub(crate) fn minor(self, coords: Size) -> f64 { - match self { - Axis::Horizontal => coords.height, - Axis::Vertical => coords.width, - } - } - - pub(crate) fn pack(self, major: f64, minor: f64) -> (f64, f64) { - match self { - Axis::Horizontal => (major, minor), - Axis::Vertical => (minor, major), - } - } - - /// Generate constraints with new values on the major axis. - fn constraints(self, bc: &BoxConstraints, min_major: f64, major: f64) -> BoxConstraints { - match self { - Axis::Horizontal => BoxConstraints::new( - Size::new(min_major, bc.min().height), - Size::new(major, bc.max().height), - ), - Axis::Vertical => BoxConstraints::new( - Size::new(bc.min().width, min_major), - Size::new(bc.max().width, major), - ), - } - } -} - impl Flex { + /// Create a new Flex oriented along the provided axis. pub fn for_axis(axis: Axis) -> Self { Flex { direction: axis, diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 63d12e5e02..65ab3ff6d1 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -34,7 +34,6 @@ mod painter; mod parse; mod progress_bar; mod radio; -mod rotated; mod scope; mod scroll; mod sized_box; @@ -72,7 +71,6 @@ pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; pub use progress_bar::ProgressBar; pub use radio::{Radio, RadioGroup}; -pub use rotated::Rotated; pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; pub use scroll::Scroll; pub use sized_box::SizedBox; @@ -83,7 +81,7 @@ pub use stepper::Stepper; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; -pub use tabs::{TabInfo, Tabs, TabsOrientation, TabsPolicy, TabsState, TabsTransition}; +pub use tabs::{TabInfo, Tabs, TabsPolicy, TabsState, TabsTransition}; pub use textbox::TextBox; pub use view_switcher::ViewSwitcher; #[doc(hidden)] diff --git a/druid/src/widget/rotated.rs b/druid/src/widget/rotated.rs deleted file mode 100644 index d4b666ade2..0000000000 --- a/druid/src/widget/rotated.rs +++ /dev/null @@ -1,111 +0,0 @@ -use crate::event::Event::{MouseDown, MouseMove, MouseUp, Wheel}; -use crate::{ - Affine, BoxConstraints, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, MouseEvent, - PaintCtx, RenderContext, Size, UpdateCtx, Vec2, Widget, -}; -use std::f64::consts::PI; - -pub struct Rotated { - child: W, - quarter_turns: u8, - transforms: Option<(Affine, Affine)>, -} - -impl Rotated { - pub fn new(child: W, quarter_turns: u8) -> Self { - Rotated { - child, - quarter_turns, - transforms: None, - } - } -} - -impl Rotated { - fn flip(&self, size: Size) -> Size { - if self.quarter_turns % 2 == 0 { - size - } else { - Size::new(size.height, size.width) - } - } - - fn affine(&self, child_size: Size, my_size: Size) -> Affine { - let a = ((self.quarter_turns % 4) as f64) * PI / 2.0; - - Affine::translate(Vec2::new(my_size.width / 2., my_size.height / 2.)) - * Affine::rotate(a) - * Affine::translate(Vec2::new(-child_size.width / 2., -child_size.height / 2.)) - } - - fn translate_mouse_event(&self, inverse: Affine, me: &MouseEvent) -> MouseEvent { - let mut me = me.clone(); - me.pos = inverse * me.pos; - me - } -} - -impl> Widget for Rotated { - fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut T, env: &Env) { - if let Some((transform, inverse)) = self.transforms { - ctx.widget_state.invalid.transform_by(inverse); - match event { - MouseMove(me) => self.child.event( - ctx, - &Event::MouseMove(self.translate_mouse_event(inverse, me)), - data, - env, - ), - MouseDown(me) => self.child.event( - ctx, - &Event::MouseDown(self.translate_mouse_event(inverse, me)), - data, - env, - ), - MouseUp(me) => self.child.event( - ctx, - &Event::MouseUp(self.translate_mouse_event(inverse, me)), - data, - env, - ), - Wheel(me) => self.child.event( - ctx, - &Event::Wheel(self.translate_mouse_event(inverse, me)), - data, - env, - ), - _ => self.child.event(ctx, event, data, env), - } - ctx.widget_state.invalid.transform_by(transform); - } - } - - fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &T, env: &Env) { - self.child.lifecycle(ctx, event, data, env) - } - - fn update(&mut self, ctx: &mut UpdateCtx, old_data: &T, data: &T, env: &Env) { - self.child.update(ctx, old_data, data, env) - } - - fn layout(&mut self, ctx: &mut LayoutCtx, bc: &BoxConstraints, data: &T, env: &Env) -> Size { - let bc = BoxConstraints::new(self.flip(bc.min()), self.flip(bc.max())); - - let child_size = self.child.layout(ctx, &bc, data, env); - let flipped_size = self.flip(child_size); - let transform = self.affine(child_size, flipped_size); - self.transforms = Some((transform, transform.inverse())); - flipped_size - } - - fn paint(&mut self, ctx: &mut PaintCtx, data: &T, env: &Env) { - if let Some((transform, inverse)) = self.transforms { - ctx.region.transform_by(inverse); - ctx.with_save(|ctx| { - ctx.transform(transform); - self.child.paint(ctx, data, env) - }); - ctx.region.transform_by(transform); - } - } -} diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index c7dc1ef02b..77c42c8195 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -37,12 +37,17 @@ type TabBarPod = WidgetPod, Box>>>; type TabIndex = usize; type Nanos = u64; // TODO: Make Duration Data? +/// Information about a tab that may be used by the TabPolicy to +/// drive the visual presentation and behaviour of its label pub struct TabInfo { + /// Name of the tab pub name: String, + /// Should the user be able to close the tab? pub can_close: bool, } impl TabInfo { + /// Create a new TabInfo pub fn new(name: String, can_close: bool) -> Self { TabInfo { name, can_close } } @@ -51,9 +56,11 @@ impl TabInfo { /// A policy that determines how a Tabs instance derives its tabs from its app data pub trait TabsPolicy: Data { /// The identity of a tab. - type Key: Hash + Eq + Clone + Debug; + type Key: Hash + Eq + Clone; - /// The input data that will a) be used to derive the tab and b) also be the input data of all the child widgets. + /// The input data that will + /// a) be used to derive the tabs present + /// b) also be the input data for all of the child widgets. type Input: Data; /// The common type for all body widgets in this set of tabs. @@ -64,21 +71,25 @@ pub trait TabsPolicy: Data { /// Usually this would be Label type LabelWidget: Widget; - /// This policy whilst it is being built. - /// This is only be useful for implementations supporting AddTab, such as StaticTabs. - /// It can be filled in with () by other implementations until associated type defaults are stable + /// The information required to build up this policy. + /// This is to support policies where at least some tabs are provided up front during widget + /// construction. If the Build type implements the AddTab trait, the add_tab and with_tab + /// methods will be available on the Tabs instance to allow the + /// It can be filled in with () by implementations that do not require it. type Build; - /// Have the tabs changed. Expected to be cheap, eg pointer or numeric comparison. + /// Examining the input data, has the set of tabs present changed? + /// Expected to be cheap, eg pointer or numeric comparison. fn tabs_changed(&self, old_data: &Self::Input, data: &Self::Input) -> bool; - /// What are the current tabs set in order. + /// From the input data, return the new set of tabs fn tabs(&self, data: &Self::Input) -> Vec; - /// Presentation information for the tab + /// For this tab key, return the relevant tab information that will drive label construction fn tab_info(&self, key: Self::Key, data: &Self::Input) -> TabInfo; - /// Body widget for the tab + /// For this tab key, return the body widget, or None if the widget can not be returned. + /// This is to support the static tabs case, where ownership transfer is a one time operation. fn tab_body(&self, key: Self::Key, data: &Self::Input) -> Option; /// Label widget for the tab. @@ -91,16 +102,19 @@ pub trait TabsPolicy: Data { #[allow(unused_variables)] /// Construct an instance of this TabsFromData from its Build type. - /// This should only be implemented if supporting AddTab - possibly only StaticTabs needs to. + /// The main use case for this is StaticTabs, where the tabs are provided by the app developer up front. fn build(build: Self::Build) -> Self { - unimplemented!() + panic!("TabsPolicy::Build called on a policy that does not support incremental building") } + /// A default implementation for make label, if you do not wish to construct a custom widget. fn default_make_label(info: &TabInfo) -> Label { Label::new(info.name.clone()).with_text_color(theme::FOREGROUND_LIGHT) } } +/// A TabsPolicy that allows the app developer to provide static tabs up front when building the +/// widget. #[derive(Clone)] pub struct StaticTabs { // This needs be able to avoid cloning the widgets we are given - @@ -108,24 +122,17 @@ pub struct StaticTabs { tabs: Rc>>, } -impl StaticTabs { - pub fn new() -> Self { +impl Default for StaticTabs { + fn default() -> Self { StaticTabs { tabs: Rc::new(Vec::new()), } } } -impl Default for StaticTabs { - fn default() -> Self { - Self::new() - } -} - impl Data for StaticTabs { fn same(&self, _other: &Self) -> bool { // Changing the tabs after construction shouldn't be possible for static tabs - // It seems pointless to compare them true } } @@ -150,7 +157,11 @@ impl TabsPolicy for StaticTabs { } fn tab_body(&self, key: Self::Key, _data: &T) -> Option { - self.tabs[key].child.take() + // This only allows a static tab to be retrieved once, but as we never indicate that the tabs have changed, + // it should only be called once. + self.tabs + .get(key) + .and_then(|initial_tab| initial_tab.child.take()) } fn tab_label(&self, _key: Self::Key, info: &TabInfo, _data: &Self::Input) -> Self::LabelWidget { @@ -164,28 +175,35 @@ impl TabsPolicy for StaticTabs { } } +/// AddTabs is an extension to TabsPolicy. +/// If a policy implements AddTab, pub trait AddTab: TabsPolicy { + /// Add a tab to the build type. fn add_tab( - tabs: &mut Self::Build, + build: &mut Self::Build, name: impl Into, child: impl Widget + 'static, ); } impl AddTab for StaticTabs { - fn add_tab(tabs: &mut Self::Build, name: impl Into, child: impl Widget + 'static) { - tabs.push(InitialTab::new(name, child)) + fn add_tab(build: &mut Self::Build, name: impl Into, child: impl Widget + 'static) { + build.push(InitialTab::new(name, child)) } } +/// This is the current state of the tabs widget as a whole. +/// This expands the input data to include a policy that determines how tabs are derived, +/// and the index of the currently selected tab #[derive(Clone, Lens, Data)] pub struct TabsState { - pub inner: TP::Input, - pub selected: TabIndex, - pub policy: TP, + inner: TP::Input, + selected: TabIndex, + policy: TP, } impl TabsState { + /// Create a new TabsState pub fn new(inner: TP::Input, selected: usize, policy: TP) -> Self { TabsState { inner, @@ -197,19 +215,18 @@ impl TabsState { pub struct TabBar { axis: Axis, - cross: CrossAxisAlignment, - orientation: TabsOrientation, + cross_axis_alignment: CrossAxisAlignment, tabs: Vec<(TP::Key, TabBarPod)>, hot: Option, phantom_tp: PhantomData, } impl TabBar { - pub fn new(axis: Axis, cross: CrossAxisAlignment, orientation: TabsOrientation) -> Self { + /// Create a new TabBar widget. + pub fn new(axis: Axis, cross_axis_alignment: CrossAxisAlignment) -> Self { TabBar { axis, - cross, - orientation, + cross_axis_alignment, tabs: vec![], hot: None, phantom_tp: Default::default(), @@ -234,11 +251,6 @@ impl TabBar { } fn ensure_tabs(&mut self, data: &TabsState) { - // Borrow checker/ type inference fun - let (orientation, axis, cross) = (self.orientation, self.axis, self.cross); - let finish_row = |w| WidgetPod::new(orientation.rotate_and_box(w, axis, cross)); - let finish_label = |w| WidgetPod::new(orientation.rotate_and_box(w, axis, cross)); - ensure_for_tabs(&mut self.tabs, &data.policy, &data.inner, |policy, key| { let info = policy.tab_info(key.clone(), &data.inner); @@ -247,7 +259,7 @@ impl TabBar { .tab_label(key.clone(), &info, &data.inner) // TODO: Type inference fails here because both sides of the lens are dependent on // associated types of the policy. Needs changes to lens derivation to embed PhantomData of the (relevant?) type params) - // of the lensed types into the lens, to type inference has something to grab hold of + // of the lensed types into the lens, so type inference has something to grab hold of .lens::, tabs_state_derived_lenses::inner>(TabsState::::inner) .padding(Insets::uniform_xy(9., 5.)); @@ -259,9 +271,9 @@ impl TabBar { data.policy.close_tab(key.clone(), &mut data.inner); }, )); - finish_row(row) + WidgetPod::new(Box::new(row)) } else { - finish_label(label) + WidgetPod::new(Box::new(label)) } }); } @@ -363,7 +375,6 @@ impl Widget> for TabBar { fn paint(&mut self, ctx: &mut PaintCtx, data: &TabsState, env: &Env) { let hl_thickness = 2.; let highlight = env.get(theme::PRIMARY_LIGHT); - // TODO: allow reversing tab order (makes more sense in some rotations) for (idx, (_, tab)) in self.tabs.iter_mut().enumerate() { let rect = tab.layout_rect(); let rect = Rect::from_origin_size(rect.origin(), rect.size()); @@ -376,9 +387,9 @@ impl Widget> for TabBar { tab.paint(ctx, data, env); if idx == data.selected { - let (maj_near, maj_far) = self.axis.major_span(&rect); - let (min_near, min_far) = self.axis.minor_span(&rect); - let minor_pos = if let CrossAxisAlignment::End = self.cross { + let (maj_near, maj_far) = self.axis.major_span(rect); + let (min_near, min_far) = self.axis.minor_span(rect); + let minor_pos = if let CrossAxisAlignment::End = self.cross_axis_alignment { min_near + (hl_thickness / 2.) } else { min_far - (hl_thickness / 2.) @@ -529,9 +540,7 @@ fn hidden_should_receive_event(evt: &Event) -> bool { fn hidden_should_receive_lifecycle(lc: &LifeCycle) -> bool { match lc { LifeCycle::WidgetAdded | LifeCycle::Internal(_) => true, - LifeCycle::Size(_) - | LifeCycle::HotChanged(_) - | LifeCycle::FocusChanged(_) => false, + LifeCycle::Size(_) | LifeCycle::HotChanged(_) | LifeCycle::FocusChanged(_) => false, } } @@ -545,8 +554,7 @@ impl Widget> for TabsBody { child.event(ctx, event, &mut data.inner, env); } - if let (Some(t_state), Event::AnimFrame(interval)) = (&mut self.transition_state, event) - { + if let (Some(t_state), Event::AnimFrame(interval)) = (&mut self.transition_state, event) { t_state.current_time += *interval; if t_state.live() { ctx.request_anim_frame(); @@ -578,8 +586,6 @@ impl Widget> for TabsBody { // Pick which events go to all and which just to active child.lifecycle(ctx, event, &data.inner, env); } - - } fn update( @@ -629,11 +635,18 @@ impl Widget> for TabsBody { data: &TabsState, env: &Env, ) -> Size { + let inner = &data.inner; if let Some(ref mut child) = self.active_child(data) { - let inner = &data.inner; let size = child.layout(ctx, bc, inner, env); child.set_layout_rect(ctx, inner, env, Rect::from_origin_size(Point::ORIGIN, size)); } + if let Some(ref mut trans_state) = self.transition_state { + if let Some(child) = Self::child(&mut self.children, trans_state.previous_idx) { + let size = child.layout(ctx, bc, inner, env); + child.set_layout_rect(ctx, inner, env, Rect::from_origin_size(Point::ORIGIN, size)); + } + } + bc.max() } @@ -691,15 +704,12 @@ impl ScopePolicy for TabsScopePolicy { } } -#[derive(Data, Copy, Clone, Debug, PartialOrd, PartialEq)] -pub enum TabsOrientation { - Standard, - Turns(u8), // These represent 90 degree rotations clockwise. -} - +/// Determines whether the tabs will have a transition animation when a new tab is selected. #[derive(Data, Copy, Clone, Debug, PartialOrd, PartialEq)] pub enum TabsTransition { + /// Change tabs instantly with no animation Instant, + /// Slide tabs across in the appropriate direction. The argument is the duration in nanoseconds Slide(Nanos), } @@ -718,30 +728,6 @@ impl TabsTransition { } } -impl TabsOrientation { - pub fn rotate_and_box + 'static, T: Data>( - self, - widget: W, - axis: Axis, - cross: CrossAxisAlignment, - ) -> Box> { - let turns = match self { - Self::Standard => match (axis, cross) { - (Axis::Horizontal, _) => 0, - (Axis::Vertical, CrossAxisAlignment::Start) => 3, - (Axis::Vertical, _) => 1, - }, - Self::Turns(turns) => turns, - }; - - if turns == 0 { - Box::new(widget) - } else { - Box::new(widget.rotate(turns)) - } - } -} - pub struct InitialTab { name: String, child: SingleUse>>, // This is to avoid cloning provided tabs @@ -769,15 +755,40 @@ enum TabsContent { Swapping, } +/// A tabs widget. +/// +/// The tabs can be provided up front, using Tabs::new() and add_tab()/with_tab(). +/// +/// Or, the tabs can be derived from the input data by implementing TabsPolicy, and providing it to +/// Tabs::from_policy() +/// +/// ``` +/// use druid::widget::{Tabs, Label, WidgetExt}; +/// use druid::{Data, Lens}; +/// +/// #[derive(Data, Clone, Lens)] +/// struct AppState{ +/// name: String +/// } +/// +/// let tabs = Tabs::new() +/// .with_tab("Connection", Label::new("Connection information")) +/// .with_tab("Proxy", Label::new("Proxy settings")) +/// .lens(AppState::name); +/// +/// +/// ``` +/// pub struct Tabs { axis: Axis, cross: CrossAxisAlignment, // Not sure if this should have another enum. Middle means nothing here - rotation: TabsOrientation, transition: TabsTransition, content: TabsContent, } impl Tabs> { + /// Create a new Tabs widget, using the static tabs policy. + /// Use with_tab or add_tab to configure the set of tabs available. pub fn new() -> Self { Tabs::building(Vec::new()) } @@ -794,17 +805,21 @@ impl Tabs { Tabs { axis: Axis::Horizontal, cross: CrossAxisAlignment::Start, - rotation: TabsOrientation::Standard, transition: Default::default(), content, } } + /// Create a Tabs widget using the provided policy. + /// This is useful for tabs derived from data. pub fn for_policy(tabs: TP) -> Self { Self::of_content(TabsContent::Complete { tabs }) } - pub fn building(tabs_from_data: TP::Build) -> Self + // This could be public if there is a case for custom policies that support static tabs - ie the AddTab method. + // It seems very likely that the whole way we do dynamic vs static will change before that + // becomes an issue. + fn building(tabs_from_data: TP::Build) -> Self where TP: AddTab, { @@ -813,26 +828,27 @@ impl Tabs { }) } + /// Lay out the tab bar along the provided axis. pub fn with_axis(mut self, axis: Axis) -> Self { self.axis = axis; self } - pub fn with_rotation(mut self, rotation: TabsOrientation) -> Self { - self.rotation = rotation; - self - } - + /// Put the tab bar at the corresponding end of the cross axis. + /// Defaults to Start. Note that Middle has the same effect as Start. pub fn with_cross_axis_alignment(mut self, cross: CrossAxisAlignment) -> Self { self.cross = cross; self } + /// Use the provided transition when tabs change pub fn with_transition(mut self, transition: TabsTransition) -> Self { self.transition = transition; self } + /// Available when the policy implements AddTab - e.g StaticTabs. + /// Return this Tabs widget with the named tab added. pub fn with_tab( mut self, name: impl Into, @@ -845,6 +861,8 @@ impl Tabs { self } + /// Available when the policy implements AddTab - e.g StaticTabs. + /// Return this Tabs widget with the named tab added. pub fn add_tab(&mut self, name: impl Into, child: impl Widget + 'static) where TP: AddTab, @@ -856,19 +874,9 @@ impl Tabs { } } - pub fn with_tabs(self, tabs: TabsFromD) -> Tabs { - Tabs { - axis: self.axis, - cross: self.cross, - rotation: self.rotation, - transition: self.transition, - content: TabsContent::Complete { tabs }, - } - } - - pub fn make_scope(&self, tabs_from_data: TP) -> WidgetPod> { + fn make_scope(&self, tabs_from_data: TP) -> WidgetPod> { let (tabs_bar, tabs_body) = ( - (TabBar::new(self.axis, self.cross, self.rotation), 0.0), + (TabBar::new(self.axis, self.cross), 0.0), ( TabsBody::new(self.axis, self.transition) .padding(5.) diff --git a/druid/src/widget/widget_ext.rs b/druid/src/widget/widget_ext.rs index 6e7233ce30..c7d73f2505 100644 --- a/druid/src/widget/widget_ext.rs +++ b/druid/src/widget/widget_ext.rs @@ -19,7 +19,6 @@ use super::{ Align, BackgroundBrush, Click, Container, Controller, ControllerHost, EnvScope, IdentityWrapper, Padding, Parse, SizedBox, WidgetId, }; -use crate::widget::Rotated; use crate::{Color, Data, Env, EventCtx, Insets, KeyOrValue, Lens, LensWrap, UnitPoint, Widget}; /// A trait that provides extra methods for combining `Widget`s. @@ -241,12 +240,6 @@ pub trait WidgetExt: Widget + Sized + 'static { IdentityWrapper::wrap(self, id) } - /// Rotate a widget by a number of quarter turns clockwise. - /// Mainly useful for text decorations. - fn rotate(self, quarter_turns: u8) -> Rotated { - Rotated::new(self, quarter_turns) - } - /// Wrap this widget in a `Box`. fn boxed(self) -> Box> { Box::new(self) From 068c715f02b5701d5440fca117d25f36f6db5c65 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Thu, 10 Sep 2020 16:31:05 +0100 Subject: [PATCH 09/12] Remove some other extraneous changes. --- CHANGELOG.md | 1 - druid-shell/src/region.rs | 8 +------- druid/examples/tabs.rs | 16 +++++++++++++++- druid/examples/widget_gallery.rs | 8 ++++---- druid/src/lib.rs | 5 +++-- druid/src/widget/tabs.rs | 16 +++++++++++----- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c94f10c70b..4a206d2098 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,6 @@ You can find its changes [documented below](#060---2020-06-01). - `Menu` commands can now choose a custom target. ([#1185] by [@finnerale]) - `Movement::StartOfDocument`, `Movement::EndOfDocument`. ([#1092] by [@sysint64]) - `TextLayout` type simplifies drawing text ([#1182] by [@cmyr]) -- 'Rotated' widget to allow widgets to be rotated by an integral number of quarter turns ([#1160] by [@rjwittams]) - 'Tabs' widget allowing static and dynamic tabbed layouts. ([#1160] by [@rjwittams]) ### Changed diff --git a/druid-shell/src/region.rs b/druid-shell/src/region.rs index 56eea7c627..99fb24f0cf 100644 --- a/druid-shell/src/region.rs +++ b/druid-shell/src/region.rs @@ -1,4 +1,4 @@ -use kurbo::{Affine, BezPath, Rect, Shape, Vec2}; +use kurbo::{BezPath, Rect, Shape, Vec2}; /// A union of rectangles, useful for describing an area that needs to be repainted. #[derive(Clone, Debug)] @@ -81,12 +81,6 @@ impl Region { } self.rects.retain(|r| r.area() > 0.0) } - - pub fn transform_by(&mut self, transform: Affine) { - for rect in &mut self.rects { - *rect = transform.transform_rect_bbox(*rect) - } - } } impl std::ops::AddAssign for Region { diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index ef4965fb60..e703283a87 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -1,3 +1,17 @@ +// 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; use druid::widget::{ Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, @@ -7,7 +21,7 @@ use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetE use instant::Duration; #[derive(Data, Clone)] -struct Basic {} +struct Basic; #[derive(Data, Clone, Lens)] struct Advanced { diff --git a/druid/examples/widget_gallery.rs b/druid/examples/widget_gallery.rs index 9cd92593da..4cd4e0c119 100644 --- a/druid/examples/widget_gallery.rs +++ b/druid/examples/widget_gallery.rs @@ -23,7 +23,7 @@ use druid::{ AppLauncher, Color, Data, Lens, Rect, Widget, WidgetExt, WidgetPod, WindowDesc, }; -#[cfg(feature = "svg")] +#[cfg(not(target_arch = "wasm32"))] use druid::widget::{Svg, SvgData}; const XI_IMAGE: &[u8] = include_bytes!("assets/xi.image"); @@ -67,7 +67,7 @@ pub fn main() { } fn ui_builder() -> impl Widget { - #[cfg(feature = "svg")] + #[cfg(not(target_arch = "wasm32"))] let svg_example = label_widget( Svg::new( include_str!("./assets/tiger.svg") @@ -77,8 +77,8 @@ fn ui_builder() -> impl Widget { "Svg", ); - #[cfg(not(feature = "svg"))] - let svg_example = label_widget(Label::new("SVG not supported (yet)").center(), "Svg"); + #[cfg(target_arch = "wasm32")] + let svg_example = label_widget(Label::new("no SVG on wasm (yet)").center(), "Svg"); Scroll::new( SquaresGrid::new() diff --git a/druid/src/lib.rs b/druid/src/lib.rs index d668cc2c20..b8c48e024e 100644 --- a/druid/src/lib.rs +++ b/druid/src/lib.rs @@ -142,6 +142,9 @@ pub use druid_shell::{kurbo, piet}; #[doc(inline)] pub use im; +#[macro_use] +pub mod lens; + mod app; mod app_delegate; mod bloom; @@ -153,8 +156,6 @@ mod data; mod env; mod event; mod ext_event; -#[macro_use] -pub mod lens; mod localization; mod menu; mod mouse; diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index 77c42c8195..ea1f49a8b0 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -176,7 +176,8 @@ impl TabsPolicy for StaticTabs { } /// AddTabs is an extension to TabsPolicy. -/// If a policy implements AddTab, +/// If a policy implements AddTab, then the add_tab and with_tab methods will be available on +/// the Tabs instance. pub trait AddTab: TabsPolicy { /// Add a tab to the build type. fn add_tab( @@ -213,7 +214,8 @@ impl TabsState { } } -pub struct TabBar { +/// This widget is the tab bar. It contains widgets that when pressed switch the active tab. +struct TabBar { axis: Axis, cross_axis_alignment: CrossAxisAlignment, tabs: Vec<(TP::Key, TabBarPod)>, @@ -408,7 +410,7 @@ impl Widget> for TabBar { } } -pub struct TabsTransitionState { +struct TabsTransitionState { previous_idx: TabIndex, current_time: u64, duration: Nanos, @@ -473,7 +475,9 @@ fn ensure_for_tabs( existing_idx } -pub struct TabsBody { +/// This widget is the tabs body. It shows the active tab, keeps other tabs hidden, and can +/// animate transitions between them. +struct TabsBody { children: Vec<(TP::Key, Option>)>, axis: Axis, transition: TabsTransition, @@ -518,6 +522,7 @@ impl TabsBody { } } +/// Possibly should be moved to Event fn hidden_should_receive_event(evt: &Event) -> bool { match evt { Event::WindowConnected @@ -537,6 +542,7 @@ fn hidden_should_receive_event(evt: &Event) -> bool { } } +/// Possibly should be moved to event. fn hidden_should_receive_lifecycle(lc: &LifeCycle) -> bool { match lc { LifeCycle::WidgetAdded | LifeCycle::Internal(_) => true, @@ -677,7 +683,7 @@ impl Widget> for TabsBody { } // This only needs to exist to be able to give a reasonable type to the TabScope -pub struct TabsScopePolicy { +struct TabsScopePolicy { tabs_from_data: TP, selected: TabIndex, } From a352942861e4452528189295286d296dc90f0bad Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Mon, 28 Sep 2020 16:26:37 +0100 Subject: [PATCH 10/12] Fix event warnings --- druid/src/widget/tabs.rs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index ea1f49a8b0..c9d140f86b 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -542,7 +542,7 @@ fn hidden_should_receive_event(evt: &Event) -> bool { } } -/// Possibly should be moved to event. +/// Possibly should be moved to Lifecycle. fn hidden_should_receive_lifecycle(lc: &LifeCycle) -> bool { match lc { LifeCycle::WidgetAdded | LifeCycle::Internal(_) => true, @@ -642,16 +642,11 @@ impl Widget> for TabsBody { env: &Env, ) -> Size { let inner = &data.inner; - if let Some(ref mut child) = self.active_child(data) { + // Laying out all children so events can be delivered to them. + for child in self.child_pods() { let size = child.layout(ctx, bc, inner, env); child.set_layout_rect(ctx, inner, env, Rect::from_origin_size(Point::ORIGIN, size)); } - if let Some(ref mut trans_state) = self.transition_state { - if let Some(child) = Self::child(&mut self.children, trans_state.previous_idx) { - let size = child.layout(ctx, bc, inner, env); - child.set_layout_rect(ctx, inner, env, Rect::from_origin_size(Point::ORIGIN, size)); - } - } bc.max() } From 9826b580a2a2a49c2f3896619cb4628b141922d5 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Mon, 28 Sep 2020 18:25:11 +0100 Subject: [PATCH 11/12] Make the tab labels LabelText inc dynamic. Use that in examples. --- druid/examples/tabs.rs | 71 ++++++++++--------- druid/src/widget/flex.rs | 4 +- druid/src/widget/tabs.rs | 143 ++++++++++++++++++++++++--------------- 3 files changed, 131 insertions(+), 87 deletions(-) diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index e703283a87..3e8edf70f9 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -20,19 +20,16 @@ use druid::widget::{ use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc}; use instant::Duration; -#[derive(Data, Clone)] -struct Basic; - #[derive(Data, Clone, Lens)] -struct Advanced { +struct DynamicTabData { highest_tab: usize, removed_tabs: usize, tab_labels: Vector, } -impl Advanced { +impl DynamicTabData { fn new(highest_tab: usize) -> Self { - Advanced { + DynamicTabData { highest_tab, removed_tabs: 0, tab_labels: (1..=highest_tab).collect(), @@ -53,6 +50,7 @@ impl Advanced { } } + // This provides a key that will monotonically increase as interactions occur. fn tabs_key(&self) -> (usize, usize) { (self.highest_tab, self.removed_tabs) } @@ -68,9 +66,8 @@ struct TabConfig { #[derive(Data, Clone, Lens)] struct AppState { tab_config: TabConfig, - basic: Basic, - advanced: Advanced, - text: String, + advanced: DynamicTabData, + first_tab_name: String, } pub fn main() { @@ -86,9 +83,8 @@ pub fn main() { cross: CrossAxisAlignment::Start, transition: Default::default(), }, - basic: Basic {}, - advanced: Advanced::new(2), - text: "Interesting placeholder".into(), + first_tab_name: "First tab".into(), + advanced: DynamicTabData::new(2), }; // start the application @@ -162,33 +158,38 @@ struct NumberedTabs; impl TabsPolicy for NumberedTabs { type Key = usize; type Build = (); - type Input = Advanced; - type LabelWidget = Label; - type BodyWidget = Label; + type Input = DynamicTabData; + type LabelWidget = Label; + type BodyWidget = Label; - fn tabs_changed(&self, old_data: &Advanced, data: &Advanced) -> bool { + fn tabs_changed(&self, old_data: &DynamicTabData, data: &DynamicTabData) -> bool { old_data.tabs_key() != data.tabs_key() } - fn tabs(&self, data: &Advanced) -> Vec { + fn tabs(&self, data: &DynamicTabData) -> Vec { data.tab_labels.iter().copied().collect() } - fn tab_info(&self, key: Self::Key, _data: &Advanced) -> TabInfo { + fn tab_info(&self, key: Self::Key, _data: &DynamicTabData) -> TabInfo { TabInfo::new(format!("Tab {:?}", key), true) } - fn tab_body(&self, key: Self::Key, _data: &Advanced) -> Option> { - Some(Label::new(format!("Dynamic tab body {:?}", key))) + fn tab_body(&self, key: Self::Key, _data: &DynamicTabData) -> Label { + Label::new(format!("Dynamic tab body {:?}", key)) } - fn close_tab(&self, key: Self::Key, data: &mut Advanced) { + 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, _data: &Self::Input) -> Self::LabelWidget { + fn tab_label( + &self, + _key: Self::Key, + info: TabInfo, + _data: &Self::Input, + ) -> Self::LabelWidget { Self::default_make_label(info) } } @@ -200,27 +201,33 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { .with_transition(tab_config.transition) .lens(AppState::advanced); - let adv = Flex::column() + 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 Advanced, _e| d.add_tab())) - .with_child(Label::new(|adv: &Advanced, _e: &Env| { + .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("Basic", Label::new("Basic kind of stuff")) - .with_tab("Advanced", adv) - .with_tab("Page 3", Label::new("Basic kind of stuff")) - .with_tab("Page 4", Label::new("Basic kind of stuff")) - .with_tab("Page 5", Label::new("Basic kind of stuff")) - .with_tab("Page 6", Label::new("Basic kind of stuff")) - .with_tab("Page 7", TextBox::new().lens(AppState::text)); + .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) } diff --git a/druid/src/widget/flex.rs b/druid/src/widget/flex.rs index 792e9e1b08..a495fcfe02 100644 --- a/druid/src/widget/flex.rs +++ b/druid/src/widget/flex.rs @@ -195,7 +195,9 @@ pub struct FlexParams { alignment: Option, } -/// An axis in visual space. Most often used by widgets to describe +/// An axis in visual space. +/// +/// Most often used by widgets to describe /// the direction in which they grow as their number of children increases. /// Has some methods for manipulating geometry with respect to the axis. #[derive(Data, Debug, Clone, Copy, PartialEq)] diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index c9d140f86b..28c85ffeb7 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -13,8 +13,8 @@ // limitations under the License. //! A widget that can switch between one of many views, hiding the inactive ones. -//! +use instant::Duration; use std::collections::HashMap; use std::fmt::Debug; use std::hash::Hash; @@ -23,44 +23,46 @@ use std::rc::Rc; use crate::kurbo::Line; use crate::piet::RenderContext; +use crate::widget::prelude::*; -use crate::widget::{Axis, CrossAxisAlignment, Flex, Label, LensScopeTransfer, Scope, ScopePolicy}; -use crate::{ - theme, Affine, BoxConstraints, Data, Env, Event, EventCtx, Insets, LayoutCtx, Lens, LifeCycle, - LifeCycleCtx, PaintCtx, Point, Rect, SingleUse, Size, UpdateCtx, Widget, WidgetExt, WidgetPod, +use crate::widget::{ + Axis, CrossAxisAlignment, Flex, Label, LabelText, LensScopeTransfer, Scope, ScopePolicy, }; -use instant::Duration; +use crate::{theme, Affine, Data, Insets, Lens, Point, Rect, SingleUse, WidgetExt, WidgetPod}; type TabsScope = Scope, Box>>>; type TabBodyPod = WidgetPod<::Input, ::BodyWidget>; type TabBarPod = WidgetPod, Box>>>; type TabIndex = usize; -type Nanos = u64; // TODO: Make Duration Data? +type Nanos = u64; /// Information about a tab that may be used by the TabPolicy to /// drive the visual presentation and behaviour of its label -pub struct TabInfo { +pub struct TabInfo { /// Name of the tab - pub name: String, + pub name: LabelText, /// Should the user be able to close the tab? pub can_close: bool, } -impl TabInfo { +impl TabInfo { /// Create a new TabInfo - pub fn new(name: String, can_close: bool) -> Self { - TabInfo { name, can_close } + pub fn new(name: impl Into>, can_close: bool) -> Self { + TabInfo { + name: name.into(), + can_close, + } } } -/// A policy that determines how a Tabs instance derives its tabs from its app data +/// A policy that determines how a Tabs instance derives its tabs from its app data. pub trait TabsPolicy: Data { /// The identity of a tab. type Key: Hash + Eq + Clone; - /// The input data that will - /// a) be used to derive the tabs present - /// b) also be the input data for all of the child widgets. + /// The input data that will: + /// a) be used to determine the tabs present + /// b) be the input data for all of the child widgets. type Input: Data; /// The common type for all body widgets in this set of tabs. @@ -86,15 +88,19 @@ pub trait TabsPolicy: Data { fn tabs(&self, data: &Self::Input) -> Vec; /// For this tab key, return the relevant tab information that will drive label construction - fn tab_info(&self, key: Self::Key, data: &Self::Input) -> TabInfo; + fn tab_info(&self, key: Self::Key, data: &Self::Input) -> TabInfo; - /// For this tab key, return the body widget, or None if the widget can not be returned. - /// This is to support the static tabs case, where ownership transfer is a one time operation. - fn tab_body(&self, key: Self::Key, data: &Self::Input) -> Option; + /// For this tab key, return the body widget + fn tab_body(&self, key: Self::Key, data: &Self::Input) -> Self::BodyWidget; /// Label widget for the tab. /// Usually implemented with a call to default_make_label ( can't default here because Self::LabelWidget isn't determined) - fn tab_label(&self, key: Self::Key, info: &TabInfo, data: &Self::Input) -> Self::LabelWidget; + fn tab_label( + &self, + key: Self::Key, + info: TabInfo, + data: &Self::Input, + ) -> Self::LabelWidget; /// Change the data to reflect the user requesting to close a tab. #[allow(unused_variables)] @@ -108,8 +114,8 @@ pub trait TabsPolicy: Data { } /// A default implementation for make label, if you do not wish to construct a custom widget. - fn default_make_label(info: &TabInfo) -> Label { - Label::new(info.name.clone()).with_text_color(theme::FOREGROUND_LIGHT) + fn default_make_label(info: TabInfo) -> Label { + Label::new(info.name).with_text_color(theme::FOREGROUND_LIGHT) } } @@ -152,19 +158,35 @@ impl TabsPolicy for StaticTabs { (0..self.tabs.len()).collect() } - fn tab_info(&self, key: Self::Key, _data: &T) -> TabInfo { - TabInfo::new(self.tabs[key].name.clone(), false) + fn tab_info(&self, key: Self::Key, _data: &T) -> TabInfo { + // This only allows a static tabs label to be retrieved once, + // but as we never indicate that the tabs have changed, + // it should only be called once per key. + TabInfo::new( + self.tabs[key] + .name + .take() + .expect("StaticTabs LabelText can only be retrieved once"), + false, + ) } - fn tab_body(&self, key: Self::Key, _data: &T) -> Option { - // This only allows a static tab to be retrieved once, but as we never indicate that the tabs have changed, - // it should only be called once. + fn tab_body(&self, key: Self::Key, _data: &T) -> Self::BodyWidget { + // This only allows a static tab to be retrieved once, + // but as we never indicate that the tabs have changed, + // it should only be called once per key. self.tabs .get(key) .and_then(|initial_tab| initial_tab.child.take()) + .expect("StaticTabs body widget can only be retrieved once") } - fn tab_label(&self, _key: Self::Key, info: &TabInfo, _data: &Self::Input) -> Self::LabelWidget { + fn tab_label( + &self, + _key: Self::Key, + info: TabInfo, + _data: &Self::Input, + ) -> Self::LabelWidget { Self::default_make_label(info) } @@ -182,13 +204,17 @@ pub trait AddTab: TabsPolicy { /// Add a tab to the build type. fn add_tab( build: &mut Self::Build, - name: impl Into, + name: impl Into>, child: impl Widget + 'static, ); } impl AddTab for StaticTabs { - fn add_tab(build: &mut Self::Build, name: impl Into, child: impl Widget + 'static) { + fn add_tab( + build: &mut Self::Build, + name: impl Into>, + child: impl Widget + 'static, + ) { build.push(InitialTab::new(name, child)) } } @@ -225,7 +251,7 @@ struct TabBar { impl TabBar { /// Create a new TabBar widget. - pub fn new(axis: Axis, cross_axis_alignment: CrossAxisAlignment) -> Self { + fn new(axis: Axis, cross_axis_alignment: CrossAxisAlignment) -> Self { TabBar { axis, cross_axis_alignment, @@ -256,16 +282,18 @@ impl TabBar { ensure_for_tabs(&mut self.tabs, &data.policy, &data.inner, |policy, key| { let info = policy.tab_info(key.clone(), &data.inner); + let can_close = info.can_close; + let label = data .policy - .tab_label(key.clone(), &info, &data.inner) + .tab_label(key.clone(), info, &data.inner) // TODO: Type inference fails here because both sides of the lens are dependent on // associated types of the policy. Needs changes to lens derivation to embed PhantomData of the (relevant?) type params) // of the lensed types into the lens, so type inference has something to grab hold of .lens::, tabs_state_derived_lenses::inner>(TabsState::::inner) .padding(Insets::uniform_xy(9., 5.)); - if info.can_close { + if can_close { let row = Flex::row() .with_child(label) .with_child(Label::new("ⓧ").on_click( @@ -331,8 +359,12 @@ impl Widget> for TabBar { ctx: &mut UpdateCtx, old_data: &TabsState, data: &TabsState, - _env: &Env, + env: &Env, ) { + for (_, tab) in self.tabs.iter_mut() { + tab.update(ctx, data, env) + } + if data.policy.tabs_changed(&old_data.inner, &data.inner) { self.ensure_tabs(data); ctx.children_changed(); @@ -418,7 +450,7 @@ struct TabsTransitionState { } impl TabsTransitionState { - pub fn new(previous_idx: TabIndex, duration: Nanos, increasing: bool) -> Self { + fn new(previous_idx: TabIndex, duration: Nanos, increasing: bool) -> Self { TabsTransitionState { previous_idx, current_time: 0, @@ -427,15 +459,15 @@ impl TabsTransitionState { } } - pub fn live(&self) -> bool { + fn live(&self) -> bool { self.current_time < self.duration } - pub fn fraction(&self) -> f64 { + fn fraction(&self) -> f64 { (self.current_time as f64) / (self.duration as f64) } - pub fn previous_transform(&self, axis: Axis, main: f64) -> Affine { + fn previous_transform(&self, axis: Axis, main: f64) -> Affine { let x = if self.increasing { -main * self.fraction() } else { @@ -444,7 +476,7 @@ impl TabsTransitionState { Affine::translate(axis.pack(x, 0.)) } - pub fn selected_transform(&self, axis: Axis, main: f64) -> Affine { + fn selected_transform(&self, axis: Axis, main: f64) -> Affine { let x = if self.increasing { main * (1.0 - self.fraction()) } else { @@ -478,7 +510,7 @@ fn ensure_for_tabs( /// This widget is the tabs body. It shows the active tab, keeps other tabs hidden, and can /// animate transitions between them. struct TabsBody { - children: Vec<(TP::Key, Option>)>, + children: Vec<(TP::Key, TabBodyPod)>, axis: Axis, transition: TabsTransition, transition_state: Option, @@ -486,7 +518,7 @@ struct TabsBody { } impl TabsBody { - pub fn new(axis: Axis, transition: TabsTransition) -> TabsBody { + fn new(axis: Axis, transition: TabsTransition) -> TabsBody { TabsBody { children: vec![], axis, @@ -501,7 +533,7 @@ impl TabsBody { &mut self.children, &data.policy, &data.inner, - |policy, key| policy.tab_body(key, &data.inner).map(WidgetPod::new), + |policy, key| WidgetPod::new(policy.tab_body(key, &data.inner)), ) } @@ -511,14 +543,14 @@ impl TabsBody { // Doesn't take self to allow separate borrowing fn child( - children: &mut Vec<(TP::Key, Option>)>, + children: &mut Vec<(TP::Key, TabBodyPod)>, idx: usize, ) -> Option<&mut TabBodyPod> { - children.get_mut(idx).and_then(|x| x.1.as_mut()) + children.get_mut(idx).map(|x| &mut x.1) } fn child_pods(&mut self) -> impl Iterator> { - self.children.iter_mut().flat_map(|x| x.1.as_mut()) + self.children.iter_mut().map(|x| &mut x.1) } } @@ -684,7 +716,7 @@ struct TabsScopePolicy { } impl TabsScopePolicy { - pub fn new(tabs_from_data: TP, selected: TabIndex) -> Self { + fn new(tabs_from_data: TP, selected: TabIndex) -> Self { Self { tabs_from_data, selected, @@ -730,15 +762,15 @@ impl TabsTransition { } pub struct InitialTab { - name: String, + name: SingleUse>, // This is to avoid cloning provided label texts child: SingleUse>>, // This is to avoid cloning provided tabs } impl InitialTab { - pub fn new(name: impl Into, child: impl Widget + 'static) -> Self { + fn new(name: impl Into>, child: impl Widget + 'static) -> Self { InitialTab { - name: name.into(), - child: SingleUse::new(Box::new(child)), + name: SingleUse::new(name.into()), + child: SingleUse::new(child.boxed()), } } } @@ -852,7 +884,7 @@ impl Tabs { /// Return this Tabs widget with the named tab added. pub fn with_tab( mut self, - name: impl Into, + name: impl Into>, child: impl Widget + 'static, ) -> Tabs where @@ -864,8 +896,11 @@ impl Tabs { /// Available when the policy implements AddTab - e.g StaticTabs. /// Return this Tabs widget with the named tab added. - pub fn add_tab(&mut self, name: impl Into, child: impl Widget + 'static) - where + pub fn add_tab( + &mut self, + name: impl Into>, + child: impl Widget + 'static, + ) where TP: AddTab, { if let TabsContent::Building { tabs } = &mut self.content { From cc29a72939562b47cbc65a0de034727cac81b847 Mon Sep 17 00:00:00 2001 From: Robert Wittams Date: Mon, 28 Sep 2020 22:10:32 +0100 Subject: [PATCH 12/12] Add TabsEdge enum. Change layout code in tabs example. --- druid/examples/tabs.rs | 88 +++++++++++++++++++++------------------- druid/src/widget/mod.rs | 2 +- druid/src/widget/tabs.rs | 49 +++++++++++++--------- 3 files changed, 78 insertions(+), 61 deletions(-) diff --git a/druid/examples/tabs.rs b/druid/examples/tabs.rs index 3e8edf70f9..a3aada4d05 100644 --- a/druid/examples/tabs.rs +++ b/druid/examples/tabs.rs @@ -14,10 +14,10 @@ use druid::im::Vector; use druid::widget::{ - Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, Padding, RadioGroup, - SizedBox, Split, TabInfo, Tabs, TabsPolicy, TabsTransition, TextBox, ViewSwitcher, + Axis, Button, CrossAxisAlignment, Flex, Label, MainAxisAlignment, RadioGroup, Split, TabInfo, + Tabs, TabsEdge, TabsPolicy, TabsTransition, TextBox, ViewSwitcher, }; -use druid::{theme, AppLauncher, Color, Data, Env, Lens, LensExt, Widget, WidgetExt, WindowDesc}; +use druid::{theme, AppLauncher, Color, Data, Env, Lens, Widget, WidgetExt, WindowDesc}; use instant::Duration; #[derive(Data, Clone, Lens)] @@ -59,7 +59,7 @@ impl DynamicTabData { #[derive(Data, Clone, Lens)] struct TabConfig { axis: Axis, - cross: CrossAxisAlignment, + edge: TabsEdge, transition: TabsTransition, } @@ -80,7 +80,7 @@ pub fn main() { let initial_state = AppState { tab_config: TabConfig { axis: Axis::Horizontal, - cross: CrossAxisAlignment::Start, + edge: TabsEdge::Leading, transition: Default::default(), }, first_tab_name: "First tab".into(), @@ -95,55 +95,61 @@ pub fn main() { } fn build_root_widget() -> impl Widget { - fn decor(label: Label) -> SizedBox { - label - .padding(5.) - .background(theme::PLACEHOLDER_COLOR) - .expand_width() + fn group + 'static>(text: &str, w: W) -> impl Widget { + Flex::column() + .cross_axis_alignment(CrossAxisAlignment::Start) + .with_child( + Label::new(text) + .background(theme::PLACEHOLDER_COLOR) + .expand_width(), + ) + .with_default_spacer() + .with_child(w) + .with_default_spacer() + .border(Color::WHITE, 0.5) } - fn group + 'static>(w: W) -> Padding { - 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![ + let axis_picker = group( + "Tab bar axis", + RadioGroup::new(vec![ ("Horizontal", Axis::Horizontal), ("Vertical", Axis::Vertical), - ])) - .lens(AppState::tab_config.then(TabConfig::axis)); + ]) + .lens(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![ + let cross_picker = group( + "Tab bar edge", + RadioGroup::new(vec![ + ("Leading", TabsEdge::Leading), + ("Trailing", TabsEdge::Trailing), + ]) + .lens(TabConfig::edge), + ); + + let transit_picker = group( + "Transition", + RadioGroup::new(vec![ ("Instant", TabsTransition::Instant), ( "Slide", TabsTransition::Slide(Duration::from_millis(250).as_nanos() as u64), ), - ])) - .lens(AppState::tab_config.then(TabConfig::transition)); + ]) + .lens(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_child(axis_picker) + .with_default_spacer() + .with_child(cross_picker) + .with_default_spacer() + .with_child(transit_picker) .with_flex_spacer(1.) - .fix_width(200.0); + .fix_width(200.0) + .lens(AppState::tab_config); let vs = ViewSwitcher::new( |app_s: &AppState, _| app_s.tab_config.clone(), @@ -197,7 +203,7 @@ impl TabsPolicy for NumberedTabs { fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { let dyn_tabs = Tabs::for_policy(NumberedTabs) .with_axis(tab_config.axis) - .with_cross_axis_alignment(tab_config.cross) + .with_edge(tab_config.edge) .with_transition(tab_config.transition) .lens(AppState::advanced); @@ -217,7 +223,7 @@ fn build_tab_widget(tab_config: &TabConfig) -> impl Widget { let main_tabs = Tabs::new() .with_axis(tab_config.axis) - .with_cross_axis_alignment(tab_config.cross) + .with_edge(tab_config.edge) .with_transition(tab_config.transition) .with_tab( |app_state: &AppState, _: &Env| app_state.first_tab_name.to_string(), diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 0546902e93..da41536c8a 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -83,7 +83,7 @@ pub use stepper::Stepper; #[cfg(feature = "svg")] pub use svg::{Svg, SvgData}; pub use switch::Switch; -pub use tabs::{TabInfo, Tabs, TabsPolicy, TabsState, TabsTransition}; +pub use tabs::{TabInfo, Tabs, TabsEdge, TabsPolicy, TabsState, TabsTransition}; pub use textbox::TextBox; pub use view_switcher::ViewSwitcher; #[doc(hidden)] diff --git a/druid/src/widget/tabs.rs b/druid/src/widget/tabs.rs index 28c85ffeb7..2ea2dca213 100644 --- a/druid/src/widget/tabs.rs +++ b/druid/src/widget/tabs.rs @@ -25,9 +25,7 @@ use crate::kurbo::Line; use crate::piet::RenderContext; use crate::widget::prelude::*; -use crate::widget::{ - Axis, CrossAxisAlignment, Flex, Label, LabelText, LensScopeTransfer, Scope, ScopePolicy, -}; +use crate::widget::{Axis, Flex, Label, LabelText, LensScopeTransfer, Scope, ScopePolicy}; use crate::{theme, Affine, Data, Insets, Lens, Point, Rect, SingleUse, WidgetExt, WidgetPod}; type TabsScope = Scope, Box>>>; @@ -243,7 +241,7 @@ impl TabsState { /// This widget is the tab bar. It contains widgets that when pressed switch the active tab. struct TabBar { axis: Axis, - cross_axis_alignment: CrossAxisAlignment, + edge: TabsEdge, tabs: Vec<(TP::Key, TabBarPod)>, hot: Option, phantom_tp: PhantomData, @@ -251,10 +249,10 @@ struct TabBar { impl TabBar { /// Create a new TabBar widget. - fn new(axis: Axis, cross_axis_alignment: CrossAxisAlignment) -> Self { + fn new(axis: Axis, edge: TabsEdge) -> Self { TabBar { axis, - cross_axis_alignment, + edge, tabs: vec![], hot: None, phantom_tp: Default::default(), @@ -423,7 +421,7 @@ impl Widget> for TabBar { if idx == data.selected { let (maj_near, maj_far) = self.axis.major_span(rect); let (min_near, min_far) = self.axis.minor_span(rect); - let minor_pos = if let CrossAxisAlignment::End = self.cross_axis_alignment { + let minor_pos = if let TabsEdge::Trailing = self.edge { min_near + (hl_thickness / 2.) } else { min_far - (hl_thickness / 2.) @@ -761,6 +759,21 @@ impl TabsTransition { } } +/// Determines where the tab bar should be placed relative to the cross axis +#[derive(Debug, Copy, Clone, PartialEq, Data)] +pub enum TabsEdge { + /// For horizontal tabs, top. For vertical tabs, left. + Leading, + /// For horizontal tabs, bottom. For vertical tabs, right. + Trailing, +} + +impl Default for TabsEdge { + fn default() -> Self { + Self::Leading + } +} + pub struct InitialTab { name: SingleUse>, // This is to avoid cloning provided label texts child: SingleUse>>, // This is to avoid cloning provided tabs @@ -814,7 +827,7 @@ enum TabsContent { /// pub struct Tabs { axis: Axis, - cross: CrossAxisAlignment, // Not sure if this should have another enum. Middle means nothing here + edge: TabsEdge, transition: TabsTransition, content: TabsContent, } @@ -837,7 +850,7 @@ impl Tabs { fn of_content(content: TabsContent) -> Self { Tabs { axis: Axis::Horizontal, - cross: CrossAxisAlignment::Start, + edge: Default::default(), transition: Default::default(), content, } @@ -867,10 +880,9 @@ impl Tabs { self } - /// Put the tab bar at the corresponding end of the cross axis. - /// Defaults to Start. Note that Middle has the same effect as Start. - pub fn with_cross_axis_alignment(mut self, cross: CrossAxisAlignment) -> Self { - self.cross = cross; + /// Put the tab bar on the specified edge of the cross axis. + pub fn with_edge(mut self, edge: TabsEdge) -> Self { + self.edge = edge; self } @@ -912,7 +924,7 @@ impl Tabs { fn make_scope(&self, tabs_from_data: TP) -> WidgetPod> { let (tabs_bar, tabs_body) = ( - (TabBar::new(self.axis, self.cross), 0.0), + (TabBar::new(self.axis, self.edge), 0.0), ( TabsBody::new(self.axis, self.transition) .padding(5.) @@ -922,7 +934,7 @@ impl Tabs { ); let mut layout: Flex> = Flex::for_axis(self.axis.cross()); - if let CrossAxisAlignment::End = self.cross { + if let TabsEdge::Trailing = self.edge { layout.add_flex_child(tabs_body.0, tabs_body.1); layout.add_flex_child(tabs_bar.0, tabs_bar.1); } else { @@ -952,10 +964,9 @@ impl Widget for Tabs { env: &Env, ) { if let LifeCycle::WidgetAdded = event { - let mut temp = TabsContent::Swapping; - std::mem::swap(&mut self.content, &mut temp); + let content = std::mem::replace(&mut self.content, TabsContent::Swapping); - self.content = match temp { + self.content = match content { TabsContent::Building { tabs } => { ctx.children_changed(); TabsContent::Running { @@ -968,7 +979,7 @@ impl Widget for Tabs { scope: self.make_scope(tabs), } } - _ => temp, + _ => content, }; } if let TabsContent::Running { scope } = &mut self.content {