Go to file
Philipp Mildenberger 53a5354c2d
xilem_web: Rewrite modifiers (`Attributes`, `Classes` and `Styles`), and cleanup/extend docs (#699)
Previously the modifier systems had design issues i.e. bugs (non-deleted
styles/classes/attributes), and were unnecessary complex.
This aims to solve this (partly) by not using separate traits, but
concrete types and a different mechanism that is closer to how
`ElementSplice` works.

There's a few fundamental properties that composable type-based
modifiers need to support to avoid surprising/buggy behavior:

* Minimize actual changes to the underlying element, as DOM traffic is
expensive.
* Be compatible to memoization: e.g. a `Rotate` view should still be
applicable to possibly memoized transform values of the underlying
element.
* Recreation when the underlying element has changed (e.g. with a change
of variants of a `OneOf`).

To support all this, the modifier system needs to retain modifiers for
each modifier-view, and track its changes of the corresponding view.
Previously all elements were directly written and separated with markers
into a `Vec` to limit the boundaries of such views, but this had issues,
when e.g. all modifiers were deleted (e.g. clearing a `Vec` of classes),
by not reacting to this (I noticed that issue in the todomvc example
with the footer).

With this PR, the count of modifiers of a modifier-view are directly
stored either (hardcoded) in the view impl or its view state, which
cleans up the concrete modifier elements (such as `AttributeModifier`,
not including a separate `Marker` variant), and makes it less prone for
errors (and is slightly less memory-intensive).

The API to use these modifiers in modifier-views was also redesigned to
hopefully be more straight-forward/idiomatic. But as mentioned above
there's still challenges, which introduce complexity (which I'd like to
hide at least for simpler cases than these modifiers, likely in a future
PR).
All of this should now be documented in the new `modifier` module, where
now the modifiers `Attributes`, `Classes` and `Styles` reside. Other
views (like events) may also end up there...

One interesting aspect compared to the previous system is the use of a
new trait `With` for modifiers.
Instead of (roughly) `Element: WithStyle`, it works with `Element:
With<Styles>`.
This prevents all kinds of reimplementations of something like
`WithStyle` for elements.
This gets especially visible in the `one_of` module, which now can be
covered by a single blanket implementation.

Further the cargo-feature "hydration" was deleted, as it causes more
headaches to maintain than it really brings benefits (minimally less
binary size), depending on the future, it may or may not make sense to
reintroduce this.
2024-10-23 18:24:43 +00:00
.github ci: Update Rust stable version and typos-cli (#700) 2024-10-21 00:53:30 +00:00
docs/assets Add diagram to document xilem layers (#322) 2024-06-03 15:36:18 +00:00
masonry Tweak get_cursor code (#713) 2024-10-23 14:37:45 +00:00
xilem Replace WidgetMut methods with free functions. (#705) 2024-10-22 13:36:48 +00:00
xilem_core xilem_web: Rewrite modifiers (`Attributes`, `Classes` and `Styles`), and cleanup/extend docs (#699) 2024-10-23 18:24:43 +00:00
xilem_web xilem_web: Rewrite modifiers (`Attributes`, `Classes` and `Styles`), and cleanup/extend docs (#699) 2024-10-23 18:24:43 +00:00
.clippy.toml Add the `lens` component (#587) 2024-09-18 08:10:50 +00:00
.gitattributes Run tests in CI, and fix fonts for snapshot testing (#233) 2024-08-05 13:01:47 +00:00
.gitignore Sync Masonry's `.gitignore` with the repo's `.gitignore`. (#272) 2024-05-06 06:25:39 +00:00
.typos.toml feat(#574): reimplementation of `Image` widget `layout` function (#605) 2024-10-02 10:08:00 +00:00
ARCHITECTURE.md Remove `xilem_web_core` artifacts (#623) 2024-10-02 08:59:16 +00:00
AUTHORS Standardize copyright headers. (#246) 2024-05-03 08:50:39 +00:00
Cargo.lock Update to Kurbo 0.11.1 (#671) 2024-10-14 16:27:40 +00:00
Cargo.toml Update to Kurbo 0.11.1 (#671) 2024-10-14 16:27:40 +00:00
LICENSE Standardize copyright headers. (#246) 2024-05-03 08:50:39 +00:00
README.md docs: fix deps discrepency between Fedora/Ubuntu (#299) 2024-09-13 15:00:32 +00:00
rustfmt.toml Add rustfmt config (#305) 2024-05-14 09:46:59 +00:00

README.md

Xilem

An experimental Rust architecture for reactive UI

Xi Zulip dependency status Apache 2.0 Build Status Crates.io Docs

This repo contains an experimental architecture, implemented with a toy UI. At a very high level, it combines ideas from Flutter, SwiftUI, and Elm. Like all of these, it uses lightweight view objects, diffing them to provide minimal updates to a retained UI. Like SwiftUI, it is strongly typed.

Community

Xi Zulip

Discussion of Xilem development happens in the Xi Zulip, specifically the #xilem channel. All public content can be read without logging in

Project structure

This diagram gives an idea what the Xilem project is built on:

Xilem project layers

On a very coarse level, Xilem is built directly on top of xilem_core and Masonry, both of which are crates in this repository.

Then Masonry is built on top of:

  • winit for window creation.
  • Vello and wgpu for 2D graphics.
  • Parley for the text stack.
  • AccessKit for plugging into accessibility APIs.

Architecture

See ARCHITECTURE.md for details.

Overall program flow

Warning:

This README is a bit out of date. To understand more of what's going on, please read the blog post, Xilem: an architecture for UI in Rust.

Like Elm, the app logic contains centralized state. On each cycle (meaning, roughly, on each high-level UI interaction such as a button click), the framework calls a closure, giving it mutable access to the app state, and the return value is a view tree. This view tree is fairly short-lived; it is used to render the UI, possibly dispatch some events, and be used as a reference for diffing by the next cycle, at which point it is dropped.

We'll use the standard counter example. Here the state is a single integer, and the view tree is a column containing two buttons.

fn app_logic(data: &mut u32) -> impl View<u32, (), Element = impl Widget> {
    Column::new((
        Button::new(format!("count: {}", data), |data| *data += 1),
        Button::new("reset", |data| *data = 0),
    ))
}

These are all just vanilla data structures. The next step is diffing or reconciling against a previous version, now a standard technique. The result is an element tree. Each node type in the view tree has a corresponding element as an associated type. The build method on a view node creates the element, and the rebuild method diffs against the previous version (for example, if the string changes) and updates the element. There's also an associated state tree, not actually needed in this simple example, but would be used for memoization.

The closures are the interesting part. When they're run, they take a mutable reference to the app data.

Components

A major goal is to support React-like components, where modules that build UI for some fragment of the overall app state are composed together.

struct AppData {
    count: u32,
}

fn count_button(count: u32) -> impl View<u32, (), Element = impl Widget> {
    Button::new(format!("count: {}", count), |data| *data += 1)
}

fn app_logic(data: &mut AppData) -> impl View<AppData, (), Element = impl Widget> {
    Adapt::new(|data: &mut AppData, thunk| thunk.call(&mut data.count),
        count_button(data.count))
}

This adapt node is very similar to a lens (quite familiar to existing Druid users), and is also very similar to the [Html.map] node in Elm. Note that in this case the data presented to the child component to render, and the mutable app state available in callbacks is the same, but that is not necessarily the case.

Memoization

In the simplest case, the app builds the entire view tree, which is diffed against the previous tree, only to find that most of it hasn't changed.

When a subtree is a pure function of some data, as is the case for the button above, it makes sense to memoize. The data is compared to the previous version, and only when it's changed is the view tree build. The signature of the memoize node is nearly identical to Html.lazy in Elm:

fn app_logic(data: &mut AppData) -> impl View<AppData, (), Element = impl Widget> {
    Memoize::new(data.count, |count| {
        Button::new(format!("count: {}", count), |data: &mut AppData| {
            data.count += 1
        })
    }),
}

The current code uses a PartialEq bound, but in practice I think it might be much more useful to use pointer equality on Rc and Arc.

The combination of memoization with pointer equality and an adapt node that calls Rc::make_mut on the parent type is actually a powerful form of change tracking, similar in scope to Adapton, self-adjusting computation, or the types of binding objects used in SwiftUI. If a piece of data is rendered in two different places, it automatically propagates the change to both of those, without having to do any explicit management of the dependency graph.

I anticipate it will also be possible to do dirty tracking manually - the app logic can set a dirty flag when a subtree needs re-rendering.

Optional type erasure

By default, view nodes are strongly typed. The type of a container includes the types of its children (through the ViewTuple trait), so for a large tree the type can become quite large. In addition, such types don't make for easy dynamic reconfiguration of the UI. SwiftUI has exactly this issue, and provides AnyView as the solution. Ours is more or less identical.

The type erasure of View nodes is not an easy trick, as the trait has two associated types and the rebuild method takes the previous view as a &Self typed parameter. Nonetheless, it is possible. (As far as I know, Olivier Faure was the first to demonstrate this technique, in Panoramix, but I'm happy to be further enlightened)

Prerequisites

Linux and BSD

You need to have installed pkg-config, clang, and the development packages of wayland, libxkbcommon, libxcb, and vulkan-loader. Most distributions have pkg-config installed by default.

To install the remaining packages on Fedora, run:

sudo dnf install clang wayland-devel libxkbcommon-x11-devel libxcb-devel vulkan-loader-devel

To install the remaining packages on Debian or Ubuntu, run:

sudo apt-get install clang libwayland-dev libxkbcommon-x11-dev libvulkan-dev

Minimum supported Rust Version (MSRV)

This version of Xilem has been verified to compile with Rust 1.79 and later.

Future versions of Xilem might increase the Rust version requirement. It will not be treated as a breaking change and as such can even happen with small patch releases.

Click here if compiling fails.

As time has passed, some of Xilem's dependencies could have released versions with a higher Rust requirement. If you encounter a compilation issue due to a dependency and don't want to upgrade your Rust toolchain, then you could downgrade the dependency.

# Use the problematic dependency's name and version
cargo update -p package_name --precise 0.1.1

License

Licensed under the Apache License, Version 2.0 (LICENSE or http://www.apache.org/licenses/LICENSE-2.0)

Some files used for examples are under different licenses:

  • The font file (RobotoFlex-Subset.ttf) in xilem/resources/fonts/roboto_flex/ is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).
  • The data file (status.csv) in xilem/resources/data/http_cats_status/ is licensed solely as documented in that folder (and is not licensed under the Apache License, Version 2.0).

Contribution

Contributions are welcome by pull request. The Rust code of conduct applies.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be licensed as above, without any additional terms or conditions.