Druid

UNMAINTAINED

The Druid project has been discontinued.

New development effort moved on to Xilem, which has a lot of fundamental changes to allow for a wider variety of applications with better performance, but it also heavily inherits from Druid. We see Xilem as the future of Druid.

Introduction

Druid is a framework for building simple graphical applications.

Druid is composed of a number of related projects. druid-shell is a low-level library that provides a common abstraction for interacting with the current OS & window manager. piet is an abstraction for doing 2D graphics; kurbo is a library for 2D geometry; and druid itself is an opinionated set of high-level APIs for building cross-platform desktop applications.

The framework is data oriented. It shares many ideas (and is directly inspired by) contemporary declarative UI frameworks such as Flutter, Jetpack Compose, and SwiftUI, while also attempting to be conceptually simple and largely non-magical. A programmer familiar with Rust should be able to understand how Druid works without special difficulty.

Prerequisites

This tutorial assumes basic familiarity with Rust and a working setup with the basic tooling like Rustup and Cargo. This tutorial will use stable Rust (v1.65.0 at the time of writing) and the latest released version of Druid (v0.8).

Key Concepts

Get started with Druid

This chapter will walk you through setting up a simple Druid application from start to finish.

Setting up Druid dependencies

If you're on Linux or OpenBSD, you'll need to install GTK-3's development kit first.

Linux

On Linux, Druid requires gtk+3.

On Ubuntu this can be installed with

> sudo apt-get install libgtk-3-dev

On Fedora

> sudo dnf install gtk3-devel glib2-devel

See GTK installation page for more installation instructions.

OpenBSD

On OpenBSD, Druid requires gtk+3; install from packages:

> pkg_add gtk+3

Starting a project

Create a new cargo binary crate, and add druid as a dependency:

> cargo new my-druid-app
      Created binary (application) `my-druid-app` package
> cd my-druid-app
> cargo add druid

You should now have a stub of a project:

> tree
.
├── Cargo.lock
├── Cargo.toml
└── src
    └── main.rs

Hello world

To show a minimal window with a label, write the following code in your main.rs:

use druid::widget::Label;
use druid::{AppLauncher, Widget, WindowDesc};

fn build_ui() -> impl Widget<()> {
    Label::new("Hello world")
}

fn main() {
    let main_window = WindowDesc::new(build_ui())
        .window_size((600.0, 400.0))
        .title("My first Druid App");
    let initial_data = ();

    AppLauncher::with_window(main_window)
        .launch(initial_data)
        .expect("Failed to launch application");
}

In our main function we create an AppLauncher, pass it a WindowDesc, and launch it. We use build_ui to create a tree of widgets to pass to our WindowDesc. For now this tree consists of one simple label widget.

This is a very simple example application, using only the bare minimum of features. We can do something more complex.

Add more widgets

The first thing we could do to make our example application more interesting is to display more than one widget. However, WindowDesc::new expects a function that returns only one Widget. We also need a way to tell Druid how to lay out our widgets.

What we need to do is initialize our WindowDesc with a widget tree, with a single widget at the root. Some widgets can have children, and know how to lay them out; these are called container widgets.

We describe our window as a widget tree with container widgets as nodes, and label widgets as the leaves. Our build_ui function is then responsible for building this widget tree.

As an example, we'll build a todo-list app. At first, this app will have two columns, one with the list, and one with a placeholder for a button, each in a box with visible borders. We'll need to use the Split, Flex and Container widgets:

use druid::widget::{Container, Flex, Split};
use druid::Color;

// ...

fn build_ui() -> impl Widget<()> {
    Split::columns(
        Container::new(
            Flex::column()
                .with_flex_child(Label::new("first item"), 1.0)
                .with_flex_child(Label::new("second item"), 1.0)
                .with_flex_child(Label::new("third item"), 1.0)
                .with_flex_child(Label::new("fourth item"), 1.0),
        )
        .border(Color::grey(0.6), 2.0),
        Container::new(
            Flex::column()
                .with_flex_child(Label::new("Button placeholder"), 1.0)
                .with_flex_child(Label::new("Textbox placeholder"), 1.0),
        )
        .border(Color::grey(0.6), 2.0),
    )
}

We get a UI which is starting to look like a real application. Still, it's inherently static. We would like to add some interactivity, but before we can do that, our next step will be to make the UI data-driven.

Widget data

You may have noticed that our build_ui() function returns impl Widget<()>. This syntax describes an existential type which implements the Widget trait, with a generic parameter.

This generic parameter is the Widget's data. Since our UI so far has been stateless, the data is the unit type. But since we're writing a todo-list, we'll want our widget to depend on the list data:

use im::Vector;
type TodoList = Vector<String>;

// ...

fn build_ui() -> impl Widget<TodoList> {
    // ...
}

Here we're using a Vector from the im crate; for reasons we'll get into later, we can't use the standard library's Vec as our data. But im::Vector is functionally equivalent to std::vec::Vec.

To build a UI that changes depending on our widget data, we use the List widget, and Label::dynamic:

use druid::widget::List;

// ...

fn build_ui() -> impl Widget<TodoList> {
    Split::columns(
        Container::new(
            // Dynamic list of Widgets
            List::new(|| Label::dynamic(|data, _| format!("List item: {data}"))),
        )
        .border(Color::grey(0.6), 2.0),
        Container::new(
            Flex::column()
                .with_flex_child(Label::new("Button placeholder"), 1.0)
                .with_flex_child(Label::new("Textbox placeholder"), 1.0),
        )
        .border(Color::grey(0.6), 2.0),
    )
}

List is a special widget that takes a collection as data, and creates one widget with per collection item, with the item as data. In other words, our List implements Widget<Vector<String>> while the label returned by Label::dynamic implements Widget<String>. This is all resolved automatically by type inference.

Label::dynamic creates a label whose content depends on the data parameter.

Now, to test our UI, we can launch it with a hardcoded list:

use im::vector;

// ...

fn main() {
    let main_window = WindowDesc::new(build_ui())
        .window_size((600.0, 400.0))
        .title("My first Druid App");
    let initial_data = vector![
        "first item".into(),
        "second item".into(),
        "third item".into(),
        "foo".into(),
        "bar".into(),
    ];

    AppLauncher::with_window(main_window)
        .launch(initial_data)
        .expect("Failed to launch application");
}

We can now change the contents of the UI depending on the data we want to display; but our UI is still static. To add user interaction, we need a way to modify our data.

Interaction widgets

First, to interact with our UI, we add a button:

use druid::widget::Button;

// ...

fn build_ui() -> impl Widget<TodoList> {
    // ...

    // Replace `Label::new("Button placeholder")` with
    Button::new("Add item")

    // ...
}

If you build this, you'll notice clicking the button doesn't do anything. We need to give it a callback, that will take the data as parameter and mutate it:

fn build_ui() -> impl Widget<TodoList> {
    // ...

    Button::new("Add item")
        .on_click(|_, data: &mut Vector<String>, _| data.push_back("New item".into()))

    // ...
}

Now, clicking on the button adds an item to our list, but it always adds the same item. To change this, we need to add a textbox to our app, which will require that we make our data type a bit more complex.

Selecting a structure's field with lenses

To complete our todo-list, we need to change our app data type. Instead of just having a list of strings, we need to have a list and a string representing the next item to be added:

struct TodoList {
    items: Vector<String>,
    next_item: String,
}

However, now we have a problem: our List widget which expected a Vector<...> won't know how to handle a struct. So, we need to modify Druid's dataflow so that, given the TodoList above, the List widget will have access to the items field. This is done with a Lens, which we'll explain next chapter.

Furthermore, to pass our type as the a generic parameter to Widget, we need it to implement the Data trait (and Clone), more on that next chapter.

So, given the two requirements above, our declaration will actually look like:

use druid::{Data, Lens};

#[derive(Clone, Data, Lens)]
struct TodoList {
    items: Vector<String>,
    next_item: String,
}

Among other things, the above declaration defines two lenses, TodoList::items and TodoList::next_item, which take a TodoList as input and give a mutable reference to its items and next_item fields, respectively.

Next, we'll use the LensWrap widget wrapper to pass items to our List widget:

use druid::widget::LensWrap;

// ...

fn build_ui() -> impl Widget<TodoList> {
    // ...

    // Replace previous List with:
    LensWrap::new(
        List::new(|| Label::dynamic(|data, _| format!("List item: {data}"))),
        TodoList::items,
    )

    // ...
}

We also need to modify the callback of our button:

fn build_ui() -> impl Widget<TodoList> {
    // ...

    // Replace previous Button with:
    Button::new("Add item").on_click(|_, data: &mut TodoList, _| {
        data.items.push_back(data.next_item.clone());
        data.next_item = String::new();
    })

    // ...
}

Finally, we add a textbox to our widget with TodoList::next_item as its data:

use druid::widget::TextBox;

// ...

fn build_ui() -> impl Widget<TodoList> {
    // ...

    // Replace `Label::new("Textbox placeholder")` with
    LensWrap::new(TextBox::new(), TodoList::next_item)

    // ...
}

Now, when we push the button, whatever was in the textbox is added to the list.

Putting it all together

If we pull all the code we have written so far, our main.rs now looks like this:

use druid::widget::Label;
use druid::{AppLauncher, Widget, WindowDesc};
use druid::widget::{Container, Flex, Split};
use druid::Color;
use druid::widget::List;
use im::Vector;
use im::vector;
use druid::widget::Button;
use druid::{Data, Lens};
use druid::widget::LensWrap;
use druid::widget::TextBox;

fn build_ui() -> impl Widget<TodoList> {
    Split::columns(
        Container::new(
            // Dynamic list of Widgets
            LensWrap::new(
                List::new(|| Label::dynamic(|data, _| format!("List item: {data}"))),
                TodoList::items,
            ),
        )
        .border(Color::grey(0.6), 2.0),
        Container::new(
            Flex::column()
                .with_flex_child(
                    Button::new("Add item").on_click(|_, data: &mut TodoList, _| {
                        data.items.push_back(data.next_item.clone());
                        data.next_item = String::new();
                    }),
                    1.0,
                )
                .with_flex_child(LensWrap::new(TextBox::new(), TodoList::next_item), 1.0),
        )
        .border(Color::grey(0.6), 2.0),
    )
}

fn main() {
    let main_window = WindowDesc::new(build_ui())
        .window_size((600.0, 400.0))
        .title("My first Druid App");
    let initial_data = TodoList {
        items: vector![
            "first item".into(),
            "second item".into(),
            "third item".into(),
            "foo".into(),
            "bar".into(),
        ],
        next_item: String::new(),
    };

    AppLauncher::with_window(main_window)
        .launch(initial_data)
        .expect("Failed to launch application");
}

We now have a list of items, which we can add to by filling a textbox and clicking a button.

Dataflow and the Data trait

The Druid architecture is based on a two-way dataflow.

At the root level, you define the application state, which is passed to each child widget as associated data. Some Widgets (eg LensWrap) will only pass a subset of that data to their children.

Some widgets (eg Button, TextBox, Checkbox) can mutate the data passed to them by their parents in reaction to user events. The data mutated in a child widget is also changed in the parent widgets, all the way to the root.

When you mutate a widget's associated data, Druid compares the old and new version, and propagates the change to the widgets that are affected by the change.

Note that, in all that workflow, Widgets don't actually store their associated data. A Button<Vector<String>> doesn't actually store a Vector<String>, instead the framework stores one per button, which is provided to widget methods.

For this to work, your model must implement the Clone and Data traits. The Data trait has a single method:

    /// Determine whether two values are the same.
    ///
    /// This is intended to always be a fast operation. If it returns
    /// `true`, the two values *must* be equal, but two equal values
    /// need not be considered the same here, as will often be the
    /// case when two copies are separately allocated.
    ///
    /// Note that "equal" above has a slightly different meaning than
    /// `PartialEq`, for example two floating point NaN values should
    /// be considered equal when they have the same bit representation.
    fn same(&self, other: &Self) -> bool;

This method checks for equality, but allows for false negatives.

Performance

It is important that your data is cheap to clone and cheap to compare; we encourage the use of reference counted pointers to allow cheap cloning of more expensive types. Arc and Rc have blanket Data impls that do pointer comparison, so if you have a type that does not implement Data, you can always just wrap it in one of those smart pointers.

Collections

Data is expected to be cheap to clone and cheap to compare, which can cause issues with collection types. For this reason, Data is not implemented for std types like Vec or HashMap.

You can always put these types inside an Rc or an Arc, or if you're dealing with larger collections you can build Druid with the im feature, which brings in the im crate, and adds a Data impl for the collections there. The im crate is a collection of immutable data structures that act a lot like the std collections, but can be cloned efficiently.

Derive

Data can be derived. This is recursive; it requires Data to be implemented for all members. For 'C style' enums (enums where no variant has any fields) this also requires an implementation of PartialEq. Data is implemented for a number of std types, including all primitive types, String, Arc, Rc, as well as Option, Result, and various tuples whose members implement Data.

Here is an example of using Data to implement a simple data model:

#![allow(unused)]
fn main() {
use druid::Data;
use std::sync::Arc;

#[derive(Clone, Data)]
/// The main model for a todo list application.
struct TodoList {
    items: Arc<Vec<TodoItem>>,
}

#[derive(Clone, Data)]
/// A single todo item.
struct TodoItem {
    category: Category,
    title: String,
    note: Option<String>,
    completed: bool,

    // `Data` is implemented for any `Arc`.
    due_date: Option<Arc<DateTime>>,

    // You can specify a custom comparison fn
    // (anything with the signature (&T, &T) -> bool).
    #[data(same_fn = "PartialEq::eq")]
    added_date: DateTime,

    // You can specify that a field should
    // be skipped when computing same-ness
    #[data(ignore)]
    debug_timestamp: usize,
}

#[derive(Clone, Data, PartialEq)]
/// The three types of tasks in the world.
enum Category {
    Work,
    Play,
    Revolution,
}
}

Mapping Data with lenses

In Druid, most container widgets expect their children to have the same associated data. If you have a Flex<Foobar>, you can only append widgets that implement Widget<Foobar> to it.

In some cases, however, you want to compose widgets that operate on different subsets of the data. Maybe you want to add two widgets to the above Flex, one that uses the field foo and another that uses the field bar, and they might respectively implement Widget<Foo> and Widget<Bar>.

Lenses allow you to bridge that type difference. A lens is a type that represents a two-way mapping between two data types. That is, a lens from X to Y can take an instance of X and give you an instance of Y, and can take a modified Y and apply the modification to X.

To expand on our Foobar example:

#![allow(unused)]
fn main() {
#[derive(Lens)]
struct Foobar {
    foo: Foo,
    bar: Bar,
}
}

The derive macro above generates two lenses: Foobar::foo and Foobar::bar. Foobar::foo can take an instance of Foobar and give you a shared or mutable reference to its foo field. Finally, the type LensWrap can take that lens and use it to map between different widget types:

#![allow(unused)]
fn main() {
fn build_foo() -> impl Widget<Foo> {
    // ...
}

fn build_bar() -> impl Widget<Bar> {

}

fn build_foobar() -> impl Widget<Foobar> {
    Flex::column()
        .with_child(
            LensWrap::new(build_foo(), Foobar::foo),
        )
        .with_child(
            LensWrap::new(build_bar(), Foobar::bar),
        )
}
}

See the Lens chapter for a more in-depth explanation of what lenses are and how they're implemented.

Widgets and the Widget trait

The Widget trait represents components of your UI. Druid includes a set of built-in widgets, and you can also write your own. You combine the built-in and custom widgets to create a widget tree; you will start with some single root widget, which will (generally) have children, which may themselves have children, and so on. Widget has a generic parameter T that represents the Data handled by that widget. Some widgets (such as layout widgets) may be entirely agnostic about what sort of Data they encounter, while other widgets (such as a slider) may expect a single type (such as f64).

Note: For more information on how different parts of your Data are exposed to different widgets, see Lens.

At a high level, Druid works like this:

  • event: an Event arrives from the operating system, such as a key press, a mouse movement, or a timer firing. This event is delivered to your root widget's event method. This method is provided mutable access to your application model; this is the only place where your model can change. Depending on the type of Event and the implementation of your event method, this event is then delivered recursively down the tree until it is handled.
  • update: After this call returns, the framework checks to see if the data was mutated. If so, it calls your root widget's update method, passing in both the new data as well as the previous data. Your widget can then update any internal state (data that the widget uses that is not part of the application model, such as appearance data) and can request a layout or a paint call if its appearance is no longer valid.
  • After update returns, the framework checks to see if any widgets in a given window have indicated that they need layout or paint. If so, the framework will call the following methods:
  • layout: This is where the framework determines where to position each widget on the screen. Druid uses a layout system heavily inspired by Flutter's box layout model: widgets are passed constraints, in the form of a minimum and a maximum allowed size, and they return a size in that range.
  • paint: After layout, the framework calls your widget's paint method. This is where your widget draws itself, using a familiar imperative 2D graphics API.
  • In addition to these four methods, there is also lifecycle, which is called in response to various changes to framework state; it is not called predictably during event handling, but only when extra information (such as if a widget has gained focus) happens as a consequence of other events.

For more information on implementing these methods, see Creating custom widgets.

Modularity and composition

Widgets are intended to be modular and composable, not monolithic. For instance, widgets generally do not control their own alignment or padding; if you have a label, and you would like it to have 8dp of horizontal padding and 4dp of vertical padding, you can just do,

use druid::widget::{Label, Padding};

fn padded_label() {
    let label: Label<()> = Label::new("Humour me");
    let padded = Padding::new((4.0, 8.0), label);
}

to force the label to be center-aligned if it is given extra space you can write,

use druid::widget::Align;

fn align_center() {
    let label: Label<()> = Label::new("Center me");
    let centered = Align::centered(label);
}

Builder methods and WidgetExt

Widgets are generally constructed using builder-style methods. Unlike the normal builder pattern, we generally do not separate the type that is built from the builder type; instead the builder methods are on the widget itself.

use druid::widget::Stepper;

fn steppers() {
    // A Stepper with default parameters
    let stepper1 = Stepper::new();

    // A Stepper that operates over a custom range
    let stepper2 = Stepper::new().with_range(10.0, 50.0);

    // A Stepper with a custom range *and* a custom step size, that
    // wraps around past its min and max values:
    let stepper3 = Stepper::new()
        .with_range(10.0, 50.0)
        .with_step(2.5)
        .with_wraparound(true);
}

Additionally, there are a large number of helper methods available on all widgets, as part of the WidgetExt trait. These builder-style methods take one widget and wrap it in another. The following two functions produce the same output:

Explicit:

use druid::widget::{Align, Padding, Stepper};

fn padded_stepper() {
    let stepper = Stepper::new().with_range(10.0, 50.0);
    let padding = Padding::new(8.0, stepper);
    let padded_and_center_aligned_stepper = Align::centered(padding);
}

WidgetExt:

use druid::widget::{Stepper, WidgetExt};

fn padded_stepper() {
    let padded_and_center_aligned_stepper =
        Stepper::new().with_range(10.0, 50.0).padding(8.0).center();
}

These builder-style methods also exist on containers. For instance, to create a stack of three labels, you can do:

use druid::widget::Flex;

fn flex_builder() -> Flex<()> {
    Flex::column()
        .with_child(Label::new("Number One"))
        .with_child(Label::new("Number Two"))
        .with_child(Label::new("Some Other Number"))
}

Lenses and the Lens trait

One of the key abstractions in druid along with Data is the Lens trait. This page explains what they are, and then how to use them. Lenses are a complex but powerful concept, that allow you to abstract over the notion of "X owns an instance of Y".

Fundamentals: Definition and Implementation

Definition

Let's start with the (simplified) definition of a Lens:

#![allow(unused)]
fn main() {
pub trait Lens<T, U> {
    fn with<F: FnOnce(&U)>(&self, data: &T, f: F);

    fn with_mut<F: FnOnce(&mut U)>(&self, data: &mut T, f: F);
}
}

The first thing to notice is the generics on the Lens itself. There are 3 types involved in the lens: Self (the lens itself), T and U. The two type parameters represent the mismatch that lenses solve: we have a function that operates on U, and an object of type T, so we need to transform T into U somehow.

Implementation

As an example, let's write a manual implementation of the Lens trait:

#![allow(unused)]
fn main() {
struct Container {
    inner: String,
    another: String,
}

// This lens doesn't have any data, because it will always map to the same field.
// A lens that mapped to, say, an index in a collection, would need to store that index.
struct InnerLens;

// Our lens will apply functions that operate on a `String` to a `Container`.
impl Lens<Container, String> for InnerLens {
    fn with<F: FnOnce(&String)>(&self, data: &Container, f: F) {
        f(&data.inner);
    }

    fn with_mut<F: FnOnce(&mut String)>(&self, data: &mut Container, f: F) {
        f(&mut data.inner);
    }
}
}

The implementation is straightforward: it projects the given function onto the inner field of our struct. (Notice that this isn't the only valid lens from Container to String we could have made - we could also project from Container to another).

You'll also notice that both methods take an immutable reference to self, even the mut variant. The lense itself should be thought of as a fixed value that knows how to do the mapping. In the above case it contains no data, and will likely not even be present in the final compiled/optimized code.

Now for a slightly more involved example

#![allow(unused)]
fn main() {
struct Container2 {
    first_name: String,
    last_name: String,
    age: u16, // in the future maybe people will live past 256?
}

struct Name {
    first: String,
    last: String,
}

struct NameLens;

impl Lens<Container2, Name> for NameLens {
    fn with<F: FnOnce(&Name)>(&self, data: &Container2, f: F) {
        let first = data.first_name.clone();
        let last = data.last_name.clone();
        f(&Name { first, last });
    }

    fn with_mut<F: FnOnce(&mut Name)>(&self, data: &mut Container2, f: F) {
        let first = data.first_name.clone();
        let last = data.last_name.clone();
        let mut name = Name { first, last };
        f(&mut name);
        data.first_name = name.first;
        data.last_name = name.last;
    }
}
}

Side note: if you try doing this with struct Name<'a> { first: &'a String, ..., you'll find that it's not possible to be generic over the mutability of the fields in Name, so we can't make the Name struct borrow the data both mutably and immutably. Even if we could in this case, things quickly get very complicated. Also, sometimes Widgets need to keep a copy of the data around for use internally. For now the accepted best practice is to make Cloneing cheap and use that.

Now as I'm sure you've realised, the above is very inefficient. Given that we will be traversing our data very often, we need it to be cheap. (This wasn't a problem before, because when we don't need to build the inner type, we can just use references. It also wouldn't be a problem if our data was cheap to copy/clone, for example any of the primitive number types u8, ... f64.) Luckily, this is exactly the kind of thing that rust excels at. Let's rewrite the above example to be fast!

#![allow(unused)]
fn main() {
struct Container2 {
    first_name: Rc<String>,
    last_name: Rc<String>,
    age: u16,
}

struct Name {
    first: Rc<String>,
    last: Rc<String>,
}

struct NameLens;

impl Lens<Container2, Name> for NameLens {
    // .. identical to previous example
}
}

As you'll see, we've introduced Rc: the reference-counted pointer. You will see this and its multithreaded cousin Arc used pervasively in the examples. Now, the only time we actually have to copy memory is when Rc::make_mut is called in the f in with_mut. This means that in the case where nothing changes, all we will be doing is incrementing and decrementing reference counts. Moreover, we give the compiler the opportunity to inline f and with/with_mut, making this abstraction potentially zero-cost (disclaimer: I haven't actually studied the produced assembly to validate this claim).

The trade-off is that we introduce more complexity into the Name type: to make changes to the data we have to use Rc::make_mut to get mutable access to the String. (The code in the lens will ensure that the newer copy of the Rcd data is saved to the outer type.) This means the writing fast Druid code requires knowledge of the Rust pointer types (Rc/Arc, and also potentially RefCell/Mutex).

We can actually do even better than this. Suppose that we are working on a vector of data rather than a string. We can import the im crate to get collections that use structural sharing, meaning that even when the vector is mutated, we only Clone what we need to. Because im is so useful, it is included in druid (behind the im feature).

#![allow(unused)]
fn main() {
struct Container2 {
    // Pretend that it's the 1980s and we store only ASCII names.
    first_name: im::Vector<u8>,
    last_name: im::Vector<u8>,
    age: u16,
}

struct Name {
    first: im::Vector<u8>,
    last: im::Vector<u8>,
}

struct NameLens;

impl Lens<Container2, Name> for NameLens {
    // .. identical to previous example
}
}

Now in addition to almost free Clones, we also have cheap incremental updates to the data itself. That means your UI won't get slowdowns if your data structure gets very large (eg a list of entries in a database).

Hopefully, this makes sense to you. This was a technical overview of lenses as generic data structures. The next section will cover how lenses are integrated in Druid in more detail.

Lenses in Druid

Now on to the more fun bit: how we can use Lenses to get all those lovely qualities we talked about in the introduction. What you'll notice in this section is that we rarely have to build lenses ourself: we can often get what we want using the Lens proc macro, or through the functions in LensExt.

Deriving lenses

Let's go back to the first example we looked at, with one of the fields removed for simplicity:

#![allow(unused)]
fn main() {
#[derive(Lens)]
struct Container {
    inner: u8,
}
}

Let's look at the code that gets generated (I captured this using cargo-expand, then removed some unimportant bits).

#![allow(unused)]
fn main() {
pub mod container_derived_lenses {
    #[allow(non_camel_case_types)]
    pub struct inner;
}
impl druid::Lens<Container, u8> for container_derived_lenses::inner {
    fn with<V, F: FnOnce(&u8) -> V>(&self, data: &Container, f: F) -> V {
        f(&data.inner)
    }
    fn with_mut<V, F: FnOnce(&mut u8) -> V>(&self, data: &mut Container, f: F) -> V {
        f(&mut data.inner)
    }
}
#[allow(non_upper_case_globals)]
impl Container {
    pub const inner: container_derived_lenses::inner = container_derived_lenses::inner;
}
}

The macro has created a new module with a long name, put a struct in it that breaks the type naming convention, implemented Lens on the type, and then put a constant in an impl block for your data type with the same name. The upshot is that we can do StructName::field_name and get a lens from the struct to its field.

Side note: Doing this makes using the lenses very simple (you just do StructName::field_name), but it can be a bit confusing, because of breaking the naming conventions. This is the reason I've included the expanded code in the page.

Composing lenses

If I told you that the concept of lenses comes from Haskell (the functional megolith), I'm sure you won't be surprised when I also tell you that they really excel when it comes to composition. Let's say we have an outer struct that contains an inner struct, with the inner struct containing a String. Now let's say we want to tell a label widget to display the string as text in a label. We could write a lens from the outer struct to the string, which would look something like f(&outer.inner.text), but actually we don't need to do this: we can use the then combinator. The full example is below

#![allow(unused)]
fn main() {
#[derive(Lens)]
struct Outer {
    inner: Inner,
}

#[derive(Lens)]
struct Inner {
    text: String
}

// `composed_lens` will contain a lens that goes from `Outer` through `Inner` to `text`.
let composed_lens = Outer::inner.then(Inner::text);
}

LensExt contains a few more useful methods for handling things like negating a boolean, or auto-Derefing a value.

There are also 3 special structs in druid::lens: Constant, Identity and Unit. Constant is a lens that always returns the same value, and always discards any changes, while Identity is a lens that does nothing. You might say "what is the point of a lens that does nothing", which would be a fair question. Well, there are some places where a lens is required, and having an identity allows the user to say act as if there was no lens. It's also used to begin a composition chain using the combinators like then. Unit is a special case of Constant where the constant in question is ().

The lens macro

Finally, there is a macro for constructing lenses on the fly. It allows you to lens into fields of a struct you don't control (so you can't derive Lens for it), it also allows lensing into tuples and tuple structs, and lastly it will create index lenses into slices.

Wrapping up

Whew, that was quite complicated. Hopefully now you have a solid understanding of the problem that lenses solve, how they solve it, and how to use them effectively.

If any parts of this page are confusing, please open an issue on the issue tracker or mention it on zulip, and we will see if we can improve the docs (and clear up any misunderstandings you might have).

The Env

The Env represents the environment; it is intended as a way of managing and accessing state about your specific application, such as color schemes, localized strings, and other resources.

The Env is created when the application is launched, and is passed down to all widgets. The Env may be modified at various points in the tree; values in the environment can be overridden with other values of the same type, but they can never be removed. If something exists in the Env at a given level of the tree, it will exist for everything 'below' that level; that is, for all children of that widget.

Keys, Values, and themes

The most prominent role of Env is to store a set of typed keys and values. The Env can only store a few types of things; these are represented by the Value type, which looks like this:

pub enum Value {
    Point(Point),
    Size(Size),
    Rect(Rect),
    Insets(Insets),
    Color(Color),
    Float(f64),
    Bool(bool),
    UnsignedInt(u64),
    String(ArcStr),
    Font(FontDescriptor),
    RoundedRectRadii(RoundedRectRadii),
    Other(Arc<dyn Any + Send + Sync>),
}

The only way to get an item out of the Env is with a Key. A Key is a combination of a string identifier and a type.

You can think of this as strict types, enforced at runtime. It is the programmer's responsibility to ensure that the key used to get a value has the same type as the one used to set it. The API is aggressive about checking for misuse, and many methods will panic if anything is amiss. In practice this shouldn't almost never happen if you follow these guidelines:

  1. Keys should be consts with unique names. If you need to use a custom key, you should declare it as a const, and give it a unique name. By convention, you should namespace your keys using something like reverse-DNS notation, or even just prefixing them with the name of your app.

    const BAD_NAME: Key<f64> = Key::new("height");
    const GOOD_NAME: Key<f64> = Key::new("com.example.my-app.main-view-height");
  2. Keys must always be set before they are used. In practice this means that most keys are set when your application launches, using AppLauncher::configure_env. Once a key has been added to the Env, it cannot be deleted, although it can be overwritten.

  3. Values can only be overwritten by values of the same type. If you have a Key<f64>, assuming that key has already been added to the Env, you cannot replace it with any other type.

Assuming these rules are followed, Env should just work.

KeyOrValue

Druid includes a KeyOrValue type that is used for setting certain properties of widgets. This is a type that can be either a concrete instance of some type, or a Key that can be used to get that type from the Env.

const IMPORTANT_LABEL_COLOR: Key<Color> = Key::new("org.linebender.example.important-label-color");
const RED: Color = Color::rgb8(0xFF, 0, 0);

fn make_labels() {
    let with_value = Label::<()>::new("Warning!").with_text_color(RED);
    let with_key = Label::<()>::new("Warning!").with_text_color(IMPORTANT_LABEL_COLOR);
}

EnvScope

You may override values in the environment for a given widget (and all of its children) by using the EnvScope widget. This is easiest when combined with the env_scope method on WidgetExt:

fn scoped_label() {
    let my_label = Label::<()>::new("Warning!").env_scope(|env, _| {
        env.set(druid::theme::TEXT_COLOR, Color::BLACK);
        env.set(druid::theme::TEXT_SIZE_NORMAL, 18.0);
    });
}

Localization

Localization is currently half-baked.

The Env contains the localization resources for the current locale. A LocalizedString can be resolved to a given string in the current locale by calling its resolve method.

Resolution independence

What is a pixel anyway?

Pixel is short for picture element and although due to its popularity it has many meanings depending on context, when talking about pixels in the context of druid a pixel means always only one thing. It is the smallest configurable area of color that the underlying platform allows druid-shell to manipulate.

The actual physical display might have a different resolution from what the platform knows or uses. Even if the display pixel resolution matches the platform resolution, the display itself can control even smaller elements than pixels - the sub-pixels.

The shape of the physical pixel could be complex and definitely varies from display model to model. However for simplicity you can think of a pixel as a square which you can choose a color for.

Display pixel density

As technology advances the physical size of pixels is getting smaller and smaller. This allows display manufacturers to put more and more pixels into the same sized screen. The pixel densities of displays are increasing.

There is also an increasing variety in the pixel density of the displays used by people. Some might have a brand new 30" 8K UHD (7680px * 4320px) display, while others might still be rocking their 30" HD ready (1366px * 768px) display. It might even be the same person on the same computer with a multi-display setup.

The naive old school approach to UI

For a very long time UIs have been designed without thinking about pixel density at all. People tended to have displays with roughly similar pixel densities, so it all kind of worked most of the time. However it breaks down horribly in a modern world. The 200px * 200px UI that looks decent on that HD ready display is barely visible on the 8K UHD display. If you redesign it according to the 8K UHD display then it won't even fit on the HD ready screen.

Platform specific band-aids

Some platforms have mitigations in place where that small 200px * 200px UI will get scaled up by essentially taking a screenshot of it and enlarging the image. This will result in a blurry UI with diagonal and curved lines suffering the most. There is more hope with fonts where the vector information is still available to the platform, and instead of scaling up the image the text can be immediately drawn at the larger size.

A better solution

The application should draw everything it can with vector graphics, and have very large resolution image assets available where vectors aren't viable. Then at runtime the application should identify the display pixel density and resize everything accordingly. The vector graphics are easy to resize and the large image assets would be scaled down to the size that makes sense for the specific display.

An even better way

Druid aims to make all of this as easy and automatic as possible. Druid has expressive vector drawing capabilities that you should use whenever possible. Vector drawing is also used by the widgets that come included with Druid. Handling different pixel densities is done at the druid-shell level already. In fact pixels mostly don't even enter the conversation at the druid level. The druid coordinate system is instead measured in display points (dp), e.g. you might say a widget has a width of 100dp. Display points are conceptually similar to Microsoft's device-independent pixels, Google's density-independent pixels, Apple's points, and CSS's pixel units.

You describe the UI using display points and then Druid will automatically translate that into pixels based on the pixel density of the platform. Remember there might be multiple displays connected with different pixel densities, and your application might have multiple windows - with each window on a different display. It will all just work, because Druid will adjust the actual pixel dimensions based on the display that the window is currently located on.

Create custom widgets

The Widget trait is the heart of Druid, and in any serious application you will eventually need to create and use custom Widgets.

Painter and Controller

There are two helper widgets in Druid that let you customize widget behaviour without needing to implement the full widget trait: Painter and Controller.

Painter

The Painter widget lets you draw arbitrary custom content, but cannot respond to events or otherwise contain update logic. Its general use is to either provide a custom background to some other widget, or to implement something like an icon or another graphical element that will be contained in some other widget.

For instance, if we had some color data and we wanted to display it as a swatch with rounded corners, we could use a Painter:

fn make_color_swatch() -> Painter<Color> {
    Painter::new(|ctx: &mut PaintCtx, data: &Color, env: &Env| {
        let bounds = ctx.size().to_rect();
        let rounded = bounds.to_rounded_rect(CORNER_RADIUS);
        ctx.fill(rounded, data);
        ctx.stroke(rounded, &env.get(druid::theme::PRIMARY_DARK), STROKE_WIDTH);
    })
}

Painter uses all the space that is available to it; if you want to give it a set size, you must pass it explicit constraints, such as by wrapping it in a SizedBox:

fn sized_swatch() -> impl Widget<Color> {
    SizedBox::new(make_color_swatch()).width(20.0).height(20.0)
}

One other useful thing about Painter is that it can be used as the background of a Container widget. If we wanted to have a label that used our swatch as a background, we could do:

fn background_label() -> impl Widget<Color> {
    Label::dynamic(|color: &Color, _| {
        let (r, g, b, _) = color.as_rgba8();
        format!("#{r:X}{g:X}{b:X}")
    })
    .background(make_color_swatch())
}

(This uses the background method on WidgetExt to embed our label in a container.)

Controller

The Controller trait is sort of the inverse of Painter; it is a way to make widgets that handle events, but don't do any layout or drawing. The idea here is that you can use some Controller type to customize the behaviour of some set of children.

The Controller trait has event, update, and lifecycle methods, just like Widget; it does not have paint or layout methods. Also unlike Widget, all of its methods are optional; you can override only the method that you need.

There's one other difference to the Controller methods; it is explicitly passed a mutable reference to its child in each method, so that it can modify it or forward events as needed.

As an arbitrary example, here is how you might use a Controller to make a textbox fire some action (say doing a search) 300ms after the last keypress:

const ACTION: Selector = Selector::new("hello.textbox-action");
const DELAY: Duration = Duration::from_millis(300);

struct TextBoxActionController {
    timer: Option<TimerToken>,
}

impl TextBoxActionController {
    pub fn new() -> Self {
        TextBoxActionController { timer: None }
    }
}

impl Controller<String, TextBox<String>> for TextBoxActionController {
    fn event(
        &mut self,
        child: &mut TextBox<String>,
        ctx: &mut EventCtx,
        event: &Event,
        data: &mut String,
        env: &Env,
    ) {
        match event {
            Event::KeyDown(k) if k.key == Key::Enter => {
                ctx.submit_command(ACTION);
            }
            Event::KeyUp(k) if k.key == Key::Enter => {
                self.timer = Some(ctx.request_timer(DELAY));
                child.event(ctx, event, data, env);
            }
            Event::Timer(token) if Some(*token) == self.timer => {
                ctx.submit_command(ACTION);
            }
            _ => child.event(ctx, event, data, env),
        }
    }
}

More information

If you want more information about Druid this document contains links more tutorials, blogposts and youtube videos.

These three projects provide the basis that Druid works on

  • Piet An abstraction for 2D graphics.
  • Kurbo A Rust library for manipulating curves
  • Skribo A Rust library for low-level text layout

Projects using Druid

  • Kondo Save disk space by cleaning unneeded files from software projects.
  • jack-mixer A jack client that provides mixing, levels and a 3-band eq.
  • kiro-synth An in progress modular sound synthesizer.
  • psst A non-Electron GUI Spotify client.
  • flac_music A music player.
  • And many more

Projects that work with Druid (widgets etc)

  • No data filled in here

Presentations

Some presentations about Druid, its background and related topics have been recorded

Blog posts

People have been blogging about Druid