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

Hello

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

Counter

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 import kas::column.
  • If you replace kas::row! with row! 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,

  1. We define a message type, Increment
  2. The button pushes a message to the message stack
  3. Our Adapt widget uses try_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

Counter 1 Counter 2

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

Calculator

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 a String will do.
  • A handler for button presses. We'll just pass a Key to fn 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:

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

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

Counter

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 impls on that type. (Unfortunately it also inhibits rustfmt from working, for now.) Here, it serves two purposes:

  1. impl Self syntax (not important here, but much more useful on structs with generics)
  2. To support the #[widget] attribute-macro. This attribute-macro is a Kas extension to impl_scope!, and can act on anything within that scope (namely, it will check existing impls of Layout, Events and Widget, reading definitions of associated type 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 within layout instead of as a field. Alas, this doesn't work when passing a field as input data (as used by display), or when code must refer to the child by name.

  • We could box the widget with Box<dyn Widget<Data = ()>>. (This is what the layout syntax does for embedded widgets.)

  • The impl_anon! macro does support impl 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 use kas::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 simple Label widget and re-write it every time count 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:

For now, see the examples: