From 26f65d3a7bc17d72e6179f1e88780f2ea980227b Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:27:10 +0100 Subject: [PATCH 01/18] Use kas 0.16.0 release --- Cargo.toml | 4 ++-- tests/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d51f55d..63a8a0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,5 +8,5 @@ publish = false [dependencies] env_logger = "0.10" -kas = "0.14.2" -kas-wgpu = "0.14.1" +kas = "0.16" +kas-wgpu = "0.16" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index e7b8a03..aafeb1b 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -7,5 +7,5 @@ publish = false [dependencies] doc-comment = "0.3.3" env_logger = "0.10" -kas = "0.14.2" -kas-wgpu = "0.14.1" +kas = "0.16" +kas-wgpu = "0.16" From 3a506c88f043473f602ab907dd46dcf4c18f891c Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:34:12 +0100 Subject: [PATCH 02/18] Update examples: copy from Kas --- examples/calculator.rs | 37 +++++++++++++++++++------------------ examples/counter.rs | 23 ++++++++++++----------- examples/hello.rs | 15 +++++++++------ examples/sync-counter.rs | 40 ++++++++++++++++++++-------------------- 4 files changed, 60 insertions(+), 55 deletions(-) diff --git a/examples/calculator.rs b/examples/calculator.rs index 97f77ab..2277e94 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -1,9 +1,10 @@ -use kas::event::NamedKey; -use kas::prelude::*; -use kas::widgets::{AccessLabel, Adapt, Button, EditBox}; use std::num::ParseFloatError; use std::str::FromStr; +use kas::event::NamedKey; +use kas::prelude::*; +use kas::widgets::{column, grid, AccessLabel, Adapt, Button, EditBox}; + type Key = kas::event::Key; fn key_button(label: &str) -> Button { @@ -15,15 +16,15 @@ fn key_button_with(label: &str, key: Key) -> Button { Button::label_msg(label, key.clone()).with_access_key(key) } -fn calc_ui() -> impl Widget { +fn calc_ui() -> Window<()> { // 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_lines(3.0, 3.0) .with_width_em(5.0, 10.0); // We use map_any to avoid passing input data (not wanted by buttons): - let buttons = kas::grid! { + let buttons = grid! { // Key bindings: C, Del (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear)) .with_access_key(NamedKey::Delete.into()), @@ -49,22 +50,22 @@ fn calc_ui() -> impl Widget { } .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); - }) + let ui = Adapt::new(column![display, buttons], Calculator::new()) + .on_message(|_, calc, key| calc.handle(key)); + + Window::new(ui, "Calculator") + .escapable() + .with_alt_bypass() + .without_nav_focus() } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::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() + let theme = kas_wgpu::ShadedTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); + app.with(calc_ui()).run() } #[derive(Clone, Copy, Debug, PartialEq)] diff --git a/examples/counter.rs b/examples/counter.rs index dc35609..9f9c0f8 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,28 +1,29 @@ use kas::prelude::*; -use kas::widgets::{format_value, Adapt, Button}; +use kas::widgets::{column, format_value, row, Button}; #[derive(Clone, Debug)] struct Increment(i32); fn counter() -> impl Widget { - let tree = kas::column![ - align!(center, format_value!("{}")), - kas::row![ + let tree = column![ + format_value!("{}").align(AlignHints::CENTER), + row![ Button::label_msg("−", Increment(-1)), Button::label_msg("+", Increment(1)), ] .map_any(), ]; - Adapt::new(tree, 0).on_message(|_, count, Increment(add)| *count += add) + tree.with_state(0) + .on_message(|_, count, Increment(add)| *count += add) } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::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() + let theme = kas::theme::SimpleTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); + let window = Window::new(counter(), "Counter").escapable(); + app.with(window).run() } diff --git a/examples/hello.rs b/examples/hello.rs index db19cda..8a6a762 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,9 +1,12 @@ -use kas::widgets::dialog::MessageBox; +use kas::widgets::{column, Button}; +use kas::window::Window; -fn main() -> kas::app::Result<()> { - env_logger::init(); +fn main() -> kas::runner::Result<()> { + let ui = column![ + "Hello, world!", + Button::label("&Close").with(|cx, _| cx.exit()) + ]; + let window = Window::new(ui, "Hello").escapable(); - let window = MessageBox::new("Message").into_window("Hello world"); - - kas::app::Default::new(())?.with(window).run() + kas::runner::Runner::new(())?.with(window).run() } diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs index 565deeb..05e4b45 100644 --- a/examples/sync-counter.rs +++ b/examples/sync-counter.rs @@ -1,24 +1,20 @@ -use kas::widgets::{format_data, label_any, Adapt, Button, Slider}; -use kas::{messages::MessageStack, Action, Window}; +use kas::widgets::{column, format_data, row, AdaptWidget, Button, Label, Slider}; +use kas::window::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 { +impl kas::runner::AppData for Count { + fn handle_messages(&mut self, messages: &mut kas::runner::MessageStack) { if let Some(Increment(add)) = messages.try_pop() { self.0 += add; - Action::UPDATE - } else { - Action::empty() } } } -fn counter() -> impl kas::Widget { +fn counter(title: &str) -> Window { // Per window state: (count, increment). type Data = (Count, i32); let initial: Data = (Count(0), 1); @@ -27,28 +23,32 @@ fn counter() -> impl kas::Widget { struct SetValue(i32); let slider = Slider::right(1..=10, |_, data: &Data| data.1).with_msg(SetValue); - let ui = kas::column![ + let ui = 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))), + Button::new(Label::new_any("Sub")).with(|cx, data: &Data| cx.push(Increment(-data.1))), + Button::new(Label::new_any("Add")).with(|cx, data: &Data| cx.push(Increment(data.1))), ], ]; - Adapt::new(ui, initial) + let ui = ui + .with_state(initial) .on_update(|_, state, count| state.0 = *count) - .on_message(|_, state, SetValue(v)| state.1 = v) + .on_message(|_, state, SetValue(v)| state.1 = v); + Window::new(ui, title).escapable() } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::Result<()> { env_logger::init(); - let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0); + let count = Count(0); + let theme = kas_wgpu::ShadedTheme::new(); - kas::app::Default::with_theme(theme) - .build(Count(0))? - .with(Window::new(counter(), "Counter 1")) - .with(Window::new(counter(), "Counter 2")) + let mut runner = kas::runner::Runner::with_theme(theme).build(count)?; + let _ = runner.config_mut().font.set_size(24.0); + runner + .with(counter("Counter 1")) + .with(counter("Counter 2")) .run() } From ef0df79cce855025cb51a05e475ba7fd114db1c9 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:35:27 +0100 Subject: [PATCH 03/18] Update examples/custom-widget.rs --- examples/custom-widget.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/custom-widget.rs b/examples/custom-widget.rs index 67390e5..31cfc43 100644 --- a/examples/custom-widget.rs +++ b/examples/custom-widget.rs @@ -5,18 +5,17 @@ use kas::widgets::{format_value, AccessLabel, Button, Row, Text}; struct Increment(i32); impl_scope! { - #[widget{ - layout = column![ - align!(center, self.display), - self.buttons, - ]; - }] + #[widget] + #[layout(column![ + self.display.align(AlignHints::CENTER), + self.buttons, + ])] struct Counter { core: widget_core!(), #[widget(&self.count)] display: Text, #[widget] - buttons: Row>, + buttons: Row<[Button; 2]>, count: i32, } impl Self { @@ -44,12 +43,13 @@ impl_scope! { } } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::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() + let window = Window::new(Counter::new(0), "Counter"); + + let theme = kas::theme::SimpleTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); + app.with(window).run() } From ddfc42216ca5dffedec4966933fa797575962182 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:40:52 +0100 Subject: [PATCH 04/18] Update README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index b9dbbc2..4818c35 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Example apps can be found in the Run examples like this: ```sh -cargo run --example simple-window +cargo run --example hello ``` ## Licence From 09f74efdb9231dd0148cc635ef77835d2b93d405 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:41:37 +0100 Subject: [PATCH 05/18] Remove local Cargo config --- .cargo/config.toml | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 .cargo/config.toml diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index a6b014e..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build] -target-dir = "target" From 54a870e2caba50933b21e22b3b41294a9c6d2776 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:44:58 +0100 Subject: [PATCH 06/18] Bump Edition to 2024 --- Cargo.toml | 2 +- examples/calculator.rs | 2 +- examples/counter.rs | 2 +- examples/custom-widget.rs | 2 +- examples/hello.rs | 2 +- examples/sync-counter.rs | 2 +- tests/Cargo.toml | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 63a8a0a..d392659 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "kas-tutorials" version = "0.1.0" authors = ["Diggory Hardy "] -edition = "2021" +edition = "2024" resolver = "2" publish = false diff --git a/examples/calculator.rs b/examples/calculator.rs index 2277e94..ba14c5e 100644 --- a/examples/calculator.rs +++ b/examples/calculator.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use kas::event::NamedKey; use kas::prelude::*; -use kas::widgets::{column, grid, AccessLabel, Adapt, Button, EditBox}; +use kas::widgets::{AccessLabel, Adapt, Button, EditBox, column, grid}; type Key = kas::event::Key; diff --git a/examples/counter.rs b/examples/counter.rs index 9f9c0f8..6d7e564 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -1,5 +1,5 @@ use kas::prelude::*; -use kas::widgets::{column, format_value, row, Button}; +use kas::widgets::{Button, column, format_value, row}; #[derive(Clone, Debug)] struct Increment(i32); diff --git a/examples/custom-widget.rs b/examples/custom-widget.rs index 31cfc43..0313a8d 100644 --- a/examples/custom-widget.rs +++ b/examples/custom-widget.rs @@ -1,5 +1,5 @@ use kas::prelude::*; -use kas::widgets::{format_value, AccessLabel, Button, Row, Text}; +use kas::widgets::{AccessLabel, Button, Row, Text, format_value}; #[derive(Clone, Debug)] struct Increment(i32); diff --git a/examples/hello.rs b/examples/hello.rs index 8a6a762..65fb023 100644 --- a/examples/hello.rs +++ b/examples/hello.rs @@ -1,4 +1,4 @@ -use kas::widgets::{column, Button}; +use kas::widgets::{Button, column}; use kas::window::Window; fn main() -> kas::runner::Result<()> { diff --git a/examples/sync-counter.rs b/examples/sync-counter.rs index 05e4b45..35cfafd 100644 --- a/examples/sync-counter.rs +++ b/examples/sync-counter.rs @@ -1,4 +1,4 @@ -use kas::widgets::{column, format_data, row, AdaptWidget, Button, Label, Slider}; +use kas::widgets::{AdaptWidget, Button, Label, Slider, column, format_data, row}; use kas::window::Window; #[derive(Clone, Debug)] diff --git a/tests/Cargo.toml b/tests/Cargo.toml index aafeb1b..5c75eb9 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "tests" version = "0.1.0" -edition = "2021" +edition = "2024" publish = false [dependencies] From ffd84fb395af54f3bc9f3e2809f1ff48ba832d15 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:46:01 +0100 Subject: [PATCH 07/18] Bump mdbook version --- .github/workflows/gh-pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 73c6db8..b280e36 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -23,7 +23,7 @@ jobs: - name: Setup mdBook uses: peaceiris/actions-mdbook@v2 with: - mdbook-version: '0.4.21' + mdbook-version: '0.4.52' # mdbook-version: 'latest' - name: Build book From 028a3e52e07f17b3ed3c8d0fe692daae912d88aa Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:47:36 +0100 Subject: [PATCH 08/18] Bump env_logger version --- Cargo.toml | 2 +- tests/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d392659..023508a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,6 @@ resolver = "2" publish = false [dependencies] -env_logger = "0.10" +env_logger = "0.11" kas = "0.16" kas-wgpu = "0.16" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 5c75eb9..e71fcdb 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -6,6 +6,6 @@ publish = false [dependencies] doc-comment = "0.3.3" -env_logger = "0.10" +env_logger = "0.11" kas = "0.16" kas-wgpu = "0.16" From 54085b18628b01bd3bb2054f91d26a39c08bce10 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 11:48:31 +0100 Subject: [PATCH 09/18] Update Intro --- src/hello.md | 17 ----------------- src/intro.md | 17 ++++++++++------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/hello.md b/src/hello.md index 46efdce..da5c7e1 100644 --- a/src/hello.md +++ b/src/hello.md @@ -24,23 +24,6 @@ fn main() -> kas::app::Result<()> { cargo run --example hello ``` -## Logging - -Enabling a logger is optional, but can be very useful for debugging: -```rust -env_logger::init(); -``` -Kas uses the [`log`](https://crates.io/crates/log) facade internally. To see the -output, we need an implementation, such as -[`env_logger`](https://crates.io/crates/env_logger). - -Trace level can be a bit chatty; to get a *reasonable* level of output you might -try this: -```sh -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`]: diff --git a/src/intro.md b/src/intro.md index d8a81cb..f5a54eb 100644 --- a/src/intro.md +++ b/src/intro.md @@ -1,7 +1,7 @@ # Kas Tutorials These tutorials concern the [Kas GUI system](https://github.com/kas-gui/kas). -See also the [Kas examples](https://github.com/kas-gui/kas/tree/master/kas-wgpu/examples) +See also the [Kas examples](https://github.com/kas-gui/kas/tree/master/examples) and [7GUIs examples](https://github.com/kas-gui/7guis/). Further reading can be found on the [Kas blog](https://kas-gui.github.io/blog/). @@ -16,11 +16,10 @@ If not, then [Learn Rust](https://www.rust-lang.org/learn)! 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**. +Kas supports **stable** Rust, however better proc-macro diagnostics (including warnings) are available when using **nightly** Rust with Kas's `nightly-diagnostics` feature. Tutorials use the latest stable release of [Kas](https://github.com/kas-gui/kas), -currently v0.14. +currently v0.16. ## Examples @@ -39,16 +38,20 @@ What is `kas`? Here is a heavily-reduced dependency tree: ```plain kas — Wrapper crate to expose all components under a single API ├── kas-core — Core types, traits and event handling +│ ├── accesskit — UI accessibility infrastructure │ ├── arboard — Clipboard support (optional) │ ├── async-global-executor — Executor supporting EventState::push_spawn (optional) │ ├── easy-cast — Numeric type-casting, re-exposed as kas::cast +│ ├── image — Imaging library for common image formats │ ├── 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) +│ │ ├── fontique — Font enumeration and fallback +│ │ ├── swash — Font introspection and glyph rendering │ │ ├── pulldown-cmark — Markdown parsing (optional) -│ │ └── rustybuzz — Shaping (optional, default) +│ │ ├── rustybuzz — Shaping (optional, default) +│ │ ├── ttf-parser — Font parser for TrueType, OpenType and AAT +│ │ └── unicode-bidi — Unicode Bidirectional Algorithm │ ├── log — Logging facade │ ├── serde — Serialization support for persistent configuration (optional) │ ├── serde_json, serde_yaml, ron — Output formats for configuration (optional) From 2769daad680549da9730eb4f26e322e316f37211 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 12:06:56 +0100 Subject: [PATCH 10/18] Move logging section to Intro --- src/intro.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/intro.md b/src/intro.md index f5a54eb..e8c63d1 100644 --- a/src/intro.md +++ b/src/intro.md @@ -32,6 +32,20 @@ cd tutorials cargo run --example counter ``` +## Logging + +Kas uses the [`log`](https://crates.io/crates/log) facade internally. To enable output, we need an implementation, such as [`env_logger`](https://crates.io/crates/env_logger). Add this to `fn main()`: +```rust +env_logger::init(); +``` + +Trace level can be a bit chatty; to get a *reasonable* level of output you might +try this: +```sh +export RUST_LOG=warn,naga=error,kas=debug +cargo run --example counter +``` + ## Kas Dependencies What is `kas`? Here is a heavily-reduced dependency tree: From 673aabee0a64413fff74dd49b47047713aec0bbd Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 12:27:46 +0100 Subject: [PATCH 11/18] Update hello screenshot --- src/screenshots/hello.png | Bin 9499 -> 4111 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/screenshots/hello.png b/src/screenshots/hello.png index 9a789926a7cdf4e56ca785562992369a6369b8a0..077c2fb4adb2f05a7c5199135d826c45e2470080 100644 GIT binary patch literal 4111 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 hwT+Jp0gK-(50DVl|_MXr6 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%0A0MB*vhu>;&N#4& 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 Date: Wed, 10 Sep 2025 12:27:46 +0100 Subject: [PATCH 12/18] Update Hello --- src/hello.md | 83 +++++++++++++++------------------------------------- 1 file changed, 24 insertions(+), 59 deletions(-) diff --git a/src/hello.md b/src/hello.md index da5c7e1..93b9a95 100644 --- a/src/hello.md +++ b/src/hello.md @@ -9,14 +9,17 @@ Lets get started with a simple message box. ```rust # extern crate kas; -use kas::widgets::dialog::MessageBox; +use kas::widgets::{Button, column}; +use kas::window::Window; -fn main() -> kas::app::Result<()> { - env_logger::init(); +fn main() -> kas::runner::Result<()> { + let ui = column![ + "Hello, world!", + Button::label("&Close").with(|cx, _| cx.exit()) + ]; + let window = Window::new(ui, "Hello").escapable(); - let window = MessageBox::new("Message").into_window("Hello world"); - - kas::app::Default::new(())?.with(window).run() + kas::runner::Runner::new(())?.with(window).run() } ``` @@ -24,64 +27,26 @@ fn main() -> kas::app::Result<()> { cargo run --example hello ``` -## A window, a shell +## The UI -Next, we construct a [`MessageBox`] widget, then wrap with a [`Window`]: -```rust -# extern crate kas; -# use kas::widgets::dialog::MessageBox; -let window = MessageBox::new("Message") - .into_window("Hello world"); -# let _: kas::Window<()> = window; -``` +We use the [`column!`] macro to construct our layout. This macro turns string literals into label widgets for us, ensuring that "Hello, world!" will appear on the screen. -Finally, we construct a default app, add this window, and run: -```rust -# 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() -# } -``` +For the button, we use a [`Button`] widget. The button's action handler calls [`EventState::exit`] to terminate the UI. (To close the window without terminating the UI, we would instead call `cx.window_action(Action::CLOSE);`.) -[`kas::app::Default`] is just a parameterisation of [`kas::app::Application`] which selects a sensible graphics backend and theme. +## The Window -If you wanted to select your own theme instead, you could do so as follows: -```rust -# 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() -# } -``` +We construct a [`Window`] over the `ui` and a title. We also call [`Window::escapable`] to allow our window to be closed using the Escape key. -Or, if you wanted to specify the graphics backend and theme: -```rust -# 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() -# } -``` +## The Runner + +Every UI needs a [`Runner`]. In this example we simply construct a runner over data `()`, add a single window, and run. In later examples you will see how we can select a theme, use input data, multiple windows and tweak the configuration. -Finally, [`Application::run`] starts our UI. This method runs the event-loop internally, returning `Ok(())` once all windows have closed successfully. +Finally, [`Runner::run`] starts our UI. This method runs the event-loop internally, returning `Ok(())` once all windows have closed successfully. -[`MessageBox`]: https://docs.rs/kas/latest/kas/widgets/dialog/struct.MessageBox.html +[`column!`]: https://docs.rs/kas/latest/kas/widgets/macro.column.html +[`Button`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html [`Window`]: https://docs.rs/kas/latest/kas/struct.Window.html -[`kas::app::Default`]: https://docs.rs/kas/latest/kas/app/type.Default.html -[`kas::app::Application`]: https://docs.rs/kas/latest/kas/app/struct.Application.html -[`Application::run`]: https://docs.rs/kas/latest/kas/app/struct.Application.html#method.run -[`winit::event_loop::EventLoop::run`]: https://docs.rs/winit/latest/winit/event_loop/struct.EventLoop.html#method.run +[`Window::escapable`]: https://docs.rs/kas/latest/kas/struct.Window.html#method.escapable +[`EventState::exit`]: https://docs.rs/kas/latest/kas/event/struct.EventState.html#method.exit +[`Runner`]: https://docs.rs/kas/latest/kas/runner/struct.Runner.html +[`Runner::run`]: https://docs.rs/kas/latest/kas/runner/struct.Runner.html#method.run From ffa66f95f0d6220f4e8b12c0a1a8cd673bfad119 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Wed, 10 Sep 2025 12:29:04 +0100 Subject: [PATCH 13/18] Update Counter --- examples/counter.rs | 10 +-- src/counter.md | 176 ++++++++++++++++++++------------------------ 2 files changed, 86 insertions(+), 100 deletions(-) diff --git a/examples/counter.rs b/examples/counter.rs index 6d7e564..7d27bcb 100644 --- a/examples/counter.rs +++ b/examples/counter.rs @@ -5,13 +5,13 @@ use kas::widgets::{Button, column, format_value, row}; struct Increment(i32); fn counter() -> impl Widget { + let buttons = row![ + Button::label_msg("−", Increment(-1)), + Button::label_msg("+", Increment(1)), + ]; let tree = column![ format_value!("{}").align(AlignHints::CENTER), - row![ - Button::label_msg("−", Increment(-1)), - Button::label_msg("+", Increment(1)), - ] - .map_any(), + buttons.map_any(), ]; tree.with_state(0) diff --git a/src/counter.md b/src/counter.md index 65d7840..e69d1e0 100644 --- a/src/counter.md +++ b/src/counter.md @@ -9,35 +9,38 @@ The last example was a bit boring. Lets get interactive! ```rust # extern crate kas; use kas::prelude::*; -use kas::widgets::{format_value, Adapt, Button}; +use kas::widgets::{Button, column, format_value, row}; #[derive(Clone, Debug)] struct Increment(i32); fn counter() -> impl Widget { - let tree = kas::column![ - align!(center, format_value!("{}")), - kas::row![ - Button::label_msg("−", Increment(-1)), - Button::label_msg("+", Increment(1)), - ] - .map_any(), + let buttons = row![ + Button::label_msg("−", Increment(-1)), + Button::label_msg("+", Increment(1)), + ]; + let tree = column![ + format_value!("{}").align(AlignHints::CENTER), + buttons.map_any(), ]; - Adapt::new(tree, 0).on_message(|_, count, Increment(add)| *count += add) + tree.with_state(0) + .on_message(|_, count, Increment(add)| *count += add) } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::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() + let theme = kas::theme::SimpleTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); + let window = Window::new(counter(), "Counter").escapable(); + app.with(window).run() } ``` +## Preamble + #### Prelude The [`kas::prelude`] includes a bunch of commonly-used, faily unambiguous stuff: @@ -61,9 +64,17 @@ This is (return position) [impl trait](https://doc.rust-lang.org/stable/rust-by- (We'll get back to this type `Data` in a bit.) -## Layout +## Widgets + +What is a widget? Simply a type implementing the [`Widget`] trait (or, depending on the context, an instance of such a type). -Our user interface should be a widget tree: lets use a column layout over the count and \[a row layout over the buttons\]. +Widgets must implement the super-traits [`Layout`] and [`Tile`], both of which are object-safe (use [`Tile::as_tile`] to get a `&dyn Tile`). [`Widget`] is also object-safe, but only where its associated [`Widget::Data`] type is specified (see [Input Data](#input-data) below). + +In this example we'll only use library widgets and macro-synthesized widgets; [custom widgets](custom-widget.md) will be covered later. + +### Layout macros + +Our user interface should be a widget tree: lets use a [`row!`] of buttons and a [`column!`] layout for the top-level UI tree: ```rust # extern crate kas; # use kas::prelude::*; @@ -71,49 +82,41 @@ Our user interface should be a widget tree: lets use a column layout over the co # #[derive(Clone, Debug)] # struct Increment(i32); # fn counter() -> impl Widget { -let tree = kas::column![ - align!(center, format_value!("{}")), - kas::row![ + let buttons = row![ Button::label_msg("−", Increment(-1)), Button::label_msg("+", Increment(1)), - ] - .map_any(), -]; -# Adapt::new(tree, 0) + ]; + let tree = column![ + format_value!("{}").align(AlignHints::CENTER), + buttons.map_any(), + ]; + # let _ = tree; # } ``` -### Layout macros - -[`kas::column!`] and [`kas::row!`] are layout macros which, as the name suggests, construct a column/row over other widgets. +[`row!`] and [`column!`] are deceptively simple macros which construct a column or row over other widgets. I say *deceptively* simple because a fair amount of these macro's functionality is hidden, such as constructing a label widget from a string literal and emulating the [`.align(..)`](`AdaptWidget::align`) and [`.map_any()`](`AdaptWidgetAny::map_any`) (see [Input Data](#input-data)) method calls we see here. Still, you *should* be able to ignore this complexity. -[`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,* +## Input Data -- (`std`) [`column!`](https://doc.rust-lang.org/stable/std/macro.column.html) 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. +The [`Widget::Data`] type mentioned above is used to provide all Kas widgets with *input data*. This is passed into [`Events::update`] (called whenever the data may have changed) and to a number of event-handling methods. +Why? Most UIs need some form of mutable state. Some modern UI toolkits like [Iced](https://github.com/iced-rs/iced) and [Xilem](https://github.com/linebender/xilem) reconstruct their view tree (over a hidden widget tree) when this state changes; [egui](https://github.com/emilk/egui) goes even further and reconstructs the whole widget tree. Older stateful toolkits like GTK and Qt require binding widget properties or explicitly updating widgets. Kas finds a compromise between these models: widgets are stateful, yet derived from a common object and updated as required. -## Input data +In our case, [`format_value!`] constructs a [`Text`] widget which formats its input data (an `i32`) to a `String` and displays that. -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). +Since it would be inconvenient to require an entire UI tree to use the same input data, Kas provides some tools to map that data (or in Xilem/Druid terminology, view that data through a lens): -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? +- [`AdaptWidget::map`] takes a closure which can, for example, map a struct-reference to a struct-field-reference. (In fact this is effectively all it can do due to lifetime restrictions; anything more complex requires using [`Adapt`] or similar.) +- [`AdaptWidgetAny::map_any`] simply discards its input, passing `&()` to its child. +- [`Adapt`] stores a mutable value in the UI tree, passing this value to its child. +- [Custom widgets](custom-widget.md) may store state in the UI tree and pass arbitrary references to children. ### 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 +In this case, we'll use `()` as our top-level data and an [`Adapt`] node for the mutable state (the count). The [next chapter](sync-counter.md) will use top-level data instead. -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, +The code: ```rust # extern crate kas; # use kas::prelude::*; @@ -122,54 +125,26 @@ Thus, # struct Increment(i32); # fn counter() -> impl Widget { # let tree = format_value!("{}"); -Adapt::new(tree, 0) + tree.with_state(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. +calls [`AdaptWidget::with_state`] to construct an [`Adapt`] widget over `0` (with type `i32`). +A reference to this (i.e. `&i32`) is passed into our display widget (`format_value!("{}")`). Meanwhile, +we used `buttons.map_any()` to ignore this value and pass `&()` to the [`Button`] widgets. ## 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 [`push`]es 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: -```rust -#[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: -```text -[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`). +While *input data* gets state *into* widgets, *messages* let us get, well, messages *out* of widgets. -### Buttons +Any widget in the UI tree may post a message. While sometimes such messages have an intended recipient, often they are simply pushed to a message stack. Any widget above the source in the UI tree may handle messages (of known type). -This should be obvious: `Button::label_msg("+", Increment(1))` constructs a [`Button`][Button] which pushes the message `Increment(1)` when pressed. +In practice, message handling has three steps: -### Handling messages +1. Define a message type, in this case `Increment`. The only requirement of this type is that it supports `Debug`. (While we could in this case just use `i32`, using a custom type improves type safety and provides a better message in the log should any message go unhandled.) +2. A widget (e.g. our buttons) pushes a message to the stack using [`EventCx::push`]. Many widgets provide convenience methods to do this, for example [`Button::label_msg`]. +3. Some widget above the sender in the UI tree retrieves the message using [`EventCx::try_pop`] and handles it somehow. [`Adapt::on_message`] provides a convenient way to write such a handler. -Finally, we can handle our button click: ```rust # extern crate kas; # use kas::prelude::*; @@ -178,27 +153,38 @@ Finally, we can handle our button click: # struct Increment(i32); # fn counter() -> impl Widget { # let tree = format_value!("{}"); -Adapt::new(tree, 0) - .on_message(|_, count, Increment(add)| *count += add) + tree.with_state(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!("{}"))`. -[map_any]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any +Aside: feel free to write your message emitters first and handlers later. If you miss a handler you will see a message like this in your log: +```text +[2025-09-10T14:38:06Z WARN kas_core::erased] unhandled: Erased(Increment(1)) +``` +While the custom message types like `Increment` will not save you from *forgetting* to handle something, they will at least yield a comprehensible message in your log and prevent something else from handling the wrong message. + +Should multiple messages use `enum` variants or discrete struct types? Either option works fine. Consider perhaps where the messages will be handled. + + +[`Widget`]: https://docs.rs/kas/latest/kas/trait.Widget.html +[`Layout`]: https://docs.rs/kas/latest/kas/trait.Layout.html +[`Tile`]: https://docs.rs/kas/latest/kas/trait.Tile.html +[`Tile::as_tile`]: https://docs.rs/kas/latest/kas/trait.Tile.html#method.as_tile +[`AdaptWidgetAny::map_any`]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any [`kas::prelude`]: https://docs.rs/kas/latest/kas/prelude/index.html -[`kas::column!`]: https://docs.rs/kas/latest/kas/macro.column.html -[`kas::row!`]: https://docs.rs/kas/latest/kas/macro.row.html -[`kas::align!`]: https://docs.rs/kas/latest/kas/macro.align.html +[`column!`]: https://docs.rs/kas/latest/kas/widgets/macro.column.html +[`row!`]: https://docs.rs/kas/latest/kas/widgets/macro.row.html +[`AdaptWidget::align`]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidget.html#method.align +[`AdaptWidget::map`]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidget.html#method.map +[`AdaptWidget::with_state`]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidget.html#method.with_state [`Widget::Data`]: https://docs.rs/kas/latest/kas/trait.Widget.html#associatedtype.Data [`Events::update`]: https://docs.rs/kas/latest/kas/trait.Events.html#method.update [`Text`]: https://docs.rs/kas/latest/kas/widgets/struct.Text.html [`format_value!`]: https://docs.rs/kas/latest/kas/widgets/macro.format_value.html [`Adapt`]: https://docs.rs/kas/latest/kas/widgets/struct.Adapt.html -[`kas::event`]: https://docs.rs/kas/latest/kas/event/index.html -[`push`]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.push -[`try_pop`]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.try_pop -[Button]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html +[`EventCx::push`]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.push +[`EventCx::try_pop`]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.try_pop +[`Button`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html [`Adapt::on_message`]: https://docs.rs/kas/latest/kas/widgets/struct.Adapt.html#method.on_message -[AccessLabel]: https://docs.rs/kas/latest/kas/widgets/struct.AccessLabel.html -[label_any]: https://docs.rs/kas/latest/kas/widgets/fn.label_any.html [`Button::label_msg`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html#method.label_msg From dfc74a3902fb73f075b3595e0adc6f13eeb6b21d Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 11 Sep 2025 08:40:16 +0100 Subject: [PATCH 14/18] Update Sync-counter --- src/sync-counter.md | 86 ++++++++++++++++++++++----------------------- 1 file changed, 43 insertions(+), 43 deletions(-) diff --git a/src/sync-counter.md b/src/sync-counter.md index b54af63..78c6a63 100644 --- a/src/sync-counter.md +++ b/src/sync-counter.md @@ -9,27 +9,23 @@ We complicate the previous example just a little bit! ```rust # extern crate kas; -use kas::widgets::{format_data, label_any, Adapt, Button, Slider}; -use kas::{messages::MessageStack, Action, Window}; +use kas::widgets::{AdaptWidget, Button, Label, Slider, column, format_data, row}; +use kas::window::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 { +impl kas::runner::AppData for Count { + fn handle_messages(&mut self, messages: &mut kas::runner::MessageStack) { if let Some(Increment(add)) = messages.try_pop() { self.0 += add; - Action::UPDATE - } else { - Action::empty() } } } -fn counter() -> impl kas::Widget { +fn counter(title: &str) -> Window { // Per window state: (count, increment). type Data = (Count, i32); let initial: Data = (Count(0), 1); @@ -38,38 +34,40 @@ fn counter() -> impl kas::Widget { struct SetValue(i32); let slider = Slider::right(1..=10, |_, data: &Data| data.1).with_msg(SetValue); - let ui = kas::column![ + let ui = 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))), + Button::new(Label::new_any("Sub")).with(|cx, data: &Data| cx.push(Increment(-data.1))), + Button::new(Label::new_any("Add")).with(|cx, data: &Data| cx.push(Increment(data.1))), ], ]; - Adapt::new(ui, initial) + let ui = ui + .with_state(initial) .on_update(|_, state, count| state.0 = *count) - .on_message(|_, state, SetValue(v)| state.1 = v) + .on_message(|_, state, SetValue(v)| state.1 = v); + Window::new(ui, title).escapable() } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::Result<()> { env_logger::init(); - let theme = kas_wgpu::ShadedTheme::new().with_font_size(24.0); + let count = Count(0); + let theme = kas_wgpu::ShadedTheme::new(); - kas::app::Default::with_theme(theme) - .build(Count(0))? - .with(Window::new(counter(), "Counter 1")) - .with(Window::new(counter(), "Counter 2")) + let mut runner = kas::runner::Runner::with_theme(theme).build(count)?; + let _ = runner.config_mut().font.set_size(24.0); + runner + .with(counter("Counter 1")) + .with(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: +In the previous example, our top-level `AppData` was `()` and our mutable state was stored in an [`Adapt`] widget. This time, we will store our counter in top-level `AppData`, in a custom type which includes a message handler: ```rust # extern crate kas; # use kas::{messages::MessageStack, Action}; @@ -90,13 +88,9 @@ impl kas::app::AppData for Count { } } ``` -[`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))`. +[`AppData::handle_messages`] is more verbose than [`Adapt::on_message`], but does the same job. The method notifies when widgets must be updated by returning [`Action::UPDATE`]. -Note that `Count` is now an input to the widgets we construct: +To integrate this into our example, we pass a `Count` object into [`kas::runner::Builder::build`] and adjust the prototype of `counter` to: ```rust # extern crate kas; # use kas::{messages::MessageStack, Action}; @@ -125,7 +119,7 @@ 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: +We'll skip right over the widget declarations to the new [`Adapt`] node: ```rust # extern crate kas; # use kas::widgets::{label_any, Adapt}; @@ -136,18 +130,21 @@ We'll skip right over the widget declarations to the new `Adapt` node: # 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) + let ui = ui + .with_state(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). +Aside: you may wonder why we store `count` in [`Adapt`]'s state at all. Why not simply pass `(&Count, &i32)` (count, increment) down to the local UI? The answer is that we can't, because of lifetimes. To be specific, the input data type is formalized as an associated type, [`Widget::Data`], which must outlive instances of that type: that is any references embedded in an input data type must outlive the instances of the widgets they are passed to. Moreover, [`AppData`] requires lifetime `'static` (more as a simplification than because we truely couldn't support non-static lifetimes here, though there really isn't much use for them). + +Aside aside: could we not make [`Widget::Data`] into a Generic Associated Type (GAT) to support lifetimes shorter than that of the widget object? Well, yes, but traits with GATs are not (yet) object-safe. This is a problem because object-safe widget types are important (both for variadic layout — e.g. a `TabStack` where pages use different widget types — and more fundamentally, namely to make [`Node`] work). So *maybe* this will be possible eventually, dependent on future Rust development. -## Multiple windows +## Running 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: +Constructing multiple windows under a UI runner is simple: ```rust # extern crate kas; # use kas::{messages::MessageStack, Action, Window}; @@ -160,15 +157,15 @@ It barely seems worth mentioning, but there's nothing stopping us from calling ` # 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() + let mut runner = kas::runner::Runner::with_theme(theme).build(count)?; + let _ = runner.config_mut().font.set_size(24.0); + runner + .with(counter("Counter 1")) + .with(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`. +Each window has its own local state stored in its [`Adapt`] node (the `increment`) while sharing the top-level `Count`. [`AppData`]: https://docs.rs/kas/latest/kas/app/trait.AppData.html [`AppData::handle_messages`]: https://docs.rs/kas/latest/kas/app/trait.AppData.html#tymethod.handle_messages @@ -176,3 +173,6 @@ Of course, each window has its own local state stored in its [`Adapt`] node (the [`Adapt::on_message`]: https://docs.rs/kas/latest/kas/widgets/struct.Adapt.html#method.on_message [`Action::UPDATE`]: https://docs.rs/kas/latest/kas/struct.Action.html#associatedconstant.UPDATE [`Adapt::on_update`]: https://docs.rs/kas/latest/kas/widgets/struct.Adapt.html#method.on_update +[`kas::runner::Builder::build`]: https://docs.rs/kas/latest/kas/runner/struct.Builder.html#method.build +[`Widget::Data`]: https://docs.rs/kas/latest/kas/trait.Widget.html#associatedtype.Data +[`Node`]: https://docs.rs/kas/latest/kas/struct.Node.html From 23fc5d122dc496919133759f59e7ba8d532398f4 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 11 Sep 2025 08:41:24 +0100 Subject: [PATCH 15/18] Add chapter on Configuration --- src/SUMMARY.md | 1 + src/config.md | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 src/config.md diff --git a/src/SUMMARY.md b/src/SUMMARY.md index db909b7..ce212c3 100644 --- a/src/SUMMARY.md +++ b/src/SUMMARY.md @@ -4,6 +4,7 @@ - [Hello: push button](hello.md) - [Counter: an interactive widget](counter.md) - [Sync-counter: Adapt'ing AppData](sync-counter.md) +- [Configuration](config.md) - [Calculator: make_widget and grid](calculator.md) - [Custom widgets](custom-widget.md) - [Data models](data-models.md) diff --git a/src/config.md b/src/config.md new file mode 100644 index 0000000..2169636 --- /dev/null +++ b/src/config.md @@ -0,0 +1,60 @@ +# Configuration + +*Topics: themes and UI configuration* + +We won't build anything new this chapter. Instead, we'll take a moment to discuss configuration. + + +## Themes + +Kas supports theme abstraction: widgets, for the most part, don't precisely determine their sizes or handle the minutae of drawing. + +Theming is abstracted and exposed to widgets through two interfaces: + +- [`SizeCx`] supplies widgets with size information +- [`DrawCx`] is used to draw widget elements + +Kas currently provides three theme implementations (along with one meta-implementation): + +- [`kas::theme::SimpleTheme`] prioritises simplicity without loss of functionality. +- [`kas::theme::FlatTheme`] extends `SimpleTheme`, putting more effort into styling while using no complex drawing techniques (well, if one doesn't count fonts). +- [`kas_wgpu::ShadedTheme`] extends `FlatTheme` using shaded drawing for bevelled widget borders. The resulting styling is rather opinionated, bordering on a tech demo (it could further be adapted to e.g. use the mouse pointer as a light source instead of assuming a fixed light position, though it would quickly become apparent that the theme lacks true shadows). +- [`kas::theme::MultiTheme`] supports run-time switching between pre-loaded themes. It is used by the [Gallery example]. + + +## Configuration + +Previously we adjusted the font size before the UI was started: +```rust +# extern crate kas; +# use kas::prelude::*; +# fn main() -> kas::runner::Result<()> { + let theme = kas::theme::SimpleTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); +# } +``` + +Various aspects of fonts, themes, event handling and shortcuts may be adjusted here; see the [`Config`] struct. + +The above snippet adjusts the default configuration before the UI is started using [`Runner::config_mut`]. The returned [`Action`] is discarded (`let _ =`) since the UI has not yet been started. + +Configuration may also be accessed at run-time ([`EventState::config`]) and adjusted using [`WindowConfig::update_base`], though this has some limitations; in particular fonts are not re-selected and new widget sizes are not fully realized without manual resizing of the window. + +Pre-launch, one may supply a configuration factory through [`Builder::with_config`]. More specifically, this allows using a [`ReadWriteFactory`] to persist configuration to/from local storage. + + +[`Runner::config_mut`]: https://docs.rs/kas/latest/kas/runner/struct.Runner.html#method.config_mut +[`Action`]: https://docs.rs/kas/latest/kas/struct.Action.html +[`EventState::config`]: https://docs.rs/kas/latest/kas/event/struct.EventState.html#method.config +[`WindowConfig::update_base`]: https://docs.rs/kas/latest/kas/config/struct.WindowConfig.html#method.update_base +[`ReadWriteFactory`]: https://docs.rs/kas/latest/kas/config/struct.ReadWriteFactory.html +[`Builder::with_config`]: https://docs.rs/kas/latest/kas/runner/struct.Builder.html#method.with_config +[Gallery example]: https://github.com/kas-gui/kas/tree/master/examples#gallery +[`Config`]: https://docs.rs/kas/latest/kas/config/struct.Config.html +[`SizeCx`]: https://docs.rs/kas/latest/kas/theme/struct.SizeCx.html +[`DrawCx`]: https://docs.rs/kas/latest/kas/theme/struct.DrawCx.html +[`kas::theme::SimpleTheme`]: https://docs.rs/kas/latest/kas/theme/struct.SimpleTheme.html +[`kas::theme::FlatTheme`]: https://docs.rs/kas/latest/kas/theme/struct.FlatTheme.html +[`kas::theme::MultiTheme`]: https://docs.rs/kas/latest/kas/theme/struct.MultiTheme.html +[`kas_wgpu::ShadedTheme`]: https://docs.rs/kas-wgpu/latest/kas_wgpu/struct.ShadedTheme.html From 82e4746359e0f7b5fc79d6db8cc665bdce51ac3e Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 11 Sep 2025 15:47:01 +0100 Subject: [PATCH 16/18] Update Calculator --- src/calculator.md | 113 ++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 58 deletions(-) diff --git a/src/calculator.md b/src/calculator.md index e5c70d1..4232edf 100644 --- a/src/calculator.md +++ b/src/calculator.md @@ -6,12 +6,13 @@ ```rust # 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; +use kas::event::NamedKey; +use kas::prelude::*; +use kas::widgets::{AccessLabel, Adapt, Button, EditBox, column, grid}; + type Key = kas::event::Key; fn key_button(label: &str) -> Button { @@ -23,15 +24,14 @@ fn key_button_with(label: &str, key: Key) -> Button { Button::label_msg(label, key.clone()).with_access_key(key) } -fn calc_ui() -> impl Widget { +fn calc_ui() -> Window<()> { // 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_lines(3.0, 3.0) .with_width_em(5.0, 10.0); - // We use map_any to avoid passing input data (not wanted by buttons): - let buttons = kas::grid! { + let buttons = grid! { // Key bindings: C, Del (0, 0) => Button::label_msg("&clear", Key::Named(NamedKey::Clear)) .with_access_key(NamedKey::Delete.into()), @@ -57,22 +57,22 @@ fn calc_ui() -> impl Widget { } .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); - }) + let ui = Adapt::new(column![display, buttons], Calculator::new()) + .on_message(|_, calc, key| calc.handle(key)); + + Window::new(ui, "Calculator") + .escapable() + .with_alt_bypass() + .without_nav_focus() } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::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() + let theme = kas_wgpu::ShadedTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); + app.with(calc_ui()).run() } #[derive(Clone, Debug)] @@ -139,7 +139,7 @@ fn key_button_with(label: &str, key: Key) -> Button { ### 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`]. +Normally, access keys are only active while holding Alt. To avoid this requirement we call [`with_alt_bypass`]. Further, we disable Tab key navigation with [`without_nav_focus`] and ensure that the window can be closed with the Esc key. ```rust # extern crate kas; # use kas::{Widget, widgets::{label_any, Adapt}}; @@ -150,21 +150,18 @@ Normally, access keys are only active while holding Alt. To avoid thi # fn handle(&mut self, _key: ()) {} # } # fn ui() -> impl Widget { -# 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); - }) +# let ui = label_any(""); + Window::new(ui, "Calculator") + .escapable() + .with_alt_bypass() + .without_nav_focus() # } ``` ## Grid layout -We already saw column and row layouts. This time, we'll use [`kas::grid!`] for layout. +We already saw column and row layouts. This time, we'll use [`grid!`] for layout. ```rust # extern crate kas; # use kas::event::NamedKey; @@ -180,36 +177,36 @@ We already saw column and row layouts. This time, we'll use [`kas::grid!`] for l # Button::label_msg(label, key.clone()).with_access_key(key) # } # fn ui() -> impl Widget { -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(); + let buttons = 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. +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. Again, we use .[map_any][]() to make our buttons (input `Data = ()`) compatible with the parent UI element (input `Data = Calculator`). @@ -220,7 +217,7 @@ Again, we use .[map_any][]() to make our buttons (input `Data = ()` [`Button`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html [`Button::with_access_key`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html#method.with_access_key [`Activate`]: https://docs.rs/kas/latest/kas/messages/struct.Activate.html -[`disable_nav_focus`]: https://docs.rs/kas/latest/kas/event/struct.ConfigCx.html#method.disable_nav_focus -[`enable_alt_bypass`]: https://docs.rs/kas/latest/kas/event/struct.EventState.html#method.enable_alt_bypass -[`kas::grid!`]: https://docs.rs/kas/latest/kas/macro.grid.html +[`without_nav_focus`]: https://docs.rs/kas/latest/kas/window/struct.Window.html#method.without_nav_focus +[`with_alt_bypass`]: https://docs.rs/kas/latest/kas/window/struct.Window.html#method.with_alt_bypass +[`grid!`]: https://docs.rs/kas/latest/kas/widgets/macro.grid.html [map_any]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any From 3a4b4c7ef1dd33464358a8e1faefb114b0b97c48 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 11 Sep 2025 15:49:14 +0100 Subject: [PATCH 17/18] Update Custom widget --- examples/custom-widget.rs | 3 +- src/custom-widget.md | 207 +++++++++++++++++++++++--------------- 2 files changed, 126 insertions(+), 84 deletions(-) diff --git a/examples/custom-widget.rs b/examples/custom-widget.rs index 0313a8d..efeb338 100644 --- a/examples/custom-widget.rs +++ b/examples/custom-widget.rs @@ -4,7 +4,8 @@ use kas::widgets::{AccessLabel, Button, Row, Text, format_value}; #[derive(Clone, Debug)] struct Increment(i32); -impl_scope! { +#[impl_self] +mod Counter { #[widget] #[layout(column![ self.display.align(AlignHints::CENTER), diff --git a/src/custom-widget.md b/src/custom-widget.md index 92419f8..979432a 100644 --- a/src/custom-widget.md +++ b/src/custom-widget.md @@ -2,6 +2,11 @@ *Topics: custom widgets* +Custom widgets are essentially the hard way to do things, but they do have their uses: + +- Custom local state and event handlers are significantly more flexible than [`Adapt`] +- Custom [`Layout`] implementations and custom mid-level graphics operations (e.g. the [Clock example](https://github.com/kas-gui/kas/tree/master/examples#clock)) + ![Counter](screenshots/counter.png) 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. @@ -9,24 +14,24 @@ Here we rewrite the counter as a custom widget. There's no reason to do so for t ```rust # extern crate kas; use kas::prelude::*; -use kas::widgets::{format_value, AccessLabel, Button, Row, Text}; +use kas::widgets::{AccessLabel, Button, Row, Text, format_value}; #[derive(Clone, Debug)] struct Increment(i32); -impl_scope! { - #[widget{ - layout = column![ - align!(center, self.display), - self.buttons, - ]; - }] +#[impl_self] +mod Counter { + #[widget] + #[layout(column![ + self.display.align(AlignHints::CENTER), + self.buttons, + ])] struct Counter { core: widget_core!(), #[widget(&self.count)] display: Text, #[widget] - buttons: Row>, + buttons: Row<[Button; 2]>, count: i32, } impl Self { @@ -54,69 +59,101 @@ impl_scope! { } } -fn main() -> kas::app::Result<()> { +fn main() -> kas::runner::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() + let window = Window::new(Counter::new(0), "Counter"); + + let theme = kas::theme::SimpleTheme::new(); + let mut app = kas::runner::Runner::with_theme(theme).build(())?; + let _ = app.config_mut().font.set_size(24.0); + app.with(window).run() } ``` ## Macros -### `impl_scope` +### `impl_self` -[`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](https://github.com/rust-lang/rustfmt/pull/5538).) Here, it serves two purposes: +[`impl_self`] is an attribute macro from [impl-tools]. This macro wraps a type definition and `impl`s on that type with a fake module of the same name. This fake module (here `mod Counter`) does not need to import (`use`) symbols from the parent module; in fact it may only contain one type definition with the same name as the fake module and `impl` items on this type. -1. `impl Self` syntax (not important here, but much more useful on structs with generics) -2. To support the [`#[widget]`][attr-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). +Unfortunately, rust-analyzer does not fully support this: it may insert `use` statements inside the fake module. It may also mis-report errors against the entire fake module. One may instead use the [`impl_scope!`] macro, however since [`rustfmt` refuses to format its contents](https://github.com/rust-lang/rustfmt/pull/5538) this is the worse option. Perhaps some day this stuff will get fixed? -### `#[widget]` +So, *why* do we have to wrap our widget implementations with a macro? Firstly, it supports `impl Self` syntax. Secondly (and much more importantly), it allows the `#[widget]` macro to operate on the type definition and various widget trait implementations simultaneously. This allows the macro to do useful things, like provide contextual default method implementations, inject debugging checks into provided method implementations, provide contextual warnings, and use a synthesized type to store extra state required by macro-generated layout code. -The [`#[widget]`][attr-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. +### `#[widget]` -**First**, we must apply [`#[widget]`][attr-widget] to the struct. (The `layout = ...;` argument (and `{ ... }` braces) are optional; some other arguments might also occur here.) -```ignore - #[widget{ - layout = column![ - align!(center, self.display), - self.buttons, - ]; - }] +The [`#[widget]`][attr-widget] attribute-macro is used to implement the [`Widget`] trait. *This is the only supported way to implement [`Widget`].* +```rust,ignore + #[widget] + struct Counter { /* .. */ } ``` -**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!()`. -```ignore +Like it or not, the `#[widget]` macro is a fairly significant piece of what makes Kas work. Fortunately, most of the complexity is hidden such that you don't need to know about it and can refer to documentation on standard Rust traits. + +To get the best diagnostics, be sure to use the `nightly-diagnostics` feature. (Hopefully Rust will stabilize custom proc-macro lints in the next year or so!) + +#### Core data + +All widgets must have a "core data" field. Typically this is named `core`; it must have type `widget_core!()` and can be initialized using `Default`. +```rust,ignore core: widget_core!(), ``` -**Third**, any fields which are child widgets must be annotated with `#[widget]`. (This enables them to be configured and updated.) +#### Child widgets -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). -```rust -# extern crate kas; -# use kas::impl_scope; -# use kas::widgets::{AccessLabel, Button, Row, Text}; -# impl_scope! { -# #[widget{ -# Data = (); -# layout = ""; -# }] - struct Counter { - core: widget_core!(), +There are two types of child widgets: hidden layout-generated children and explicit children. The latter are fields with a `#[widget]` attribute: +```rust,ignore #[widget(&self.count)] display: Text, #[widget] - buttons: Row>, - count: i32, - } -# } + buttons: Row<[Button; 2]>, ``` -**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]`][attr-widget].) +The first of these is a [`Text`] widget, passed `&self.count` as input data. The second is a [`Row`] widget over [`Button`]s over [`AccessLabel`]s. Since we didn't specify a data mapping for this second widget, it is is passed the `Count` widget's input data (`()`). + +Omitting `#[widget]` on a field which is a child widget is an error; sometimes the outer `#[widget]` attribute-macro will report the issue but not always. For example, if we omit the attribute on `buttons` and run, we get a backtrace like the following: +``` +thread 'main' (413532) panicked at /path/to/kas/crates/kas-core/src/core/data.rs:123:13: +WidgetStatus of #INVALID: require Configured, found New +stack backtrace: + 0: __rustc::rust_begin_unwind + at /rustc/a1208bf765ba783ee4ebdc4c29ab0a0c215806ef/library/std/src/panicking.rs:698:5 + 1: core::panicking::panic_fmt + at /rustc/a1208bf765ba783ee4ebdc4c29ab0a0c215806ef/library/core/src/panicking.rs:75:14 + 2: kas_core::core::data::WidgetStatus::require + at /path/to/kas/crates/kas-core/src/core/data.rs:123:13 + 3: kas_core::core::data::WidgetStatus::size_rules + at /path/to/kas/crates/kas-core/src/core/data.rs:157:18 + 4: as kas_core::core::layout::Layout>::size_rules + at /path/to/kas/crates/kas-widgets/src/list.rs:207:1 + 5: ::size_rules::{{closure}} + at ./examples/custom-widget.rs:7:1 + ... + 27: custom_widget::main + at ./examples/custom-widget.rs:55:22 +``` +This tells us that some widget should have been `Configured` but had status `New` when calling `size_rules` — because we forgot to say that `buttons` is a `#[widget]` and thus needs to be configured. (This is in fact a debug-mode only check; release builds crash with a much-less-useful backtrace.) + +### Layout + +All widgets must implement the [`Layout`] trait, but only a few do so directly. Most, instead, use the `#[layout]` attribute-macro. + +```rust,ignore + #[layout(column![ + self.display.align(AlignHints::CENTER), + self.buttons, + ])] + struct Counter { /* .. */ } +``` + +In this case, we are *not* using the [`column!`] macro (which would not be able to reference `self.display`) but rather an emulation of it. Behaviour should be identical aside from this ability to reference struct fields and not needing to `use kas::widgets::column`. + +### Widget traits + +[`Widget`] has super-trait [`Tile`] which has super-trait [`Layout`]. Futher, [`Events`] is usually implemented (unless there is no event-handling logic). Impls of any of these traits may appear in a widget implementation, but none are required. + +It is however required to define the associated type [`Widget::Data`]. Since it is common to implement [`Events`] instead of [`Widget`] and `trait Events: Widget`, the `#[widget]` macro allows you to take the liberty of defining `type Data` on `Events` instead of `Widget`: ```rust # extern crate kas; # use kas::prelude::*; @@ -143,22 +180,26 @@ We can use this attribute to configure the child widget's input data too: in thi # } ``` -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.) +In this case we implement one event-handling method, [`Events::handle_messages`], to update `self.count` when an `Increment` message is received. -**Fifth**, we must specify widget layout somehow. There are two main ways of doing this: implement [`Layout`] or use the `layout` argument of [`#[widget]`][attr-widget]. To recap, we use: -```ignore - #[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! +#### Updating state + +When updating local state in a custom widget, it is requried to explicitly trigger an update to any widgets using that state as their input data. This can be done in a few ways: + +- cx.[action][](self, [Action::UPDATE]) will notify that an update to `self` (and children) is required +- cx.[update][](self.[as_node][](data)) will update `self` (and children) immediately +- cx.[update][](self.display.[as_node][](&self.count)) will update `self.display` immediately + +Note that previously: + +- We used [`Adapt::on_message`] to update state: this automatically updates children +- We used [`AppData::handle_messages`]: again, this automatically updates children + +Custom widgets are not the same in this regard. 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 +### Aside: the type of child widgets Our `Counter` has two (explicit) child widgets, and we must specify the type of each: ```rust @@ -175,43 +216,32 @@ Our `Counter` has two (explicit) child widgets, and we must specify the type of #[widget(&self.count)] display: Text, #[widget] - buttons: Row>, + buttons: Row<[Button; 2]>, # 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` in these cases and be done. Alas, Rust does not support this. We are not completely without options however: +There is no real issue in this case, but widget types can get significantly harder to write than `Row<[Button; 2]>`. Worse, some widget types are impossible to write (e.g. the result of [`row!`] or widget generics instantiated with a closure). So what can we do instead? -- 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>`. (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](https://doc.rust-lang.org/nightly/unstable-book/language-features/type-alias-impl-trait.html) "soon". This requires writing a type-def outside of the widget definition but is supported in nightly: +- It would be nice if `impl Widget` worked; alas, it does not, and I have seen little interest in support for field-position-impl-trait. But I believe Rust *could* support this. +- Rust may stabilise support for [`impl Trait` in type aliases](https://doc.rust-lang.org/nightly/unstable-book/language-features/type-alias-impl-trait.html) "soon". This requires writing a type-def outside of the widget definition but is supported in nightly Rust: ```rust,ignore type MyButtons = impl Widget; ``` +- We could use a `Box`: `Box>`. +- We could embed our `buttons` in the `#[layout]` instead of using a field. This is not always possible (e.g. for `display` which takes `&self.count` as input data). Since `#[layout]` uses `Box` internally this is effectively the same as above. +- The [`impl_anon!`] macro *does* support `impl Trait` syntax. The required code is unfortunately *a bit* hacky (hidden type generics) and at least a little prone to spitting out misleading error messages instead of *just working*. Best practice is to cross your fingers. -### 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. - +[`impl_self`]: https://docs.rs/impl-tools/latest/impl_tools/attr.impl_self.html [`impl_scope!`]: https://docs.rs/impl-tools/latest/impl_tools/macro.impl_scope.html [`impl_anon!`]: https://docs.rs/impl-tools/latest/impl_tools/macro.impl_anon.html [attr-widget]: https://docs.rs/kas/latest/kas/attr.widget.html [`Widget`]: https://docs.rs/kas/latest/kas/trait.Widget.html +[`Widget::Data`]: https://docs.rs/kas/latest/kas/trait.Widget.html#associatedtype.Data +[`Tile`]: https://docs.rs/kas/latest/kas/trait.Tile.html [`Events`]: https://docs.rs/kas/latest/kas/trait.Events.html -[`kas::column!`]: https://docs.rs/kas/latest/kas/macro.column.html +[`Events::handle_messages`]: https://docs.rs/kas/latest/kas/trait.Events.html#method.handle_messages [`Default`]: https://doc.rust-lang.org/stable/std/default/trait.Default.html [`Layout`]: https://docs.rs/kas/latest/kas/trait.Layout.html [impl-tools]: https://crates.io/crates/impl-tools @@ -219,3 +249,14 @@ But for now, custom widgets still have their uses: [`Label`]: https://docs.rs/kas/latest/kas/widgets/struct.Label.html [`Text`]: https://docs.rs/kas/latest/kas/widgets/struct.Text.html [`kas::resvg::Canvas`]: https://docs.rs/kas/latest/kas/resvg/struct.Canvas.html +[`column!`]: https://docs.rs/kas/latest/kas/widgets/macro.column.html +[`row!`]: https://docs.rs/kas/latest/kas/widgets/macro.row.html +[`Button`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html +[`Row`]: https://docs.rs/kas/latest/kas/widgets/struct.Row.html +[`AccessLabel`]: https://docs.rs/kas/latest/kas/widgets/struct.AccessLabel.html +[action]: https://docs.rs/kas/latest/kas/event/struct.EventState.html#method.action +[update]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.update +[Action::UPDATE]: https://docs.rs/kas/latest/kas/struct.Action.html#associatedconstant.UPDATE +[as_node]: https://docs.rs/kas/latest/kas/trait.Widget.html#method.as_node +[`Adapt::on_message`]: https://docs.rs/kas/latest/kas/widgets/struct.Adapt.html#method.on_message +[`AppData::handle_messages`]: https://docs.rs/kas/latest/kas/app/trait.AppData.html#tymethod.handle_messages From bf1aed9e1f2535ea7533479ad85e3b1ae4faeb63 Mon Sep 17 00:00:00 2001 From: Diggory Hardy Date: Thu, 11 Sep 2025 18:13:16 +0100 Subject: [PATCH 18/18] Fix tests --- src/calculator.md | 6 +++--- src/config.md | 1 + src/counter.md | 4 ++-- src/custom-widget.md | 22 ++++------------------ src/sync-counter.md | 38 +++++++++++++++++++------------------- 5 files changed, 29 insertions(+), 42 deletions(-) diff --git a/src/calculator.md b/src/calculator.md index 4232edf..ae08230 100644 --- a/src/calculator.md +++ b/src/calculator.md @@ -142,7 +142,7 @@ fn key_button_with(label: &str, key: Key) -> Button { Normally, access keys are only active while holding Alt. To avoid this requirement we call [`with_alt_bypass`]. Further, we disable Tab key navigation with [`without_nav_focus`] and ensure that the window can be closed with the Esc key. ```rust # extern crate kas; -# use kas::{Widget, widgets::{label_any, Adapt}}; +# use kas::{Widget, widgets::{Label, Adapt}, window::Window}; # #[derive(Debug)] # struct Calculator; # impl Calculator { @@ -150,7 +150,7 @@ Normally, access keys are only active while holding Alt. To avoid thi # fn handle(&mut self, _key: ()) {} # } # fn ui() -> impl Widget { -# let ui = label_any(""); +# let ui = Label::new_any(""); Window::new(ui, "Calculator") .escapable() .with_alt_bypass() @@ -166,7 +166,7 @@ We already saw column and row layouts. This time, we'll use [`grid!`] for layout # extern crate kas; # use kas::event::NamedKey; # use kas::prelude::*; -# use kas::widgets::{AccessLabel, Button}; +# use kas::widgets::{AccessLabel, Button, grid}; # type Key = kas::event::Key; # fn key_button(label: &str) -> Button { # let string = AccessString::from(label); diff --git a/src/config.md b/src/config.md index 2169636..926419f 100644 --- a/src/config.md +++ b/src/config.md @@ -32,6 +32,7 @@ Previously we adjusted the font size before the UI was started: let theme = kas::theme::SimpleTheme::new(); let mut app = kas::runner::Runner::with_theme(theme).build(())?; let _ = app.config_mut().font.set_size(24.0); + # Ok(()) # } ``` diff --git a/src/counter.md b/src/counter.md index e69d1e0..9dcfdbe 100644 --- a/src/counter.md +++ b/src/counter.md @@ -78,7 +78,7 @@ Our user interface should be a widget tree: lets use a [`row!`] of buttons and a ```rust # extern crate kas; # use kas::prelude::*; -# use kas::widgets::{format_value, Adapt, Button}; +# use kas::widgets::{Adapt, AdaptWidget, Button, column, format_value, row}; # #[derive(Clone, Debug)] # struct Increment(i32); # fn counter() -> impl Widget { @@ -90,7 +90,7 @@ Our user interface should be a widget tree: lets use a [`row!`] of buttons and a format_value!("{}").align(AlignHints::CENTER), buttons.map_any(), ]; - # let _ = tree; + # tree.with_state(0) # } ``` diff --git a/src/custom-widget.md b/src/custom-widget.md index 979432a..6829d81 100644 --- a/src/custom-widget.md +++ b/src/custom-widget.md @@ -113,7 +113,7 @@ There are two types of child widgets: hidden layout-generated children and expli The first of these is a [`Text`] widget, passed `&self.count` as input data. The second is a [`Row`] widget over [`Button`]s over [`AccessLabel`]s. Since we didn't specify a data mapping for this second widget, it is is passed the `Count` widget's input data (`()`). Omitting `#[widget]` on a field which is a child widget is an error; sometimes the outer `#[widget]` attribute-macro will report the issue but not always. For example, if we omit the attribute on `buttons` and run, we get a backtrace like the following: -``` +```ignore thread 'main' (413532) panicked at /path/to/kas/crates/kas-core/src/core/data.rs:123:13: WidgetStatus of #INVALID: require Configured, found New stack backtrace: @@ -160,9 +160,8 @@ It is however required to define the associated type [`Widget::Data`]. Since it # #[derive(Clone, Debug)] # struct Increment(i32); # impl_scope! { -# #[widget{ -# layout = ""; -# }] +# #[widget] +# #[layout("")] # struct Counter { # core: widget_core!(), # count: i32, @@ -202,24 +201,11 @@ Don't worry about remembering each step; macro diagnostics should point you in t ### Aside: the type of child widgets Our `Counter` has two (explicit) child widgets, and we must specify the type of each: -```rust -# extern crate kas; -# use kas::impl_scope; -# use kas::widgets::{AccessLabel, Button, Row, Text}; -# impl_scope! { -# #[widget{ -# Data = (); -# layout = ""; -# }] -# struct Counter { -# core: widget_core!(), +```rust,ignore #[widget(&self.count)] display: Text, #[widget] buttons: Row<[Button; 2]>, -# count: i32, -# } -# } ``` There is no real issue in this case, but widget types can get significantly harder to write than `Row<[Button; 2]>`. Worse, some widget types are impossible to write (e.g. the result of [`row!`] or widget generics instantiated with a closure). So what can we do instead? diff --git a/src/sync-counter.md b/src/sync-counter.md index 78c6a63..5e3b618 100644 --- a/src/sync-counter.md +++ b/src/sync-counter.md @@ -70,38 +70,35 @@ fn main() -> kas::runner::Result<()> { In the previous example, our top-level `AppData` was `()` and our mutable state was stored in an [`Adapt`] widget. This time, we will store our counter in top-level `AppData`, in a custom type which includes a message handler: ```rust # extern crate kas; -# use kas::{messages::MessageStack, Action}; +# use kas::{runner::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 { +impl kas::runner::AppData for Count { + fn handle_messages(&mut self, messages: &mut kas::runner::MessageStack) { if let Some(Increment(add)) = messages.try_pop() { self.0 += add; - Action::UPDATE - } else { - Action::empty() } } } ``` -[`AppData::handle_messages`] is more verbose than [`Adapt::on_message`], but does the same job. The method notifies when widgets must be updated by returning [`Action::UPDATE`]. +[`AppData::handle_messages`] is more verbose than [`Adapt::on_message`], but does the same job. To integrate this into our example, we pass a `Count` object into [`kas::runner::Builder::build`] and adjust the prototype of `counter` to: ```rust # extern crate kas; -# use kas::{messages::MessageStack, Action}; +# use kas::{runner::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() } +# impl kas::runner::AppData for Count { +# fn handle_messages(&mut self, messages: &mut kas::runner::MessageStack) {} # } fn counter() -> impl kas::Widget { // ... - # kas::widgets::label_any("") + # kas::widgets::Label::new_any("") } ``` @@ -122,18 +119,19 @@ Note that our local data includes a *copy* of the top-level data `Count` (along We'll skip right over the widget declarations to the new [`Adapt`] node: ```rust # extern crate kas; -# use kas::widgets::{label_any, Adapt}; +# use kas::widgets::{Adapt, AdaptWidget, Label}; # #[derive(Clone, Copy, Debug)] # struct Count(i32); # fn counter() -> impl kas::Widget { # #[derive(Clone, Debug)] # struct SetValue(i32); -# let ui = label_any(""); +# let ui = Label::new_any(""); # let initial = (Count(0), 1); let ui = ui .with_state(initial) .on_update(|_, state, count| state.0 = *count) .on_message(|_, state, SetValue(v)| state.1 = v); + # ui # } ``` 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`. @@ -147,16 +145,18 @@ Aside aside: could we not make [`Widget::Data`] into a Generic Associated Type ( Constructing multiple windows under a UI runner is simple: ```rust # extern crate kas; -# use kas::{messages::MessageStack, Action, Window}; +# use kas::{runner::MessageStack, Action, window::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() } +# impl kas::runner::AppData for Count { +# fn handle_messages(&mut self, messages: &mut kas::runner::MessageStack) {} # } -# fn counter() -> impl kas::Widget { -# kas::widgets::label_any("") +# fn counter(title: &str) -> Window { +# Window::new(kas::widgets::Label::new_any(""), title) # } -# fn main() -> kas::app::Result<()> { +# fn main() -> kas::runner::Result<()> { + # let count = Count(0); + # let theme = kas_wgpu::ShadedTheme::new(); let mut runner = kas::runner::Runner::with_theme(theme).build(count)?; let _ = runner.config_mut().font.set_size(24.0); runner