Counter: an interactive widget
Topics: layout, input data, messages
The last example was a bit boring. Lets get interactive!
extern crate kas; use kas::prelude::*; use kas::widgets::{format_value, Adapt, Button}; #[derive(Clone, Debug)] struct Increment(i32); fn counter() -> impl Widget<Data = ()> { let tree = kas::column![ align!(center, format_value!("{}")), kas::row![ Button::label_msg("−", Increment(-1)), Button::label_msg("+", Increment(1)), ] .map_any(), ]; Adapt::new(tree, 0).on_message(|_, count, Increment(add)| *count += add) } fn main() -> kas::app::Result<()> { env_logger::init(); let theme = kas::theme::SimpleTheme::new().with_font_size(24.0); kas::app::Default::with_theme(theme) .build(())? .with(Window::new(counter(), "Counter")) .run() }
Prelude
The kas::prelude
includes a bunch of commonly-used, faily unambiguous stuff:
#![allow(unused)] fn main() { extern crate kas; use kas::prelude::*; }
Impl trait
If you're new to Rust, you might find the following confusing:
#![allow(unused)] fn main() { extern crate kas; use kas::prelude::*; fn counter() -> impl Widget<Data = ()> { // ... kas::widgets::Label::new("") } }
This is (return position) impl trait, specifying that the [Widget
] trait's associated type Data
is ()
.
(We'll get back to this type Data
in a bit.)
Layout
Our user interface should be a widget tree: lets use a column layout over the count and [a row layout over the buttons].
#![allow(unused)] fn main() { extern crate kas; use kas::prelude::*; use kas::widgets::{format_value, Adapt, Button}; #[derive(Clone, Debug)] struct Increment(i32); fn counter() -> impl Widget<Data = ()> { let tree = kas::column![ align!(center, format_value!("{}")), kas::row![ Button::label_msg("−", Increment(-1)), Button::label_msg("+", Increment(1)), ] .map_any(), ]; Adapt::new(tree, 0) } }
Layout macros
kas::column!
and kas::row!
are layout macros which, as the name suggests, construct a column/row over other widgets.
kas::align!
is another layout macro. Above, the kas::
prefix is skipped, not because kas::align
was imported, but because layout macros (in this case kas::column!
) have direct support for parsing and evaluating other layout macros. (If you wrote kas::align!
instead the result would function identically but with slightly different code generation.)
Now, you could, if you prefer, import the layout macros: use kas::{align, column, row};
. However,
- (
std
)column!
is a very different macro. This can result in surprising error messages if you forget to importkas::column
. - If you replace
kas::row!
withrow!
you will get a compile error: the layout macro parser cannot handle.map_any()
.kas::row![..]
evaluates to a complete widget;row![..]
as an embedded layout does not.
Input data
So, you may have wondered what the Widget::Data
type encountered above is about. All widgets in Kas are provided input data (via Events::update
) when the UI is initialised and whenever that data changes (not strictly true as you'll see when we get to custom widgets).
The point is, a widget like Text
is essentially a function Fn(&A) -> String
where &A
is your input data. format_value!
is just a convenient macro to construct a Text
widget.
Thus, format_value!("{}")
is a Text
widget which formats some input data to a String
. But what input data?
Providing input data: Adapt
There are three methods of providing input data to a UI:
- Custom widgets (advanced topic)
- Top-level app data (the
()
of.build(())
; we'll be using this in the next chapter) Adapt
nodes
All widgets in Kas may store state (though some are not persistent, namely view widgets (another advanced topic)). Adapt
is a widget which stores user-defined data and message handlers.
Thus,
#![allow(unused)] fn main() { extern crate kas; use kas::prelude::*; use kas::widgets::{format_value, Adapt}; #[derive(Clone, Debug)] struct Increment(i32); fn counter() -> impl Widget<Data = ()> { let tree = format_value!("{}"); Adapt::new(tree, 0) } }
is a widget which wraps tree
, providing it with input data of 0.
But to make this do something we need one more concept: messages.
Mapping data
We should briefly justify .map_any()
in our example: our Text
widget expects input data (of type i32
), while Button::label_msg
constructs a Button<AccessLabel>
expecting data of type ()
.
The method .map_any()
maps the row of buttons to a new widget supporting (and ignoring) any input data.
We could instead use Button::new(label_any("+"))
which serves the same purpose, but ignoring that input data much further down the tree.
Messages
Kas has a fairly simple event-handling model: events (like mouse clicks) and input data go down the tree, messages come back up. You can read more about this in kas::event
docs.
When widgets receive an event, often this must be handled by some widget higher up the tree (an ancestor). For example, our "+" button must cause our Adapt
widget to increment its state. To do that,
- We define a message type,
Increment
- The button
push
es a message to the message stack - Our
Adapt
widget usestry_pop
to retrieve that message
Aside: widgets have an associated type Data
. So why don't they also have an associated type Message
(or Msg
for short)? Early versions of Kas (up to v0.10) did in fact have an Msg
type, but this had some issues: translating message types between child and parent widgets was a pain, and supporting multiple message types was even more of a pain (mapping to a custom enum), and the Msg
type must be specified when using dyn Widget
. Using a variadic (type-erased) message stack completely avoids these issues, and at worst you'll see an unhandled
warning in the log. In contrast, compile-time typing of input data is considerably more useful and probably a little easier to deal with (the main nuisance being mapping input data to ()
for widgets like labels which don't use it).
Message types
What is a message? Nearly anything: the type must support [Debug
] and should have a unique name. Our example defines:
#![allow(unused)] fn main() { #[derive(Clone, Debug)] struct Increment(i32); }
Note that if your UI pushes a message to the stack but fails to handle it, you will get a warning message like this:
[WARN kas_core::erased] unhandled: counter::Increment::Increment(1)
Use of built-in types like ()
or i32
is possible but considered bad practice (imagine if the above warning was just unhandled: 1
).
Buttons
This should be obvious: Button::label_msg("+", Increment(1))
constructs a Button
which pushes the message Increment(1)
when pressed.
Handling messages
Finally, we can handle our button click:
#![allow(unused)] fn main() { extern crate kas; use kas::prelude::*; use kas::widgets::{format_value, Adapt}; #[derive(Clone, Debug)] struct Increment(i32); fn counter() -> impl Widget<Data = ()> { let tree = format_value!("{}"); Adapt::new(tree, 0) .on_message(|_, count, Increment(add)| *count += add) } }
Adapt::on_message
calls our closure whenever an Increment
message is pushed with a mutable reference to its state, count
. After handling our message, Adapt
will update its descendants with the new value of count
, thus refreshing the label: format_value!("{}"))
.