diff --git a/druid/src/lens/lens.rs b/druid/src/lens/lens.rs index 178b9204b9..9808d037f2 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 Tuple2(pub L1, pub L2); + +impl Lens for Tuple2 +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..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, 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/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 a00130b4e1..139c9291ce 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 maybe; 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 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..493f2876a0 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,11 @@ 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 (for example, it is used to implement + /// [`Maybe`][crate::widget::Maybe]). + #[doc(alias = "null")] pub fn empty() -> Self { Self { inner: None,