• Stars
    star
    3,444
  • Rank 12,968 (Top 0.3 %)
  • Language
    Rust
  • License
    Apache License 2.0
  • Created almost 2 years ago
  • Updated 3 months ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

An experimental Rust native UI framework

Xilem

An experimental Rust architecture for reactive UI

Xi Zulip dependency status Apache 2.0 Build Status

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 stream. All public content can be read without logging in

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

Building and running Xilem on Linux and BSD requires pkg-config, clang, and the development packages of libxkbcommon, libxcb, and vulkan-loader, to be installed.

Most distributions have pkg-config installed by default. To install the remaining packages on Fedora, run

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

To install them on Debian or Ubuntu, run

sudo apt-get install clang libxkbcommon-x11-dev pkg-config libvulkan-dev

License

Licensed under the Apache License, Version 2.0 (LICENSE or http://www.apache.org/licenses/LICENSE-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.

More Repositories

1

druid

A data-first Rust-native UI design toolkit.
Rust
9,543
star
2

vello

A GPU compute-centric 2D renderer.
Rust
2,315
star
3

piet

An abstraction for 2D graphics.
Rust
1,251
star
4

runebender

A font editor written in Rust.
Rust
762
star
5

kurbo

A Rust library for manipulating curves
Rust
698
star
6

masonry

Rust UI design toolkit - moved.
Rust
405
star
7

skribo

A Rust library for low-level text layout.
Rust
325
star
8

glazier

Deprecated Rust Window Creation Library
Rust
209
star
9

parley

Rich text layout library
Rust
197
star
10

piet-metal

Experimental Metal-based GPU renderer for piet 2D graphics.
Rust
140
star
11

bevy_vello

An integration to render with Vello in Bevy game engine.
Rust
117
star
12

spline

A spline for interactive 2D curve design
Rust
112
star
13

druid-widget-nursery

A place where Druid widgets come to mature before moving to the Druid repo.
Rust
87
star
14

velato

An integration to parse and render Lottie with Vello.
Rust
71
star
15

norad

Rust crate for working with Unified Font Object files
Rust
43
star
16

rbf-interp

An implementation of Radial Basis Function multidimensional interpolation
Rust
35
star
17

gpu-stroke-expansion-paper

Rust
35
star
18

2d.graphics

Repo for an ideational book on 2D graphics, plus tools to make images
26
star
19

peniko

Primitive types for styling vector graphics.
Rust
22
star
20

vello_svg

An integration to render SVG files with Vello.
Rust
15
star
21

wiki

Wiki and documentation for 2D graphics projects
9
star
22

android_trace

Support for Android NDK Tracing in Rust
Rust
6
star
23

rfcs

Suggestions for major changes to the linebender ecosystem
6
star
24

linebender.github.io

Main webpage for linebender org
SCSS
5
star
25

interpoli

Rust
3
star
26

runebender.app

Ruby
2
star
27

piet-snapshots

Data for snapshot testing of piet backends
2
star