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.