Druid
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.
Druid 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.
Goals and Status
The current goal of Druid is to make it easy to write a program in Rust that
can present a GUI and accept user input. Running your program should be as
simple as cargo run
.
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.
Set up Druid
This tutorial assumes basic familliarity with Rust and a working setup with the basic tooling like Rustup and Cargo. This tutorial will use stable Rust (v1.39.0 at the time of writing) and the latest released version of Druid.
This tutorial will first walk you through setting up the dependencies for developing a Druid application, then it will show you how to set up a basic application, build it and run it.
Setting up Druid dependencies
In addition to including the druid
library in your project
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.
Starting a project
Starting a project is as easy as creating an empty application with
cargo new my-application
and adding the druid dependency to your Cargo.toml
[dependencies]
druid = "0.7.0"
// or to be on the bleeding edge:
druid = { git = "https://github.com/linebender/druid.git" }
Get started with Druid
this is outdated, and should be replaced with a walkthrough of getting a simple app built and running.
This chapter will walk you through setting up a simple Druid application from start to finish.
Set up a Druid project
Setting up a project is a simple as creating a new Rust project;
> cargo new druid-example
And then adding Druid as a dependency to Cargo.toml
[dependencies]
druid = "0.7.0"
To show a minimal window with a label replace main.rs
with this;
use druid::{AppLauncher, WindowDesc, Widget, PlatformError};
use druid::widget::Label;
fn build_ui() -> impl Widget<()> {
Label::new("Hello world")
}
fn main() -> Result<(), PlatformError> {
AppLauncher::with_window(WindowDesc::new(build_ui)).launch(())?;
Ok(())
}
In our main function we create an AppLauncher
, pass it a WindowDesc
that wraps build_ui function and launch it. Druid will use our build_ui
function to build and rebuild our main window every time it needs to refresh. build_ui
returns a tree of widgets. For now this tree consists of one simple label widget.
This is a very simple example application and it's missing some important pieces. We will add these in the coming few paragraphs.
Draw more widgets
The first thing we could do to make our example application more interesting is to draw more than one widget. Unfortunately WindowDesc::new
expects a function that returns only one Widget. We also need a way to tell Druid how to lay-out our widgets.
We solve both these problems by passing in a widget-tree with one single widget at the top. Widgets can have children and widgets higher up in the tree know how to lay-out their children. That way we describe a window as a widget-tree with layout containers as the branches and widgets as the leaves. Our build_ui
function is then responsible for building this widget tree.
To see how this works we will divide our window in four. We'll have two rows and two columns with a single label in each of the quadrants. We can lay-out our labels using the Flex
widget.
fn build_ui() -> impl Widget<()> {
Flex::row()
.with_flex_child(
Flex::column()
.with_flex_child(Label::new("top left"), 1.0)
.with_flex_child(Label::new("bottom left"), 1.0),
1.0)
.with_flex_child(
Flex::column()
.with_flex_child(Label::new("top right"), 1.0)
.with_flex_child(Label::new("bottom right"), 1.0),
1.0)
}
This looks nice but the labels on the left are drawn right against the window edge, so we needs some padding. Lets say we also want to center the two bottom labels. Unlike many other UI frameworks, widgets in Druid don't have padding or alignment properties themselves. Widgets are kept as simple as possible.
Features like padding or alignment are implemented in separate widgets. To add padding you simply wrap the labels in a Padding
widget. Centering widgets is done using the Align
widget set to centered
.
fn build_ui() -> impl Widget<()> {
Padding::new(
10.0,
Flex::row()
.with_flex_child(
Flex::column()
.with_flex_child(Label::new("top left"), 1.0)
.with_flex_child(Align::centered(Label::new("bottom left")), 1.0),
1.0)
.with_flex_child(
Flex::column()
.with_flex_child(Label::new("top right"), 1.0)
.with_flex_child(Align::centered(Label::new("bottom right")), 1.0),
1.0))
}
Do not forget to import the new widgets;
use druid::widget::{Label, Flex, Padding, Align};
Application state
We can display a window and draw and position widgets in it. Now it's time to find out how we can tie these widgets to the rest of our application. First lets see how we can display information from our application in the user interface. For this we need to define what our application's state looks like.
...
Handle user input
...
Putting it all together
...
Model data and the Data
trait
The heart of a Druid application is your application model. Your model drives your UI. When you mutate your model, Druid compares the old and new version, and propagates the change to the components ('widgets') of your application that are affected by the change.
For this to work, your model must implement the Clone
and Data
traits. It
is important that your model be cheap to clone; we encourage the use of
reference counted pointers to allow cheap cloning of more expensive types. Arc
and Rc
have blanket Data
impls, so if you have a type that does not
implement Data
, you can always just wrap it in one of those smart pointers.
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;
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, // `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, title: String, note: Option<String>, completed: bool, } #[derive(Clone, Data, PartialEq)] /// The three types of tasks in the world. enum Category { Work, Play, Revolution, } }
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
. This is not a huge issue, however; 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.
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
Let's say we're building a todo list application, and we are designing the widget that will represent a single todo item. Our data model looks like this:
/// A single todo item.
#[derive(Clone, Data)]
struct TodoItem {
title: String,
completed: bool,
urgent: bool,
}
We would like our widget to display the title of the item, and then below
that to display two checkmarks that toggle the 'completed' and 'urgent' bools.
Checkbox
(a widget included in Druid) implements Widget<bool>
.
How do we use it with TodoItem
? By using a Lens
.
Conceptual
You can think of a lens as a way of "focusing in" on one part of the data. You
have a TodoItem
, but you want a bool
.
Lens
is a trait for types that perform this "focusing in" (aka lensing).
A simplified version of the Lens
trait might look like this:
trait SimpleLens<In, Out> {
fn focus(&self, data: &In) -> Out;
}
That is, this type takes an instance of In
, and returns an instance of Out
.
For instance, imagine we wanted a lens to focus onto the completed
state of
our TodoItem
. With our simple trait, we might do:
/// This is the type of the lens itself; in this case it has no state.
struct CompletedLens;
impl SimpleLens<TodoItem, bool> for CompletedLens {
fn focus(&self, data: &TodoItem) -> bool {
data.completed
}
}
Note:
Lens
isn't that helpful on its own; in Druid it is generally used alongsideLensWrap
, which is a special widget that uses aLens
to change theData
type of its child. Lets say we have aCheckbox
, but our data is aTodoItem
: we can do,LensWrap::new(my_checkbox, CompletedLens)
in order to bridge the gap.
Our example is missing out on an important feature of lenses, though, which is that
they allow mutations that occur on the lensed data to propagate back to the
source. For this to work, lenses actually work with closures. The real signature
of Lens
looks more like this (names changed for clarity):
pub trait Lens<In, Out> {
/// Get non-mut access to the field.
fn with<R, F: FnOnce(&Out) -> R>(&self, data: &In, f: F) -> R;
/// Get mut access to the field.
fn with_mut<R, F: FnOnce(&mut Out) -> R>(&self, data: &mut In, f: F) -> R;
}
Here In
refers to the input to the Lens
and Out
is the output. F
is a
closure that can return a result, R
.
Now, instead of just being passed Out
directly from the function, we pass the
function a closure that will itself be passed an Out
; if our closure returns
a result, that will be given back to us.
This is unnecessary in the case of non-mutable access, but it is important for
mutable access, because in many circumstances (such as when using an Rc
or
Arc
) accessing a field mutably is expensive even if you don't do any mutation.
In any case, the real implementation of our lens would look like,
struct CompletedLens;
impl Lens<TodoItem, bool> for CompletedLens {
fn with<R, F: FnOnce(&bool) -> R>(&self, data: &TodoItem, f: F) -> R {
f(&data.completed)
}
fn with_mut<R, F: FnOnce(&mut bool) -> R>(&self, data: &mut TodoItem, f: F) -> R {
f(&mut data.completed)
}
}
That seems pretty simple and fairly annoying to write, which is why you generally don't have to.
Deriving lenses
For simple field access, you can derive
the Lens
trait.
/// A single todo item.
#[derive(Clone, Data, Lens)]
struct TodoItem {
title: String,
completed: bool,
urgent: bool,
}
This handles the boilerplate of writing a lens for each field. It also does
something slightly sneaky: it exposes the generated lenses through the type
itself, as associated constants. What this means is that if you want to use the
lens that gives you the completed
field, you can access it via
TodoItem::completed
. The generated code basically looks something like:
struct GeneratedLens_AppData_title;
struct GeneratedLens_AppData_completed;
struct GeneratedLens_AppData_urgent;
impl TodoItem {
const title = GeneratedLens_AppData_title;
const completed = GeneratedLens_AppData_completed;
const urgent = GeneratedLens_AppData_urgent;
}
One consequence of this is that if your type has a method with the same name as
one of its fields, derive
will fail. To get around this, you can specify a
custom name for a field's lens:
#[derive(Lens)]
struct Item {
#[lens(name = "count_lens")]
count: usize,
}
// This works now:
impl Item {
fn count(&self) -> usize {
self.count
}
}
Using lenses
The easiest way to use a lens is with the lens
method that is provided through
the WigetExt
trait; this is a convenient way to wrap a widget in a LensWrap
with a given lens.
Let's build the UI for our todo list item:
use druid::widget::{Checkbox, Flex, Label, Widget, WidgetExt};
fn make_todo_item() -> impl Widget<TodoItem> {
// A label that generates its text based on the data:
let title = Label::dynamic(|text: &String, _| text.to_string()).lens(TodoItem::title);
let completed = Checkbox::new("Completed:").lens(TodoItem::completed);
let urgent = Checkbox::new("Urgent:").lens(TodoItem::urgent);
Flex::column()
// label on top
.with_child(title)
// two checkboxes below
.with_child(Flex::row().with_child(completed).with_child(urgent))
}
Advanced lenses
Field access is a very simple (and common, and useful) case, but lenses can do much more than that.
LensExt
and combinators
Similar to the WidgetExt
trait, we offer a LensExt
trait that provides
various functions for composing lenses. These are similar to the various methods
on iterator; you can map
from one lens to another, you can index into a
collection, or you can efficiently access data in an Arc
without unnecessary
mutation; see the main crate documentation for more.
As your application gets more complicated, it will become likely that you want
to use fancier sorts of lensing, and map
and company can start to get out
of hand; when that happens, you can always implement a lens by hand.
Getting something from a collection
Your application is a contact book, and you would like a lens that focuses on a specific contact. You might write something like this:
#[derive(Clone, Data)]
struct Contact {
// fields
}
type ContactId = u64;
#[derive(Clone, Data)]
struct Contacts {
inner: Arc<HashMap<ContactId, Contact>>,
}
// Lets write a lens that returns a specific contact based on its id, if it exists.
struct ContactIdLens(ContactId);
impl Lens<Contacts, Option<Contact>> for ContactIdLens {
fn with<R, F: FnOnce(&Option<Contact>) -> R>(&self, data: &Contacts, f: F) -> R {
let contact = data.inner.get(&self.0).cloned();
f(&contact)
}
fn with_mut<R, F: FnOnce(&mut Option<Contact>) -> R>(&self, data: &mut Contacts, f: F) -> R {
// get an immutable copy
let mut contact = data.inner.get(&self.0).cloned();
let result = f(&mut contact);
// only actually mutate the collection if our result is mutated;
let changed = match (contact.as_ref(), data.inner.get(&self.0)) {
(Some(one), Some(two)) => !one.same(two),
(None, None) => false,
_ => true,
};
if changed {
// if !data.inner.get(&self.0).same(&contact.as_ref()) {
let contacts = Arc::make_mut(&mut data.inner);
// if we're none, we were deleted, and remove from the map; else replace
match contact {
Some(contact) => contacts.insert(self.0, contact),
None => contacts.remove(&self.0),
};
}
result
}
}
Doing a conversion
What if you have a distance in miles that you would like to display in kilometres?
struct MilesToKm;
const KM_PER_MILE: f64 = 1.609_344;
impl Lens<f64, f64> for MilesToKm {
fn with<R, F: FnOnce(&f64) -> R>(&self, data: &f64, f: F) -> R {
let kms = *data * KM_PER_MILE;
f(&kms)
}
fn with_mut<R, F: FnOnce(&mut f64) -> R>(&self, data: &mut f64, f: F) -> R {
let mut kms = *data * KM_PER_MILE;
let kms_2 = kms;
let result = f(&mut kms);
// avoid doing the conversion if unchanged, it might be lossy?
if !kms.same(&kms_2) {
let miles = kms * KM_PER_MILE.recip();
*data = miles;
}
result
}
}
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),
}
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. This is less scary than it sounds, assuming the user follows a few simple guidelines. That said, It is the programmer's responsibility to ensure that the environment is used correctly. The API is aggressive about checking for misuse, and many methods will panic if anything is amiss. In practice this should be easy to avoid, by following a few simple 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::LABEL_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.
In general, you should not need to worry about localization directly. See the localization chapter for an overview of localization in Druid.
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.
High pixel density images with druid
TODO: Write this section after it's more clear how this works and if its even solved.
Localization (TODO)
Command (TODO)
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 contraints, 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!("#{:X}{:X}{:X}", r, g, b)
})
.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),
}
}
}
todo
v controller, painter
- how to do layout
- how constraints work
- child widget, set_layout_rect
- paint bounds
- container widgets
- widgetpod & architecture
- commands and widgetid
- focus / active / hot
- request paint & request layout
- changing widgets at runtime
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.
- pull requests welcome
Projects that work with Druid (widgets etc)
- pull requests welcome
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