From 1efb4e8686521ad7fa53931f46982a63335d4ad6 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 17 Jan 2021 23:30:59 +0000 Subject: [PATCH 1/6] Add 2 features - A `Lens` that combines 2 lenses into a tuple - A widget that displays an `Option`, showing nothing if there isn't anything to show. --- druid/src/lens/lens.rs | 28 ++++++++++++ druid/src/lens/mod.rs | 2 +- druid/src/widget/mod.rs | 2 + druid/src/widget/optional.rs | 89 ++++++++++++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 druid/src/widget/optional.rs diff --git a/druid/src/lens/lens.rs b/druid/src/lens/lens.rs index 178b9204b9..43382ae5db 100644 --- a/druid/src/lens/lens.rs +++ b/druid/src/lens/lens.rs @@ -567,3 +567,31 @@ impl Lens for Constant { f(&mut tmp) } } + +/// A lens that combines two lenses into a tuple. +#[derive(Debug, Copy, Clone)] +pub struct Tuple(pub L1, pub L2); + +impl Lens for Tuple +where + L1B: Clone, + L2B: Clone, + L1: Lens, + L2: Lens, +{ + fn with V>(&self, data: &A, f: F) -> V { + let l1b = self.0.with(data, |v| v.clone()); + let l2b = self.1.with(data, |v| v.clone()); + f(&(l1b, l2b)) + } + fn with_mut V>(&self, data: &mut A, f: F) -> V { + let l1b = self.0.with(data, |v| v.clone()); + let l2b = self.1.with(data, |v| v.clone()); + let mut tuple = (l1b, l2b); + let out = f(&mut tuple); + let (l1b, l2b) = tuple; + self.0.with_mut(data, |v| *v = l1b); + self.1.with_mut(data, |v| *v = l2b); + out + } +} diff --git a/druid/src/lens/mod.rs b/druid/src/lens/mod.rs index 3d99cac01e..55a67ebca9 100644 --- a/druid/src/lens/mod.rs +++ b/druid/src/lens/mod.rs @@ -49,6 +49,6 @@ #[allow(clippy::module_inception)] #[macro_use] mod lens; -pub use lens::{Constant, Deref, Field, Identity, InArc, Index, Map, Ref, Then, Unit}; +pub use lens::{Constant, Deref, Field, Identity, InArc, Index, Map, Ref, Then, Tuple, Unit}; #[doc(hidden)] pub use lens::{Lens, LensExt}; diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index a00130b4e1..24bc6a5343 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -36,6 +36,7 @@ mod invalidation; mod label; mod lens_wrap; mod list; +mod optional; mod padding; mod painter; mod parse; @@ -76,6 +77,7 @@ pub use identity_wrapper::IdentityWrapper; pub use label::{Label, LabelText, LineBreaking, RawLabel}; pub use lens_wrap::LensWrap; pub use list::{List, ListIter}; +pub use optional::Optional; pub use padding::Padding; pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; diff --git a/druid/src/widget/optional.rs b/druid/src/widget/optional.rs new file mode 100644 index 0000000000..d9f94006f3 --- /dev/null +++ b/druid/src/widget/optional.rs @@ -0,0 +1,89 @@ +// Copyright 2019 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 displays only when an `Option` is `Some`. + +use crate::widget::prelude::*; +use crate::{Data, Point, WidgetPod}; + +/// A widget that displays only when an `Option` is `Some`. +/// +/// If you want to display a widget in the `None` case, wrap this in an `Either` widget. +pub struct Optional { + inner: WidgetPod>>, + /// Keep track of whether we've done 'WidgetAdded'. We can't do this until we get `Some` data + /// for the first time. + init: bool, +} + +impl Optional { + /// Create a new widget that only shows when data is `Some`. + pub fn new(inner: impl Widget + 'static) -> Optional { + Optional { + inner: WidgetPod::new(inner).boxed(), + init: false, + } + } +} + +impl Widget> for Optional { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Option, env: &Env) { + if let Some(data) = data.as_mut() { + self.inner.event(ctx, event, data, env); + } + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &Option, + env: &Env, + ) { + if let Some(data) = data.as_ref() { + self.inner.lifecycle(ctx, event, data, env); + } + } + + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &Option, data: &Option, env: &Env) { + if old_data.is_some() != data.is_some() { + ctx.request_layout(); + } + if let Some(data) = data.as_ref() { + self.inner.update(ctx, data, env); + } + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &Option, + env: &Env, + ) -> Size { + if let Some(data) = data.as_ref() { + let size = self.inner.layout(ctx, bc, data, env); + self.inner.set_origin(ctx, data, env, Point::ORIGIN); + size + } else { + bc.min() + } + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &Option, env: &Env) { + if let Some(data) = data.as_ref() { + self.inner.paint(ctx, data, env); + } + } +} From f218e0a14a1baa1b9e1203df820738a1ea05efb1 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Sun, 17 Jan 2021 23:33:47 +0000 Subject: [PATCH 2/6] Make sure lifecycle methods are always forwarded. --- druid/src/widget/optional.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/druid/src/widget/optional.rs b/druid/src/widget/optional.rs index d9f94006f3..efe62af2aa 100644 --- a/druid/src/widget/optional.rs +++ b/druid/src/widget/optional.rs @@ -22,9 +22,6 @@ use crate::{Data, Point, WidgetPod}; /// If you want to display a widget in the `None` case, wrap this in an `Either` widget. pub struct Optional { inner: WidgetPod>>, - /// Keep track of whether we've done 'WidgetAdded'. We can't do this until we get `Some` data - /// for the first time. - init: bool, } impl Optional { @@ -32,12 +29,11 @@ impl Optional { pub fn new(inner: impl Widget + 'static) -> Optional { Optional { inner: WidgetPod::new(inner).boxed(), - init: false, } } } -impl Widget> for Optional { +impl Widget> for Optional { fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Option, env: &Env) { if let Some(data) = data.as_mut() { self.inner.event(ctx, event, data, env); @@ -53,6 +49,8 @@ impl Widget> for Optional { ) { if let Some(data) = data.as_ref() { self.inner.lifecycle(ctx, event, data, env); + } else { + self.inner.lifecycle(ctx, event, &Default::default(), env); } } From a90d9ef955f0bb0ba7d5049467f21924aeb25ae1 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Thu, 21 Jan 2021 16:14:11 +0000 Subject: [PATCH 3/6] Use `Maybe` from runebender rather than `Optional`. --- druid/src/widget/maybe.rs | 182 ++++++++++++++++++++++++++++++++++ druid/src/widget/mod.rs | 4 +- druid/src/widget/sized_box.rs | 7 +- 3 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 druid/src/widget/maybe.rs diff --git a/druid/src/widget/maybe.rs b/druid/src/widget/maybe.rs new file mode 100644 index 0000000000..ded0bac011 --- /dev/null +++ b/druid/src/widget/maybe.rs @@ -0,0 +1,182 @@ +// Copyright 2021 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 for optional data, with different `Some` and `None` children. + +use druid::{ + BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, LifeCycle, LifeCycleCtx, PaintCtx, Size, + UpdateCtx, Widget, WidgetExt, WidgetPod, +}; + +use druid::widget::SizedBox; + +/// A widget that switches between two possible child views, for `Data` that +/// is `Option`. +pub struct Maybe { + some_maker: Box Box>>, + none_maker: Box Box>>, + widget: MaybeWidget, +} + +/// Internal widget, which is either the `Some` variant, or the `None` variant. +#[allow(clippy::large_enum_variant)] +enum MaybeWidget { + Some(WidgetPod>>), + None(WidgetPod<(), Box>>), +} + +impl Maybe { + /// Create a new `Maybe` widget with a `Some` and a `None` branch. + pub fn new( + // we make these generic so that the caller doesn't have to explicitly + // box. We don't technically *need* to box, but it seems simpler. + some_maker: impl Fn() -> W1 + 'static, + none_maker: impl Fn() -> W2 + 'static, + ) -> Maybe + where + W1: Widget + 'static, + W2: Widget<()> + 'static, + { + let widget = MaybeWidget::Some(WidgetPod::new(some_maker().boxed())); + Maybe { + some_maker: Box::new(move || some_maker().boxed()), + none_maker: Box::new(move || none_maker().boxed()), + widget, + } + } + + /// Create a new `Maybe` widget where the `None` branch is an empty widget. + pub fn or_empty + 'static>(some_maker: impl Fn() -> W1 + 'static) -> Maybe { + Self::new(some_maker, SizedBox::empty) + } + + /// Re-create the internal widget, usually in response to the optional going `Some` -> `None` + /// or the reverse. + fn rebuild_widget(&mut self, is_some: bool) { + if is_some { + self.widget = MaybeWidget::Some(WidgetPod::new((self.some_maker)())); + } else { + self.widget = MaybeWidget::None(WidgetPod::new((self.none_maker)())); + } + } +} + +impl Widget> for Maybe { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Option, env: &Env) { + if data.is_some() == self.widget.is_some() { + match data.as_mut() { + Some(d) => self.widget.with_some(|w| w.event(ctx, event, d, env)), + None => self.widget.with_none(|w| w.event(ctx, event, &mut (), env)), + }; + } + } + + fn lifecycle( + &mut self, + ctx: &mut LifeCycleCtx, + event: &LifeCycle, + data: &Option, + env: &Env, + ) { + if data.is_some() != self.widget.is_some() { + // possible if getting lifecycle after an event that changed the data, + // or on WidgetAdded + self.rebuild_widget(data.is_some()); + } + assert_eq!(data.is_some(), self.widget.is_some(), "{:?}", event); + match data.as_ref() { + Some(d) => self.widget.with_some(|w| w.lifecycle(ctx, event, d, env)), + None => self.widget.with_none(|w| w.lifecycle(ctx, event, &(), env)), + }; + } + + fn update(&mut self, ctx: &mut UpdateCtx, old_data: &Option, data: &Option, env: &Env) { + if old_data.is_some() != data.is_some() { + self.rebuild_widget(data.is_some()); + ctx.children_changed(); + } else { + match data { + Some(new) => self.widget.with_some(|w| w.update(ctx, new, env)), + None => self.widget.with_none(|w| w.update(ctx, &(), env)), + }; + } + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &Option, + env: &Env, + ) -> Size { + match data.as_ref() { + Some(d) => self.widget.with_some(|w| { + let size = w.layout(ctx, bc, d, env); + w.set_layout_rect(ctx, d, env, size.to_rect()); + size + }), + None => self.widget.with_none(|w| { + let size = w.layout(ctx, bc, &(), env); + w.set_layout_rect(ctx, &(), env, size.to_rect()); + size + }), + } + .unwrap_or_default() + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &Option, env: &Env) { + match data.as_ref() { + Some(d) => self.widget.with_some(|w| w.paint(ctx, d, env)), + None => self.widget.with_none(|w| w.paint(ctx, &(), env)), + }; + } +} + +impl MaybeWidget { + /// Like `Option::is_some`. + fn is_some(&self) -> bool { + match self { + Self::Some(_) => true, + Self::None(_) => false, + } + } + + /// Lens to the `Some` variant. + fn with_some>>) -> R>( + &mut self, + f: F, + ) -> Option { + match self { + Self::Some(widget) => Some(f(widget)), + Self::None(_) => { + log::warn!("`MaybeWidget::with_some` called on `None` value"); + None + } + } + } + + /// Lens to the `None` variant. + fn with_none>>) -> R>( + &mut self, + f: F, + ) -> Option { + match self { + Self::None(widget) => Some(f(widget)), + Self::Some(_) => { + log::warn!("`MaybeWidget::with_none` called on `Some` value"); + None + } + } + } +} diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 24bc6a5343..139c9291ce 100644 --- a/druid/src/widget/mod.rs +++ b/druid/src/widget/mod.rs @@ -36,7 +36,7 @@ mod invalidation; mod label; mod lens_wrap; mod list; -mod optional; +mod maybe; mod padding; mod painter; mod parse; @@ -77,7 +77,7 @@ pub use identity_wrapper::IdentityWrapper; pub use label::{Label, LabelText, LineBreaking, RawLabel}; pub use lens_wrap::LensWrap; pub use list::{List, ListIter}; -pub use optional::Optional; +pub use maybe::Maybe; pub use padding::Padding; pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; diff --git a/druid/src/widget/sized_box.rs b/druid/src/widget/sized_box.rs index 4b4a6cf51b..cdcad65237 100644 --- a/druid/src/widget/sized_box.rs +++ b/druid/src/widget/sized_box.rs @@ -22,8 +22,8 @@ use crate::Data; /// A widget with predefined size. /// /// If given a child, this widget forces its child to have a specific width and/or height -/// (assuming values are permitted by this widget's parent). If either the width or height is not set, -/// this widget will size itself to match the child's size in that dimension. +/// (assuming values are permitted by this widget's parent). If either the width or height is not +/// set, this widget will size itself to match the child's size in that dimension. /// /// If not given a child, SizedBox will try to size itself as close to the specified height /// and width as possible given the parent's constraints. If height or width is not set, @@ -45,6 +45,9 @@ impl SizedBox { } /// Construct container without child, and both width and height not set. + /// + /// If the widget is unchanged, it will do nothing, which can be useful if you want to draw a + /// widget some of the time (e.g. for [`Maybe`][crate::widgets::Maybe]). pub fn empty() -> Self { Self { inner: None, From ab0fc512c7f4f95305ac6dcec831bd7ff7dd2cd4 Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Thu, 21 Jan 2021 17:46:35 +0000 Subject: [PATCH 4/6] Fix doc link --- druid/src/widget/sized_box.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/druid/src/widget/sized_box.rs b/druid/src/widget/sized_box.rs index cdcad65237..67c4bfea91 100644 --- a/druid/src/widget/sized_box.rs +++ b/druid/src/widget/sized_box.rs @@ -47,7 +47,7 @@ impl SizedBox { /// Construct container without child, and both width and height not set. /// /// If the widget is unchanged, it will do nothing, which can be useful if you want to draw a - /// widget some of the time (e.g. for [`Maybe`][crate::widgets::Maybe]). + /// widget some of the time (e.g. for [`Maybe`][crate::widget::Maybe]). pub fn empty() -> Self { Self { inner: None, From 102e9759e050239b287138dd516737686a6ea0bd Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Thu, 21 Jan 2021 17:48:51 +0000 Subject: [PATCH 5/6] Change wording slightly. --- druid/src/widget/sized_box.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/druid/src/widget/sized_box.rs b/druid/src/widget/sized_box.rs index 67c4bfea91..4090512749 100644 --- a/druid/src/widget/sized_box.rs +++ b/druid/src/widget/sized_box.rs @@ -47,7 +47,8 @@ impl SizedBox { /// Construct container without child, and both width and height not set. /// /// If the widget is unchanged, it will do nothing, which can be useful if you want to draw a - /// widget some of the time (e.g. for [`Maybe`][crate::widget::Maybe]). + /// widget some of the time (for example, it is used to implement + /// [`Maybe`][crate::widget::Maybe]). pub fn empty() -> Self { Self { inner: None, From 4d674f72d64d689ab05b271d8af998dcd1973f8e Mon Sep 17 00:00:00 2001 From: Richard Dodd Date: Wed, 27 Jan 2021 11:05:58 +0000 Subject: [PATCH 6/6] Address feedback - Change `Tuple` to `Tuple2`. - Put a doc alias of "null" on `SizedBox::empty`. - Remove `Optional` which had already been removed from widget `mod`. --- druid/src/lens/lens.rs | 4 +- druid/src/lens/mod.rs | 2 +- druid/src/widget/optional.rs | 87 ----------------------------------- druid/src/widget/sized_box.rs | 1 + 4 files changed, 4 insertions(+), 90 deletions(-) delete mode 100644 druid/src/widget/optional.rs diff --git a/druid/src/lens/lens.rs b/druid/src/lens/lens.rs index 43382ae5db..9808d037f2 100644 --- a/druid/src/lens/lens.rs +++ b/druid/src/lens/lens.rs @@ -570,9 +570,9 @@ impl Lens for Constant { /// A lens that combines two lenses into a tuple. #[derive(Debug, Copy, Clone)] -pub struct Tuple(pub L1, pub L2); +pub struct Tuple2(pub L1, pub L2); -impl Lens for Tuple +impl Lens for Tuple2 where L1B: Clone, L2B: Clone, diff --git a/druid/src/lens/mod.rs b/druid/src/lens/mod.rs index 55a67ebca9..b942608d5e 100644 --- a/druid/src/lens/mod.rs +++ b/druid/src/lens/mod.rs @@ -49,6 +49,6 @@ #[allow(clippy::module_inception)] #[macro_use] mod lens; -pub use lens::{Constant, Deref, Field, Identity, InArc, Index, Map, Ref, Then, Tuple, Unit}; +pub use lens::{Constant, Deref, Field, Identity, InArc, Index, Map, Ref, Then, Tuple2, Unit}; #[doc(hidden)] pub use lens::{Lens, LensExt}; diff --git a/druid/src/widget/optional.rs b/druid/src/widget/optional.rs deleted file mode 100644 index efe62af2aa..0000000000 --- a/druid/src/widget/optional.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2019 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 displays only when an `Option` is `Some`. - -use crate::widget::prelude::*; -use crate::{Data, Point, WidgetPod}; - -/// A widget that displays only when an `Option` is `Some`. -/// -/// If you want to display a widget in the `None` case, wrap this in an `Either` widget. -pub struct Optional { - inner: WidgetPod>>, -} - -impl Optional { - /// Create a new widget that only shows when data is `Some`. - pub fn new(inner: impl Widget + 'static) -> Optional { - Optional { - inner: WidgetPod::new(inner).boxed(), - } - } -} - -impl Widget> for Optional { - fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Option, env: &Env) { - if let Some(data) = data.as_mut() { - self.inner.event(ctx, event, data, env); - } - } - - fn lifecycle( - &mut self, - ctx: &mut LifeCycleCtx, - event: &LifeCycle, - data: &Option, - env: &Env, - ) { - if let Some(data) = data.as_ref() { - self.inner.lifecycle(ctx, event, data, env); - } else { - self.inner.lifecycle(ctx, event, &Default::default(), env); - } - } - - fn update(&mut self, ctx: &mut UpdateCtx, old_data: &Option, data: &Option, env: &Env) { - if old_data.is_some() != data.is_some() { - ctx.request_layout(); - } - if let Some(data) = data.as_ref() { - self.inner.update(ctx, data, env); - } - } - - fn layout( - &mut self, - ctx: &mut LayoutCtx, - bc: &BoxConstraints, - data: &Option, - env: &Env, - ) -> Size { - if let Some(data) = data.as_ref() { - let size = self.inner.layout(ctx, bc, data, env); - self.inner.set_origin(ctx, data, env, Point::ORIGIN); - size - } else { - bc.min() - } - } - - fn paint(&mut self, ctx: &mut PaintCtx, data: &Option, env: &Env) { - if let Some(data) = data.as_ref() { - self.inner.paint(ctx, data, env); - } - } -} diff --git a/druid/src/widget/sized_box.rs b/druid/src/widget/sized_box.rs index 4090512749..493f2876a0 100644 --- a/druid/src/widget/sized_box.rs +++ b/druid/src/widget/sized_box.rs @@ -49,6 +49,7 @@ impl SizedBox { /// If the widget is unchanged, it will do nothing, which can be useful if you want to draw a /// widget some of the time (for example, it is used to implement /// [`Maybe`][crate::widget::Maybe]). + #[doc(alias = "null")] pub fn empty() -> Self { Self { inner: None,