Add the `lens` component (#587)

See
https://xi.zulipchat.com/#narrow/stream/354396-xilem/topic/Lens.20View

Usage:
```rust
fn app_logic(state: &mut FlightPlanner) -> impl WidgetView<FlightPlanner> {
    lens(date_picker, state, |state| &mut state.date)
}

struct FlightPlanner {
    date: Date,
    available_flights: Vec<Flight>,
}
```

Also extends the docs features in Xilem Core, and increases the
complexity threshold

---------

Co-authored-by: Kaur Kuut <strom@nevermore.ee>
Co-authored-by: Philipp Mildenberger <philipp@mildenberger.me>
This commit is contained in:
Daniel McNab 2024-09-18 09:10:50 +01:00 committed by GitHub
parent 2ccd9d4712
commit 0d56c592f5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 146 additions and 32 deletions

View File

@ -1,2 +1,7 @@
# Don't warn about these identifiers when using clippy::doc_markdown.
doc-valid-idents = ["MathML", ".."]
# The default clippy value for this is 250, which causes warnings for rather simple types
# like Box<dyn Fn(&mut Env, &T)>, which seems overly strict. The new value of 400 is
# a simple guess. It might be worth lowering this, or using the default, in the future.
type-complexity-threshold = 400

View File

@ -75,7 +75,6 @@ pub(crate) struct RenderRootState {
pub(crate) scenes: HashMap<WidgetId, Scene>,
}
#[allow(clippy::type_complexity)]
pub(crate) struct MutateCallback {
pub(crate) id: WidgetId,
pub(crate) callback: Box<dyn FnOnce(WidgetMut<'_, Box<dyn Widget>>)>,

View File

@ -1,12 +1,12 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
//! Modularizing state can be done with `map_state` which maps a subset of the state from the parent view state
//! Modularizing state can be done with `lens` which allows using modular components.
use masonry::widget::MainAxisAlignment;
use winit::error::EventLoopError;
use xilem::{
core::map_state,
core::lens,
view::{button, flex, label, Axis},
EventLoop, WidgetView, Xilem,
};
@ -17,7 +17,7 @@ struct AppState {
global_count: i32,
}
fn modularized_counter(count: &mut i32) -> impl WidgetView<i32> {
fn modular_counter(count: &mut i32) -> impl WidgetView<i32> {
flex((
label(format!("modularized count: {count}")),
button("+", |count| *count += 1),
@ -27,10 +27,7 @@ fn modularized_counter(count: &mut i32) -> impl WidgetView<i32> {
fn app_logic(state: &mut AppState) -> impl WidgetView<AppState> {
flex((
map_state(
modularized_counter(&mut state.modularized_count),
|state: &mut AppState| &mut state.modularized_count,
),
lens(modular_counter, state, |state| &mut state.modularized_count),
button(
format!("clicked {} times", state.global_count),
|state: &mut AppState| state.global_count += 1,

View File

@ -1,9 +1,41 @@
// Copyright 2024 the Xilem Authors
// SPDX-License-Identifier: Apache-2.0
//! Fake implementations of Xilem traits for use within documentation examples and tests.
// Hide these docs from "general audiences" online.
// but keep them available for developers of Xilem Core to browse.
#![cfg_attr(docsrs, doc(hidden))]
use crate::ViewPathTracker;
//! Fake implementations of Xilem traits for use within documentation examples and tests.
//!
//! Users of Xilem Core should not use these traits.
//!
//! The items defined in this trait will often be imported in doc comments in renamed form.
//! This is mostly intended for writing documentation internally to Xilem Core.
//!
//! This module is not required to follow semver. It is public only for documentation purposes.
//!
//! # Examples
//!
//! ```
//! /// A view to do something fundamental
//! ///
//! /// # Examples
//! /// ```
//! /// # use xilem_core::docs::{DocsView as WidgetView, State};
//! /// use xilem_core::interesting_primitive;
//! /// fn user_component() -> WidgetView<State> {
//! /// interesting_primitive()
//! /// }
//! ///
//! /// ```
//! fn interesting_primitive() -> InterestingPrimitive {
//! // ...
//! # InterestingPrimitive
//! }
//! # struct InterestingPrimitive;
//! ```
use crate::{run_once, View, ViewPathTracker};
/// A type used for documentation
pub enum Fake {}
@ -20,3 +52,21 @@ impl ViewPathTracker for Fake {
match *self {}
}
}
/// A version of [`View`] used for documentation.
///
/// This will often be imported by a different name in a hidden use item.
///
/// In most cases, that name will be `WidgetView`, as Xilem Core's documentation is
/// primarily targeted at users of [Xilem](https://crates.io/crates/xilem/).
pub trait DocsView<State, Action = ()>: View<State, Action, Fake> {}
impl<V, State, Action> DocsView<State, Action> for V where V: View<State, Action, Fake> {}
/// A state type usable in a component
pub struct State;
/// A minimal component.
pub fn some_component<Action>(_: &mut State) -> impl DocsView<State, Action> {
// The view which does nothing already exists in `run_once`.
run_once(|| {})
}

View File

@ -28,8 +28,8 @@ pub use view::{View, ViewId, ViewMarker, ViewPathTracker};
mod views;
pub use views::{
adapt, fork, frozen, map_action, map_state, memoize, one_of, run_once, run_once_raw, Adapt,
AdaptThunk, Fork, Frozen, MapAction, MapState, Memoize, OrphanView, RunOnce,
adapt, fork, frozen, lens, map_action, map_state, memoize, one_of, run_once, run_once_raw,
Adapt, AdaptThunk, Fork, Frozen, MapAction, MapState, Memoize, OrphanView, RunOnce,
};
mod message;

View File

@ -17,7 +17,6 @@ pub struct MapAction<
> {
map_fn: F,
child: V,
#[allow(clippy::type_complexity)]
phantom: PhantomData<fn() -> (State, ParentAction, ChildAction)>,
}

View File

@ -5,12 +5,13 @@ use core::marker::PhantomData;
use crate::{MessageResult, Mut, View, ViewId, ViewMarker, ViewPathTracker};
/// A view that "extracts" state from a [`View<ParentState,_,_>`] to [`View<ChildState,_,_>`].
/// This allows modularization of views based on their state.
pub struct MapState<ParentState, ChildState, V, F = fn(&mut ParentState) -> &mut ChildState> {
f: F,
/// The View for [`map_state`] and [`lens`].
///
/// See their documentation for more context.
pub struct MapState<V, F, ParentState, ChildState, Action, Context, Message> {
map_state: F,
child: V,
phantom: PhantomData<fn() -> (ParentState, ChildState)>,
phantom: PhantomData<fn(ParentState) -> (ChildState, Action, Context, Message)>,
}
/// A view that "extracts" state from a [`View<ParentState,_,_>`] to [`View<ChildState,_,_>`].
@ -42,7 +43,7 @@ pub struct MapState<ParentState, ChildState, V, F = fn(&mut ParentState) -> &mut
pub fn map_state<ParentState, ChildState, Action, Context: ViewPathTracker, Message, V, F>(
view: V,
f: F,
) -> MapState<ParentState, ChildState, V, F>
) -> MapState<V, F, ParentState, ChildState, Action, Context, Message>
where
ParentState: 'static,
ChildState: 'static,
@ -50,20 +51,82 @@ where
F: Fn(&mut ParentState) -> &mut ChildState + 'static,
{
MapState {
f,
map_state: f,
child: view,
phantom: PhantomData,
}
}
impl<ParentState, ChildState, V, F> ViewMarker for MapState<ParentState, ChildState, V, F> {}
impl<ParentState, ChildState, Action, Context: ViewPathTracker, Message, V, F>
View<ParentState, Action, Context, Message> for MapState<ParentState, ChildState, V, F>
/// An adapter which allows using a component which only uses one field of the current state.
///
/// In Xilem, many components are functions of the form `fn my_component(&mut SomeState) -> impl WidgetView<SomeState>`.
/// For example, a date picker might be of the form `fn date_picker(&mut Date) -> impl WidgetView<Date>`.
/// The `lens` View allows using these components in a higher-level component, where the higher level state has
/// a field of the inner component's state type.
/// For example, a flight finder app might have a `Date` field for the currently selected date.
///
/// The parameters of this view are:
/// - `component`: The child component the lens is being created for.
/// - `state`: The current outer view's state
/// - `map`: A function from the higher-level state type to `component`'s state type
///
/// This is a wrapper around [`map_state`].
/// That view can be used if the child doesn't follow the expected component signature.
///
/// # Examples
///
/// In code, the date picker example might look like:
///
/// ```
/// # use xilem_core::docs::{DocsView as WidgetView, State as Date, State as Flight, some_component as date_picker};
/// use xilem_core::lens;
///
/// fn app_logic(state: &mut FlightPlanner) -> impl WidgetView<FlightPlanner> {
/// lens(date_picker, state, |state| &mut state.date)
/// }
///
/// struct FlightPlanner {
/// date: Date,
/// available_flights: Vec<Flight>,
/// }
/// ```
pub fn lens<OuterState, Action, Context, Message, InnerState, StateF, InnerView, Component>(
component: Component,
state: &mut OuterState,
// This parameter ordering does run into https://github.com/rust-lang/rustfmt/issues/3605
// Our general advice is to make sure that the lens arguments are short enough...
map: StateF,
) -> MapState<InnerView, StateF, OuterState, InnerState, Action, Context, Message>
where
StateF: Fn(&mut OuterState) -> &mut InnerState + Send + Sync + 'static,
Component: FnOnce(&mut InnerState) -> InnerView,
InnerView: View<InnerState, Action, Context, Message>,
Context: ViewPathTracker,
{
let mapped = map(state);
let view = component(mapped);
MapState {
child: view,
map_state: map,
phantom: PhantomData,
}
}
impl<V, F, ParentState, ChildState, Action, Context, Message> ViewMarker
for MapState<V, F, ParentState, ChildState, Action, Context, Message>
{
}
impl<ParentState, ChildState, Action, Context, Message, V, F>
View<ParentState, Action, Context, Message>
for MapState<V, F, ParentState, ChildState, Action, Context, Message>
where
ParentState: 'static,
ChildState: 'static,
V: View<ChildState, Action, Context, Message>,
F: Fn(&mut ParentState) -> &mut ChildState + 'static,
Action: 'static,
Context: ViewPathTracker + 'static,
Message: 'static,
{
type ViewState = V::ViewState;
type Element = V::Element;
@ -99,6 +162,6 @@ where
app_state: &mut ParentState,
) -> MessageResult<Action, Message> {
self.child
.message(view_state, id_path, message, (self.f)(app_state))
.message(view_state, id_path, message, (self.map_state)(app_state))
}
}

View File

@ -8,7 +8,7 @@ mod adapt;
pub use adapt::{adapt, Adapt, AdaptThunk};
mod map_state;
pub use map_state::{map_state, MapState};
pub use map_state::{lens, map_state, MapState};
mod map_action;
pub use map_action::{map_action, MapAction};

View File

@ -16,9 +16,9 @@ use crate::{MessageResult, NoElement, View, ViewMarker, ViewPathTracker};
/// This can be useful for logging a value:
///
/// ```
/// # use xilem_core::{run_once, View, docs::{Fake as ViewCtx}, PhantomView};
/// # use xilem_core::{run_once, View, docs::{Fake as ViewCtx, DocsView as WidgetView}};
/// # struct AppData;
/// fn log_lifecycle(data: &mut AppData) -> impl PhantomView<AppData, (), ViewCtx> {
/// fn log_lifecycle(data: &mut AppData) -> impl WidgetView<AppData, ()> {
/// run_once(|| eprintln!("View constructed"))
/// }
/// ```
@ -32,11 +32,11 @@ use crate::{MessageResult, NoElement, View, ViewMarker, ViewPathTracker};
/// // <https://doc.rust-lang.org/error_codes/E0080.html>
/// // Note that this error code is only checked on nightly
/// ```compile_fail,E0080
/// # use xilem_core::{run_once, View, docs::{Fake as ViewCtx}, PhantomView};
/// # use xilem_core::{run_once, View, docs::{DocsView as WidgetView}};
/// # struct AppData {
/// # data: u32
/// # }
/// fn log_data(app: &mut AppData) -> impl PhantomView<AppData, (), ViewCtx> {
/// fn log_data(app: &mut AppData) -> impl WidgetView<AppData, ()> {
/// let val = app.data;
/// run_once(move || println!("{}", val))
/// }

View File

@ -16,7 +16,6 @@ pub struct MemoizedAwait<State, Action, OA, InitFuture, Data, Callback, F, FOut>
callback: Callback,
debounce_ms: usize,
reset_debounce_on_update: bool,
#[allow(clippy::type_complexity)]
phantom: PhantomData<fn() -> (State, Action, OA, F, FOut)>,
}

View File

@ -22,7 +22,6 @@ pub struct OnEvent<V, State, Action, Event, Callback> {
pub(crate) capture: bool,
pub(crate) passive: bool,
pub(crate) handler: Callback,
#[allow(clippy::type_complexity)]
pub(crate) phantom_event_ty: PhantomData<fn() -> (State, Action, Event)>,
}

View File

@ -188,7 +188,10 @@ pub trait DomView<State, Action = ()>:
}
/// See [`map_state`](`core::map_state`)
fn map_state<ParentState, F>(self, f: F) -> MapState<ParentState, State, Self, F>
fn map_state<ParentState, F>(
self,
f: F,
) -> MapState<Self, F, ParentState, State, Action, ViewCtx, DynMessage>
where
State: 'static,
ParentState: 'static,