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