linebender / linebender/druid

[xilem] `SwiftUI` like `Enviorments` via `Provide`/`Inject` pattern

Open
#2,207 0 comments 1 reaction 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
9.7k
Forks
565
PR merge metrics
No merged PRs in 30d

Description

as mentioned briefly in [PR:2170](https://github.com/linebender/druid/pull/2170) `SwiftUI`s way of [using `Environment`](https://developer.apple.com/documentation/swiftui/view/environment(_:_:)) to pass data from parent view to its children, notably styling information, is desirable in `xilem` as well. Below i present an idea for typesafe implementatio, loosely based on [`vue.js`' `Provide`/`Inject` pattern](https://vuejs.org/guide/components/provide-inject.html#provide).

![Source: vue.js documentation](https://user-images.githubusercontent.com/1255913/174607333-39aa02ae-0fd8-4cc4-9b8c-6699c1635cd2.png)

the code samples below are rust flavored pseudo-code, so please don't focus on the exact syntax.

# Proposal

lets start from developer facing api, by means of a code sample.

```rust
struct Prop(u32);
impl Default for Prop {...}

fn user() -> impl View<_> {
inject::(|prop| {
format!("prop value is {}", prop)
})
}

fn provider() -> impl View<_> {
provide(Prop(1), user())
}
```

Two new helper functions are provided.

```rust
struct Inject {}
impl View for Inject {}

fn inject() -> Inject<_>
fn provide() -> Inject<_>
```

Here `T` is a tuple type of the injected/ provided values. e.g.
```rust
let wants_prop: Inject<(Prop), _> = inject::(|prop| {
format!("prop value is {}", prop)
});

let wants_prop_and_foo: Inject<(Prop, Foo), _> = inject::(|prop| {
inject::(|foo| {
format!("prop value is {}, foo is {}", prop, foo)
})
});
```

While `Inject` "adds" to the tuple type, `Provide` "subtracts"

```rust
let _: View<_> = provide(Prop(1), wants_prop);

let wants_foo: Inject<(Foo), _> = provide(Prop(1), wants_prop_and_foo);
let _: Inject<(Prop), _> = provide(Foo(), wants_prop_and_foo);
```

Note how by using the "newtype" idiom we can use the typecheck for resolution at compile time, instead of using something like HashMap as storage and resolving at runtime. This also closely mirrors the way `SwiftUI` handles [custom `EnvironmentKey`s](https://developer.apple.com/documentation/swiftui/environmentkey). The developer should also be required to `impl Default for` their custom types, similar to the [requirement `SwiftUI` places](https://developer.apple.com/documentation/swiftui/environmentkey/defaultvalue).

## Styling

By offering the developer `xilem::environment_key::ButtonStyle(Box` they can provide a custom "drawer" for the button widget, again, [similar to how `SwiftUI` handles it](https://developer.apple.com/documentation/swiftui/buttonstyle).

```rust
mod xilem {
mod views {
mod button {
trait ButtonStyle {
type Configuration;
type Body: View;
fn make_body(configuration: Configuration) -> Body;
}
}
}

mod environment_key {
struct ButtonStyle(Box)
}

fn button(
label: impl Into,
clicked: impl Fn(&mut T) -> A + 'static
) -> ... {
Inject::::new(|button_style| {
Interactive::::new(
button_style.make_body(params),
|event, _controller, data: &mut AppData| {
clicked()
}
)
}
}

struct FancyStyle {}
impl xilem::views::button::ButtonStyle for FancyStyle {}

app() -> _ {
use xilem::{button, environment_key::ButtonStyle};
provide(
ButtonStyle(FancyStyle{}),
button("so fancy", || {
print!("fancy click");
})
)
}
```

## upward dataflow

By using `struct MyPreference(Box)` in the `Provide`/`Inject` pattern, it is also possible to pass data from a child to its ancestors, similar [how `PreferenceKey`s in `SwiftUI` work](https://developer.apple.com/documentation/swiftui/preferencekey).

A motivating example is to use `LayoutObserver` to report the measurement upwards.

# Implementation

To best of my understanding of current `xilem` codebase, a new type of context, which is passed through the view tree, is needed. Some logic could be borrowed from the `Adapt` implementation. Additionally a type "addition" and "subtraction" machinery to manage the tuple type `T` of the `Inject` structs is needed.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reading PR 2170 and the existing Adapt implementation in the xilem codebase. Define the required view-tree context and the type addition/subtraction machinery for Provide and Inject, then validate the proposal's parent-to-child styling and upward dataflow examples. Done means the proposed environment pattern is implemented and usable by xilem views.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.