Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Tuple optional #1540

Merged
merged 6 commits into from
Feb 9, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions druid/src/lens/lens.rs
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,31 @@ impl<A, B: Clone> Lens<A, B> for Constant<B> {
f(&mut tmp)
}
}

/// A lens that combines two lenses into a tuple.
#[derive(Debug, Copy, Clone)]
pub struct Tuple2<L1, L2>(pub L1, pub L2);

impl<A, L1B, L2B, L1, L2> Lens<A, (L1B, L2B)> for Tuple2<L1, L2>
where
L1B: Clone,
L2B: Clone,
L1: Lens<A, L1B>,
L2: Lens<A, L2B>,
{
fn with<V, F: FnOnce(&(L1B, L2B)) -> 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, F: FnOnce(&mut (L1B, L2B)) -> 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
}
}
2 changes: 1 addition & 1 deletion druid/src/lens/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
182 changes: 182 additions & 0 deletions druid/src/widget/maybe.rs
Original file line number Diff line number Diff line change
@@ -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<T>`.
pub struct Maybe<T> {
some_maker: Box<dyn Fn() -> Box<dyn Widget<T>>>,
none_maker: Box<dyn Fn() -> Box<dyn Widget<()>>>,
widget: MaybeWidget<T>,
}

/// Internal widget, which is either the `Some` variant, or the `None` variant.
#[allow(clippy::large_enum_variant)]
enum MaybeWidget<T> {
Some(WidgetPod<T, Box<dyn Widget<T>>>),
None(WidgetPod<(), Box<dyn Widget<()>>>),
}

impl<T: Data> Maybe<T> {
/// Create a new `Maybe` widget with a `Some` and a `None` branch.
pub fn new<W1, W2>(
// 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<T>
where
W1: Widget<T> + '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<W1: Widget<T> + 'static>(some_maker: impl Fn() -> W1 + 'static) -> Maybe<T> {
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<T: Data> Widget<Option<T>> for Maybe<T> {
fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut Option<T>, 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<T>,
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<T>, data: &Option<T>, 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<T>,
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<T>, 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<T> MaybeWidget<T> {
/// 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, F: FnOnce(&mut WidgetPod<T, Box<dyn Widget<T>>>) -> R>(
&mut self,
f: F,
) -> Option<R> {
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, F: FnOnce(&mut WidgetPod<(), Box<dyn Widget<()>>>) -> R>(
&mut self,
f: F,
) -> Option<R> {
match self {
Self::None(widget) => Some(f(widget)),
Self::Some(_) => {
log::warn!("`MaybeWidget::with_none` called on `Some` value");
None
}
}
}
}
2 changes: 2 additions & 0 deletions druid/src/widget/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mod invalidation;
mod label;
mod lens_wrap;
mod list;
mod maybe;
mod padding;
mod painter;
mod parse;
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 7 additions & 2 deletions druid/src/widget/sized_box.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -45,6 +45,11 @@ impl<T> SizedBox<T> {
}

/// 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 {
derekdreery marked this conversation as resolved.
Show resolved Hide resolved
Self {
inner: None,
Expand Down