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
- the
Data
trait: How you represent your application model. - the
Widget
trait: How you represent your UI. - the
Lens
trait: How you associate parts of your model with parts of your UI.
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, seeLens
.
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'sevent
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 ofEvent
and the implementation of yourevent
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 alayout
or apaint
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'spaint
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. Lens
es 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 inName
, so we can't make theName
struct borrow the data both mutably and immutably. Even if we could in this case, things quickly get very complicated. Also, sometimesWidget
s need to keep a copy of the data around for use internally. For now the accepted best practice is to makeClone
ing 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 Rc
d 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 Clone
s, 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 Lens
es 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-Deref
ing 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.
Key
s, Value
s, 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:
-
Key
s should beconst
s with unique names. If you need to use a custom key, you should declare it as aconst
, 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");
-
Key
s must always be set before they are used. In practice this means that most keys are set when your application launches, usingAppLauncher::configure_env
. Once a key has been added to theEnv
, it cannot be deleted, although it can be overwritten. -
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 theEnv
, 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 Widget
s.
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.
Related projects
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
- Declarative UI patterns in Rust by Raph Levien at the Bay Area Rust Meetup December 3 2019
- Data oriented GUI in Rust by Raph Levien at the Bay Area Rust Meetup June 28 2018
Blog posts
People have been blogging about Druid
- Building a widget for Druid a blog post by Paul Miller on how to create custom Widgets that explains lots of Druid on the way