diff --git a/AUTHORS b/AUTHORS index 11b8a359e6..14d9778a34 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,3 +11,4 @@ Kaiyin Zhong Kaur Kuut Leopold Luley Andrey Kabylin +Robert Wittams \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d933a1d8d0..1166c67638 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ You can find its changes [documented below](#060---2020-06-01). - The Lens derive now supports an `ignore` attribute. ([#1133] by [@jneem]) - `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]) ### Changed @@ -399,6 +400,7 @@ Last release without a changelog :( [#1143]: https://github.com/linebender/druid/pull/1143 [#1145]: https://github.com/linebender/druid/pull/1145 [#1152]: https://github.com/linebender/druid/pull/1152 +[#1151]: https://github.com/linebender/druid/pull/1151 [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/contexts.rs b/druid/src/contexts.rs index 400bd8e010..e09375d3f8 100644 --- a/druid/src/contexts.rs +++ b/druid/src/contexts.rs @@ -510,6 +510,12 @@ impl EventCtx<'_, '_> { } } +impl UpdateCtx<'_, '_> { + pub fn has_requested_update(&mut self) -> bool { + self.widget_state.request_update + } +} + impl LifeCycleCtx<'_, '_> { /// Registers a child widget. /// diff --git a/druid/src/lens/lens.rs b/druid/src/lens/lens.rs index bb477754c6..2f04c0668e 100644 --- a/druid/src/lens/lens.rs +++ b/druid/src/lens/lens.rs @@ -232,7 +232,7 @@ where let lens = &self.lens; lens.with(old_data, |old_data| { lens.with(data, |data| { - if !old_data.same(data) { + if ctx.has_requested_update() || !old_data.same(data) { inner.update(ctx, old_data, data, env); } }) diff --git a/druid/src/widget/mod.rs b/druid/src/widget/mod.rs index 27807da3f8..5af57a32f0 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 scope; mod scroll; mod sized_box; mod slider; @@ -69,6 +70,7 @@ pub use painter::{BackgroundBrush, Painter}; pub use parse::Parse; pub use progress_bar::ProgressBar; pub use radio::{Radio, RadioGroup}; +pub use scope::{DefaultScopePolicy, LensScopeTransfer, Scope, ScopePolicy, ScopeTransfer}; pub use scroll::Scroll; pub use sized_box::SizedBox; pub use slider::Slider; diff --git a/druid/src/widget/scope.rs b/druid/src/widget/scope.rs new file mode 100644 index 0000000000..3400dc8748 --- /dev/null +++ b/druid/src/widget/scope.rs @@ -0,0 +1,282 @@ +use crate::kurbo::{Point, Rect}; +use crate::{ + BoxConstraints, Data, Env, Event, EventCtx, LayoutCtx, Lens, LifeCycle, LifeCycleCtx, PaintCtx, + Size, UpdateCtx, Widget, WidgetPod, +}; +use std::marker::PhantomData; + +/// A policy that controls how a Scope will interact with its surrounding application data. +/// Specifically, how to create an initial State from the Input, and how to synchronise the two. +pub trait ScopePolicy { + /// The type of data that comes in from the surrounding application or scope. + type In: Data; + /// The type of data that the Scope will maintain internally. + /// This will usually be larger than the input data, and will embed the input data. + type State: Data; + /// The type of transfer that will be used to synchronise internal and application state + type Transfer: ScopeTransfer; + /// Make a new state and transfer from the input. + /// This consumes the policy, so non cloneable items can make their way into the state this way. + fn create(self, inner: &Self::In) -> (Self::State, Self::Transfer); +} + +pub trait ScopeTransfer { + type In: Data; + type State: Data; + + /// Replace the input we have within our State with a new one from outside + fn read_input(&self, state: &mut Self::State, inner: &Self::In); + /// Take the modifications we have made and write them back + /// to our input. + fn write_back_input(&self, state: &Self::State, inner: &mut Self::In); +} + +/// A default implementation of scope policy that takes a function and a transfer +pub struct DefaultScopePolicy Transfer::State, Transfer: ScopeTransfer> { + make_state: F, + transfer: Transfer, +} + +impl Transfer::State, Transfer: ScopeTransfer> + DefaultScopePolicy +{ + pub fn new(make_state: F, transfer: Transfer) -> Self { + DefaultScopePolicy { + make_state, + transfer, + } + } +} + +impl State, L: Lens, In: Data, State: Data> + DefaultScopePolicy> +{ + pub fn from_lens(make_state: F, lens: L) -> Self { + Self::new(make_state, LensScopeTransfer::new(lens)) + } +} + +impl Transfer::State, Transfer: ScopeTransfer> ScopePolicy + for DefaultScopePolicy +{ + type In = Transfer::In; + type State = Transfer::State; + type Transfer = Transfer; + + fn create(self, inner: &Self::In) -> (Self::State, Self::Transfer) { + let state = (self.make_state)(inner.clone()); + (state, self.transfer) + } +} + +/// A scope transfer that uses a Lens to synchronise between a large internal state and a small input. +pub struct LensScopeTransfer, In, State> { + lens: L, + phantom_in: PhantomData, + phantom_state: PhantomData, +} + +impl, In, State> LensScopeTransfer { + pub fn new(lens: L) -> Self { + LensScopeTransfer { + lens, + phantom_in: PhantomData::default(), + phantom_state: PhantomData::default(), + } + } +} + +impl, In: Data, State: Data> ScopeTransfer for LensScopeTransfer { + type In = In; + type State = State; + + fn read_input(&self, state: &mut State, data: &In) { + self.lens.with_mut(state, |inner| { + if !inner.same(&data) { + *inner = data.clone() + } + }); + } + + fn write_back_input(&self, state: &State, data: &mut In) { + self.lens.with(state, |inner| { + if !inner.same(&data) { + *data = inner.clone(); + } + }); + } +} + +enum ScopeContent { + Policy { + policy: Option, + }, + Transfer { + state: SP::State, + transfer: SP::Transfer, + }, +} + +/// A widget that allows encapsulation of application state. +/// +/// This is useful in circumstances where +/// * A (potentially reusable) widget is composed of a tree of multiple cooperating child widgets +/// * Those widgets communicate amongst themselves using Druids reactive data mechanisms +/// * It is undesirable to complicate the surrounding application state with the internal details +/// of the widget. +/// +/// +/// Examples include: +/// * In a tabs widget composed of a tab bar, and a widget switching body, those widgets need to +/// cooperate on which tab is selected. However not every user of a tabs widget wishes to +/// encumber their application state with this internal detail - especially as many tabs widgets may +/// reasonably exist in an involved application. +/// * In a table/grid widget composed of various internal widgets, many things need to be synchronised. +/// Scroll position, heading moves, drag operations, sort/filter operations. For many applications +/// access to this internal data outside of the table widget isn't needed. +/// For this reason it may be useful to use a Scope to establish private state. +/// +/// A scope embeds some input state (from its surrounding application or parent scope) +/// into a larger piece of internal state. This is controlled by a user provided policy. +/// +/// The ScopePolicy needs to do two things +/// a) Create a new scope from the initial value of its input, +/// b) Provide two way synchronisation between the input and the state via a ScopeTransfer +/// +/// Convenience methods are provided to make a policy from a function and a lens. +/// It may sometimes be advisable to implement ScopePolicy directly if you need to +/// mention the type of a Scope. +/// +/// # Examples +/// ``` +/// use druid::{Data, Lens, WidgetExt}; +/// use druid::widget::{TextBox, Scope}; +/// #[derive(Clone, Data, Lens)] +/// struct AppState { +/// name: String, +/// } +/// +/// #[derive(Clone, Data, Lens)] +/// struct PrivateState { +/// text: String, +/// other: u32, +/// } +/// +/// impl PrivateState { +/// pub fn new(text: String) -> Self { +/// PrivateState { text, other: 0 } +/// } +/// } +/// +/// fn main() { +/// let scope = Scope::from_lens( +/// PrivateState::new, +/// PrivateState::text, +/// TextBox::new().lens(PrivateState::text), +/// ); +/// } +/// ``` +pub struct Scope> { + content: ScopeContent, + inner: WidgetPod, +} + +impl> Scope { + /// Create a new scope from a policy and an inner widget + pub fn new(policy: SP, inner: W) -> Self { + Scope { + content: ScopeContent::Policy { + policy: Some(policy), + }, + inner: WidgetPod::new(inner), + } + } + + fn with_state( + &mut self, + data: &SP::In, + mut f: impl FnMut(&mut SP::State, &mut WidgetPod) -> V, + ) -> V { + match &mut self.content { + ScopeContent::Policy { policy } => { + // We know that the policy is a Some - it is an option to allow + // us to take ownership before replacing the content. + let (mut state, policy) = policy.take().unwrap().create(data); + let v = f(&mut state, &mut self.inner); + self.content = ScopeContent::Transfer { + state, + transfer: policy, + }; + v + } + ScopeContent::Transfer { + ref mut state, + transfer, + } => { + transfer.read_input(state, data); + f(state, &mut self.inner) + } + } + } + + fn write_back_input(&mut self, data: &mut SP::In) { + if let ScopeContent::Transfer { state, transfer } = &mut self.content { + transfer.write_back_input(state, data) + } + } +} + +impl< + F: Fn(Transfer::In) -> Transfer::State, + Transfer: ScopeTransfer, + W: Widget, + > Scope, W> +{ + /// Create a new policy from a function creating the state, and a ScopeTransfer synchronising it + pub fn from_function(make_state: F, transfer: Transfer, inner: W) -> Self { + Self::new(DefaultScopePolicy::new(make_state, transfer), inner) + } +} + +impl State, L: Lens, W: Widget> + Scope>, W> +{ + /// Create a new policy from a function creating the state, and a Lens synchronising it + pub fn from_lens(make_state: F, lens: L, inner: W) -> Self { + Self::new(DefaultScopePolicy::from_lens(make_state, lens), inner) + } +} + +impl> Widget for Scope { + fn event(&mut self, ctx: &mut EventCtx, event: &Event, data: &mut SP::In, env: &Env) { + self.with_state(data, |state, inner| inner.event(ctx, event, state, env)); + self.write_back_input(data); + ctx.request_update() + } + + fn lifecycle(&mut self, ctx: &mut LifeCycleCtx, event: &LifeCycle, data: &SP::In, env: &Env) { + self.with_state(data, |state, inner| inner.lifecycle(ctx, event, state, env)); + } + + fn update(&mut self, ctx: &mut UpdateCtx, _old_data: &SP::In, data: &SP::In, env: &Env) { + self.with_state(data, |state, inner| inner.update(ctx, state, env)); + } + + fn layout( + &mut self, + ctx: &mut LayoutCtx, + bc: &BoxConstraints, + data: &SP::In, + env: &Env, + ) -> Size { + self.with_state(data, |state, inner| { + let size = inner.layout(ctx, bc, state, env); + inner.set_layout_rect(ctx, state, env, Rect::from_origin_size(Point::ORIGIN, size)); + size + }) + } + + fn paint(&mut self, ctx: &mut PaintCtx, data: &SP::In, env: &Env) { + self.with_state(data, |state, inner| inner.paint_raw(ctx, state, env)); + } +}