Kas Tutorials
These tutorials concern the Kas GUI system. See also the Kas examples and 7GUIs examples.
Further reading can be found on the Kas blog.
Please ask questions on the discussion boards or on the tutorials issue tracker.
Requirements
It is assumed that you are already familiar with the Rust language. If not, then Learn Rust! You are not expected to master Rust before learning Kas, but this tutorial series assumes a moderate understanding of the language.
Kas supports both nightly and stable Rust. Due to the nature of procedural macros, better diagnostics are available when using nightly.
Tutorials use the latest stable release of Kas, currently v0.14.
Examples
All significant examples can be found as working apps in the example directory.
To run the examples locally, check out the tutorials
repository, then run e.g.:
git clone https://github.com/kas-gui/tutorials.git
cd tutorials
cargo run --example counter
Kas Dependencies
What is kas
? Here is a heavily-reduced dependency tree:
kas — Wrapper crate to expose all components under a single API
├── kas-core — Core types, traits and event handling
│ ├── arboard — Clipboard support (optional)
│ ├── async-global-executor — Executor supporting EventState::push_spawn (optional)
│ ├── easy-cast — Numeric type-casting, re-exposed as kas::cast
│ ├── kas-macros (proc-macro) — Macros
│ │ └── impl-tools-lib — Backend used to implement macros
│ ├── kas-text — Font handling, type setting
│ │ ├── ab_glyph — Glyph rastering
│ │ ├── harfbuzz_rs — Shaping (optional)
│ │ ├── pulldown-cmark — Markdown parsing (optional)
│ │ └── rustybuzz — Shaping (optional, default)
│ ├── log — Logging facade
│ ├── serde — Serialization support for persistent configuration (optional)
│ ├── serde_json, serde_yaml, ron — Output formats for configuration (optional)
│ ├── smithay-clipboard — Wayland clipboard support (optional)
│ └── winit — Cross-platform window creation
│ │ └── raw-window-handle — Interoperability for Rust Windowing applications
├── kas-widgets — Standard widget collection
├── kas-resvg — Canvas and Svg widgets
│ ├── resvg — An SVG rendering library
│ └── tiny-skia — Tiny CPU-only Skia subset
├── kas-view — "View widgets" over data models (optional)
└── kas-wgpu — Kas graphics backend over WGPU
└── wgpu — Rusty WebGPU API wrapper
Licence
This tutorial, including text but excluding code samples, is licensed under CC BY-SA 4.0
Code samples found within this tutorial are marked with CC0 1.0
Hello World!
Topics: logging, app, run
Lets get started with a simple message box. Source.
extern crate kas; use kas::widgets::dialog::MessageBox; fn main() -> kas::app::Result<()> { env_logger::init(); let window = MessageBox::new("Message").into_window("Hello world"); kas::app::Default::new(())?.with(window).run() }
cargo run --example hello
Logging
Enabling a logger is optional, but can be very useful for debugging:
#![allow(unused)] fn main() { env_logger::init(); }
Kas uses the log
facade internally. To see the
output, we need an implementation, such as
env_logger
.
Trace level can be a bit chatty; to get a reasonable level of output you might try this:
export RUST_LOG=warn,naga=error,kas=debug
cargo run --example hello
A window, a shell
Next, we construct a MessageBox
widget, then wrap with a Window
:
#![allow(unused)] fn main() { extern crate kas; use kas::widgets::dialog::MessageBox; let window = MessageBox::new("Message") .into_window("Hello world"); let _: kas::Window<()> = window; }
Finally, we construct a default app, add this window, and run:
extern crate kas; use kas::widgets::dialog::MessageBox; fn main() -> kas::app::Result<()> { let window = MessageBox::new("Message").into_window("Hello world"); kas::app::Default::new(())? .with(window) .run() }
kas::app::Default
is just a parameterisation of kas::app::Application
which selects a sensible graphics backend and theme.
If you wanted to select your own theme instead, you could do so as follows:
extern crate kas; use kas::widgets::dialog::MessageBox; fn main() -> kas::app::Result<()> { let window = MessageBox::new("Message").into_window("Hello world"); let theme = kas::theme::SimpleTheme::new(); kas::app::Default::with_theme(theme) .build(())? .with(window) .run() }
Or, if you wanted to specify the graphics backend and theme:
extern crate kas; use kas::widgets::dialog::MessageBox; fn main() -> kas::app::Result<()> { let window = MessageBox::new("Message").into_window("Hello world"); kas_wgpu::WgpuBuilder::new(()) .with_theme(kas_wgpu::ShadedTheme::new()) .build(())? .with(window) .run() }
Finally, Application::run
starts our UI. This method runs the event-loop internally, returning Ok(())
once all windows have closed successfully.
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!("{}"))
.
Sync-counter: data models
Topics: top-level AppData
, multiple windows
We complicate the previous example just a little bit!
extern crate kas; use kas::widgets::{format_data, label_any, Adapt, Button, Slider}; use kas::{messages::MessageStack, Action, Window}; #[derive(Clone, Debug)] struct Increment(i32); #[derive(Clone, Copy, Debug)] struct Count(i32); impl kas::app::AppData for Count { fn handle_messages(&mut self, messages: &mut MessageStack) -> Action { if let Some(Increment(add)) = messages.try_pop() { self.0 += add; Action::UPDATE } else { Action::empty() } } } fn counter() -> impl kas::Widget<Data = Count> { // Per window state: (count, increment). type Data = (Count, i32); let initial: Data = (Count(0), 1); #[derive(Clone, Debug)] struct SetValue(i32); let slider = Slider::right(1..=10, |_, data: &Data| data.1).with_msg(SetValue); let ui = kas::column![ format_data!(data: &Data, "Count: {}", data.0.0), row![slider, format_data!(data: &Data, "{}", data.1)], row![ Button::new(label_any("Sub")).with(|cx, data: &Data| cx.push(Increment(-data.1))), Button::new(label_any("Add")).with(|cx, data: &Data| cx.push(Increment(data.1))), ], ]; Adapt::new(ui, initial) .on_update(|_, state, count| state.0 = *count) .on_message(|_, state, SetValue(v)| state.1 = v) } fn main() -> kas::app::Result<()> { env_logger::init(); let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0); kas::app::Default::with_theme(theme) .build(Count(0))? .with(Window::new(counter(), "Counter 1")) .with(Window::new(counter(), "Counter 2")) .run() }
AppData
In the previous example, our top-level AppData
was ()
: .build(())
.
This time, we want to store our counter in top-level AppData
. But, as we saw with Adapt
, state which doesn't react to messages is useless; hence we use a custom type and implement a message handler:
#![allow(unused)] fn main() { extern crate kas; use kas::{messages::MessageStack, Action}; #[derive(Clone, Debug)] struct Increment(i32); #[derive(Clone, Copy, Debug)] struct Count(i32); impl kas::app::AppData for Count { fn handle_messages(&mut self, messages: &mut MessageStack) -> Action { if let Some(Increment(add)) = messages.try_pop() { self.0 += add; Action::UPDATE } else { Action::empty() } } } }
AppData::handle_messages
is less succinct than Adapt::on_message
, but dones the same job. The method notifies when widgets must be updated by returning Action::UPDATE
.
As an input
We initialise our app with an instance of Count
: .build(Count(0))
.
Note that Count
is now an input to the widgets we construct:
#![allow(unused)] fn main() { extern crate kas; use kas::{messages::MessageStack, Action}; #[derive(Clone, Copy, Debug)] struct Count(i32); impl kas::app::AppData for Count { fn handle_messages(&mut self, messages: &mut MessageStack) -> Action { Action::empty() } } fn counter() -> impl kas::Widget<Data = Count> { // ... kas::widgets::label_any("") } }
Adapting app data
We could at this point simply repeat the previous example, skipping the Adapt
node since our AppData
implementation already does the work. But lets make things more interesting by combining top-level state with local state.
We define a new data type for local state and construct an initial instance:
#![allow(unused)] fn main() { #[derive(Clone, Copy, Debug)] struct Count(i32); // Per window state: (count, increment). type Data = (Count, i32); let initial: Data = (Count(0), 1); }
Note that our local data includes a copy of the top-level data Count
(along with an initial value, Count(0)
, which will be replaced before it is ever used).
We'll skip right over the widget declarations to the new Adapt
node:
#![allow(unused)] fn main() { extern crate kas; use kas::widgets::{label_any, Adapt}; #[derive(Clone, Copy, Debug)] struct Count(i32); fn counter() -> impl kas::Widget<Data = Count> { #[derive(Clone, Debug)] struct SetValue(i32); let ui = label_any(""); let initial = (Count(0), 1); Adapt::new(ui, initial) .on_update(|_, state, count| state.0 = *count) .on_message(|_, state, SetValue(v)| state.1 = v) } }
The notable addition here is Adapt::on_update
, which takes a closure over the expected mutable reference to local state
as well as input data count
(i.e. the top-level data), allowing us to update local state with the latest top-level count
.
Aside: this is really not how adapting top-level data with local state is supposed to work. Ideally, we'd omit the local copy of Count
entirely and pass something like (&Count, i32)
to local widgets. But, as any Rustacean knows, a reference requires a lifetime, and dealing with lifetimes can get complicated. The plan is to update our approach once Rust supports object-safe GATs (also known as Extended Generic Associated Types).
Multiple windows
It barely seems worth mentioning, but there's nothing stopping us from calling fn counter
multiple times and constructing a new window around each:
extern crate kas; use kas::{messages::MessageStack, Action, Window}; #[derive(Clone, Copy, Debug)] struct Count(i32); impl kas::app::AppData for Count { fn handle_messages(&mut self, messages: &mut MessageStack) -> Action { Action::empty() } } fn counter() -> impl kas::Widget<Data = Count> { kas::widgets::label_any("") } fn main() -> kas::app::Result<()> { let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0); kas::app::Default::with_theme(theme) .build(Count(0))? .with(Window::new(counter(), "Counter 1")) .with(Window::new(counter(), "Counter 2")) .run() }
Of course, each window has its own local state stored in its Adapt
node (the increment
) while sharing the top-level Count
.
Calculator: make_widget and grid layout
Topics: grid layout, access keys
extern crate kas; use kas::event::NamedKey; use kas::prelude::*; use kas::widgets::{AccessLabel, Adapt, Button, EditBox}; use std::num::ParseFloatError; use std::str::FromStr; type Key = kas::event::Key<kas::event::SmolStr>; fn key_button(label: &str) -> Button<AccessLabel> { let string = AccessString::from(label); let key = string.key().unwrap().clone(); Button::label_msg(string, key) } fn key_button_with(label: &str, key: Key) -> Button<AccessLabel> { Button::label_msg(label, key.clone()).with_access_key(key) } fn calc_ui() -> impl Widget<Data = ()> { // We could use kas::widget::Text, but EditBox looks better. let display = EditBox::string(|calc: &Calculator| calc.display()) .with_multi_line(true) .with_lines(3, 3) .with_width_em(5.0, 10.0); // We use map_any to avoid passing input data (not wanted by buttons): let buttons = kas::grid! { // Key bindings: C, Del (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear)) .with_access_key(NamedKey::Delete.into()), // Widget is hidden but has key binding. // TODO(opt): exclude from layout & drawing. (0, 0) => key_button_with("", NamedKey::Backspace.into()), (1, 0) => key_button_with("&÷", Key::Character("/".into())), (2, 0) => key_button_with("&×", Key::Character("*".into())), (3, 0) => key_button_with("&−", Key::Character("-".into())), (0, 1) => key_button("&7"), (1, 1) => key_button("&8"), (2, 1) => key_button("&9"), (3, 1..3) => key_button("&+"), (0, 2) => key_button("&4"), (1, 2) => key_button("&5"), (2, 2) => key_button("&6"), (0, 3) => key_button("&1"), (1, 3) => key_button("&2"), (2, 3) => key_button("&3"), (3, 3..5) => key_button_with("&=", NamedKey::Enter.into()), (0..2, 4) => key_button("&0"), (2, 4) => key_button("&."), } .map_any(); Adapt::new(kas::column![display, buttons], Calculator::new()) .on_message(|_, calc, key| calc.handle(key)) .on_configure(|cx, _| { cx.disable_nav_focus(true); cx.enable_alt_bypass(true); }) } fn main() -> kas::app::Result<()> { env_logger::init(); let theme = kas_wgpu::ShadedTheme::new().with_font_size(16.0); kas::app::Default::with_theme(theme) .build(())? .with(Window::new(calc_ui(), "Calculator")) .run() } #[derive(Clone, Debug)] struct Calculator { // ... } impl Calculator { fn new() -> Calculator { Calculator { // ... } } fn display(&self) -> String { // ... String::new() } fn handle(&mut self, key: Key) { // ... let _ = key; } }
The back-end: Calculator
First things first, lets define our backend, Calculator
. It must have:
- Internal state (fields)
- A constructor (
new
) - Some type of output. A
display
function returning aString
will do. - A handler for button presses. We'll just pass a
Key
tofn handle
.
Fill out the implementation yourself or copy from the full source.
Access keys
To make the calculator keyboard-accessible, we'll use access keys (see more on Wikipedia or Windows app docs). Kas supports these via:
AccessString
supports parsing things like&File
as the labelFile
with access key FAccessLabel
is a label widget over anAccessString
which sends the messageActivate
when its access key is pressed. AButton
widget will react to this message.Button::with_access_key
may be used to specify arbitrary access keys.
Button helper fns
To make constructing buttons easier, we define some helper functions. (These facilitate defining the button message more than they do the access keys.)
#![allow(unused)] fn main() { extern crate kas; use kas::text::AccessString; use kas::widgets::{AccessLabel, Button}; type Key = kas::event::Key<kas::event::SmolStr>; fn key_button(label: &str) -> Button<AccessLabel> { let string = AccessString::from(label); let key = string.key().unwrap().clone(); Button::label_msg(string, key) } fn key_button_with(label: &str, key: Key) -> Button<AccessLabel> { Button::label_msg(label, key.clone()).with_access_key(key) } }
Navigation focus and Alt-bypass
Normally, access keys are only active while holding Alt. To avoid this requirement we call enable_alt_bypass
. Further, we disable Tab key navigation with disable_nav_focus
.
#![allow(unused)] fn main() { extern crate kas; use kas::{Widget, widgets::{label_any, Adapt}}; #[derive(Debug)] struct Calculator; impl Calculator { fn new() -> Self { Calculator } fn handle(&mut self, _key: ()) {} } fn ui() -> impl Widget<Data = ()> { let display = label_any(""); let buttons = label_any(""); Adapt::new(kas::column![display, buttons], Calculator::new()) .on_message(|_, calc, key| calc.handle(key)) .on_configure(|cx, _| { cx.disable_nav_focus(true); cx.enable_alt_bypass(true); }) } }
Grid layout
We already saw column and row layouts. This time, we'll use kas::grid!
for layout.
#![allow(unused)] fn main() { extern crate kas; use kas::event::NamedKey; use kas::prelude::*; use kas::widgets::{AccessLabel, Button}; type Key = kas::event::Key<kas::event::SmolStr>; fn key_button(label: &str) -> Button<AccessLabel> { let string = AccessString::from(label); let key = string.key().unwrap().clone(); Button::label_msg(string, key) } fn key_button_with(label: &str, key: Key) -> Button<AccessLabel> { Button::label_msg(label, key.clone()).with_access_key(key) } fn ui() -> impl Widget<Data = i32> { let buttons = kas::grid! { // Key bindings: C, Del (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear)) .with_access_key(NamedKey::Delete.into()), // Widget is hidden but has key binding. // TODO(opt): exclude from layout & drawing. (0, 0) => key_button_with("", NamedKey::Backspace.into()), (1, 0) => key_button_with("&÷", Key::Character("/".into())), (2, 0) => key_button_with("&×", Key::Character("*".into())), (3, 0) => key_button_with("&−", Key::Character("-".into())), (0, 1) => key_button("&7"), (1, 1) => key_button("&8"), (2, 1) => key_button("&9"), (3, 1..3) => key_button("&+"), (0, 2) => key_button("&4"), (1, 2) => key_button("&5"), (2, 2) => key_button("&6"), (0, 3) => key_button("&1"), (1, 3) => key_button("&2"), (2, 3) => key_button("&3"), (3, 3..5) => key_button_with("&=", NamedKey::Enter.into()), (0..2, 4) => key_button("&0"), (2, 4) => key_button("&."), } .map_any(); buttons } }
Worth noting is our hidden Backspace
button. This is just another cell, but hidden under the clear
button. Yes, this is a sub-optimal hack (the widget is still sized and drawn); it works but might see a less hacky solution in the future.
Again, we use .map_any()
to make our buttons (input Data = ()
) compatible with the parent UI element (input Data = Calculator
).
Counter: a simple widget
Topics: custom widgets
Here we rewrite the counter as a custom widget. There's no reason to do so for this particular case, but it serves as a simple example to the topic.
extern crate kas; use kas::prelude::*; use kas::widgets::{format_value, AccessLabel, Button, Row, Text}; #[derive(Clone, Debug)] struct Increment(i32); impl_scope! { #[widget{ layout = column![ align!(center, self.display), self.buttons, ]; }] struct Counter { core: widget_core!(), #[widget(&self.count)] display: Text<i32, String>, #[widget] buttons: Row<Button<AccessLabel>>, count: i32, } impl Self { fn new(count: i32) -> Self { Counter { core: Default::default(), display: format_value!("{}"), buttons: Row::new([ Button::label_msg("-", Increment(-1)), Button::label_msg("+", Increment(1)), ]), count, } } } impl Events for Self { type Data = (); fn handle_messages(&mut self, cx: &mut EventCx, data: &()) { if let Some(Increment(incr)) = cx.try_pop() { self.count += incr; cx.update(self.as_node(data)); } } } } 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::new(0), "Counter")) .run() }
Macros
impl_scope
impl_scope!
is a macro from impl-tools. This macro wraps a type definition and impl
s on that type. (Unfortunately it also inhibits rustfmt
from working, for now.) Here, it serves two purposes:
impl Self
syntax (not important here, but much more useful on structs with generics)- To support the
#[widget]
attribute-macro. This attribute-macro is a Kas extension toimpl_scope!
, and can act on anything within that scope (namely, it will check existing impls ofLayout
,Events
andWidget
, reading definitions of associatedtype Data
, injecting certain missing methods into these impls, and write new impls).
#[widget]
The #[widget]
attribute-macro is used to implement the Widget
trait. This is the only supported way to implement Widget
. There are a few parts to this.
First, we must apply #[widget]
to the struct. (The layout = ...;
argument (and { ... }
braces) are optional; some other arguments might also occur here.)
#[widget{
layout = column![
align!(center, self.display),
self.buttons,
];
}]
Second, all widgets must have "core data". This might be an instance of CoreData
or might be some custom generated struct (but with the same public rect
and id
fields and constructible via Default
). We must provide a field of type widget_core!()
.
core: widget_core!(),
Third, any fields which are child widgets must be annotated with #[widget]
. (This enables them to be configured and updated.)
We can use this attribute to configure the child widget's input data too: in this case, display
is passed &self.count
. Beware only that there is no automatic update mechanism: when mutating a field used as input data it may be necessary to explicitly update the affected widget(s) (see the note after the fourth step below).
#![allow(unused)] fn main() { extern crate kas; use kas::impl_scope; use kas::widgets::{AccessLabel, Button, Row, Text}; impl_scope! { #[widget{ Data = (); layout = ""; }] struct Counter { core: widget_core!(), #[widget(&self.count)] display: Text<i32, String>, #[widget] buttons: Row<Button<AccessLabel>>, count: i32, } } }
Fourth, the input Data
type to our Counter
widget must be specified somewhere. In our case, we specify this by implementing Events
. (If this trait impl was omitted, you could write Data = ();
as an argument to #[widget]
.)
#![allow(unused)] fn main() { extern crate kas; use kas::prelude::*; #[derive(Clone, Debug)] struct Increment(i32); impl_scope! { #[widget{ layout = ""; }] struct Counter { core: widget_core!(), count: i32, } impl Events for Self { type Data = (); fn handle_messages(&mut self, cx: &mut EventCx, data: &()) { if let Some(Increment(incr)) = cx.try_pop() { self.count += incr; cx.update(self.as_node(data)); } } } } }
Notice here that after mutating self.count
we call cx.update(self.as_node(data))
in order to update self
(and all children recursively). (In this case it would suffice to update only display
, e.g. via cx.update(self.display.as_node(&self.count))
, if you prefer to trade complexity for slightly more efficient code.)
Fifth, we must specify widget layout somehow. There are two main ways of doing this: implement Layout
or use the layout
argument of #[widget]
. To recap, we use:
#[widget{
layout = column![
align!(center, self.display),
self.buttons,
];
}]
This is macro-parsed layout syntax (not real macros). Don't use kas::column!
here; it won't know what self.display
is!
Don't worry about remembering each step; macro diagnostics should point you in the right direction. Detection of fields which are child widgets is however imperfect (nor can it be), so try to at least remember to apply #[widget]
attributes.
Aside: child widget type
Our Counter
has two (explicit) child widgets, and we must specify the type of each:
#![allow(unused)] fn main() { extern crate kas; use kas::impl_scope; use kas::widgets::{AccessLabel, Button, Row, Text}; impl_scope! { #[widget{ Data = (); layout = ""; }] struct Counter { core: widget_core!(), #[widget(&self.count)] display: Text<i32, String>, #[widget] buttons: Row<Button<AccessLabel>>, count: i32, } } }
Here, this is no problem (though note that we used Row::new([..])
not kas::row![..]
specifically to have a known widget type). In other cases, widget types can get hard (or even impossible) to write.
It would therefore be nice if we could just write impl Widget<Data = ()>
in these cases and be done. Alas, Rust does not support this. We are not completely without options however:
-
We could define our
buttons
directly withinlayout
instead of as a field. Alas, this doesn't work when passing a field as input data (as used bydisplay
), or when code must refer to the child by name. -
We could box the widget with
Box<dyn Widget<Data = ()>>
. (This is what thelayout
syntax does for embedded widgets.) -
The
impl_anon!
macro does supportimpl Trait
syntax. The required code is unfortunately a bit hacky (hidden type generics) and might sometimes cause issues. -
It looks likely that Rust will stabilise support for
impl Trait
in type aliases "soon". This requires writing a type-def outside of the widget definition but is supported in nightly:type MyButtons = impl Widget<Data = ()>;
Aside: uses
Before Kas 0.14, all widgets were custom widgets. (Yes, this made simple things hard.)
In the future, custom widgets might become obsolete, or might at least change significantly.
But for now, custom widgets still have their uses:
- Anything with a custom
Layout
implementation. E.g. if you want some custom graphics, you can either usekas::resvg::Canvas
or a custom widget. - Child widgets as named fields allows direct read/write access on these widgets. For example, instead of passing a
Text
widget the count to display via input data, we could use a simpleLabel
widget and re-write it every timecount
changes. Adapt
is the "standard" way of storing local state, but as seen here custom widgets may also do so, and you may have good reasons for this (e.g. to provide different data to different children without lots of mapping).- Since input data is a new feature, there are probably some cases it doesn't support yet. One notable example is anything requring a lifetime.
Sync-counter: data models
Topics: data models and view widgets
TODO:
ListView
andListData
Driver
, including predefined implsFilter
andUnsafeFilteredList
. This rather messy to use (improvable?). The latter should eventually be replaced with a safe variant.MatrixView
andMatrixData
. (Will possibly gain support for row/column labels and be renamedTableView
.)
For now, see the examples:
examples/ldata-list-view.rs
usesListView
with customListData
andDriver
examples/gallery.rs
'sfilter_list
usesUnsafeFilteredList
with a customDriver
. Less code but possibly more complex.examples/times-tables.rs
usesMatrixView
with customMatrixData
anddriver::NavView
. Probably the easiest example.