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

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, 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

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 alongside LensWrap, which is a special widget that uses a Lens to change the Data type of its child. Lets say we have a Checkbox, but our data is a TodoItem: 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.

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),
}

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.

  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::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 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 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

Blog posts

People have been blogging about Druid