Skip to content

Commit aa8dd68

Browse files
authored
Merge pull request #19 from kas-gui/work
Add custom-widget, data-models
2 parents fa3da15 + 6180293 commit aa8dd68

File tree

6 files changed

+315
-8
lines changed

6 files changed

+315
-8
lines changed

examples/custom-widget.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
use kas::prelude::*;
2+
use kas::widgets::{format_value, AccessLabel, Button, Row, Text};
3+
4+
#[derive(Clone, Debug)]
5+
struct Increment(i32);
6+
7+
impl_scope! {
8+
#[widget{
9+
layout = column![
10+
align!(center, self.display),
11+
self.buttons,
12+
];
13+
}]
14+
struct Counter {
15+
core: widget_core!(),
16+
#[widget(&self.count)]
17+
display: Text<i32, String>,
18+
#[widget]
19+
buttons: Row<Button<AccessLabel>>,
20+
count: i32,
21+
}
22+
impl Self {
23+
fn new(count: i32) -> Self {
24+
Counter {
25+
core: Default::default(),
26+
display: format_value!("{}"),
27+
buttons: Row::new([
28+
Button::label_msg("-", Increment(-1)),
29+
Button::label_msg("+", Increment(1)),
30+
]),
31+
count,
32+
}
33+
}
34+
}
35+
impl Events for Self {
36+
type Data = ();
37+
38+
fn handle_messages(&mut self, cx: &mut EventCx, data: &()) {
39+
if let Some(Increment(incr)) = cx.try_pop() {
40+
self.count += incr;
41+
cx.update(self.as_node(data));
42+
}
43+
}
44+
}
45+
}
46+
47+
fn main() -> kas::app::Result<()> {
48+
env_logger::init();
49+
50+
let theme = kas::theme::SimpleTheme::new().with_font_size(24.0);
51+
kas::app::Default::with_theme(theme)
52+
.build(())?
53+
.with(Window::new(Counter::new(0), "Counter"))
54+
.run()
55+
}

src/SUMMARY.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,5 @@
55
- [Counter: an interactive widget](counter.md)
66
- [Sync-counter: Adapt'ing AppData](sync-counter.md)
77
- [Calculator: make_widget and grid](calculator.md)
8+
- [Custom widgets](custom-widget.md)
9+
- [Data models](data-models.md)

src/calculator.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ let buttons = kas::grid! {
211211

212212
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.
213213

214-
Again, we must use [`map_any`] to make our buttons (input `Data = ()`) compatible with the parent UI element (input `Data = Calculator`).
214+
Again, we use <code>.[map_any][]()</code> to make our buttons (input `Data = ()`) compatible with the parent UI element (input `Data = Calculator`).
215215

216216

217217
[`Key`]: https://docs.rs/kas/latest/kas/event/enum.Key.html
@@ -223,4 +223,4 @@ Again, we must use [`map_any`] to make our buttons (input `Data = ()`) compatibl
223223
[`disable_nav_focus`]: https://docs.rs/kas/latest/kas/event/struct.ConfigCx.html#method.disable_nav_focus
224224
[`enable_alt_bypass`]: https://docs.rs/kas/latest/kas/event/struct.EventState.html#method.enable_alt_bypass
225225
[`kas::grid!`]: https://docs.rs/kas/latest/kas/macro.grid.html
226-
[`map_any`]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any
226+
[map_any]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any

src/counter.md

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ let tree = kas::column![
9292
Now, you could, if you prefer, import the layout macros: `use kas::{align, column, row};`. *However,*
9393

9494
- (`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`.
95-
- If you replace `kas::row!` with `row!` you will get a compile error: the layout macro parser cannot handle <code>.[map_any](https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any)()</code>. `kas::row![..]` evaluates to a complete widget; `row![..]` as an embedded layout does not.
95+
- If you replace `kas::row!` with `row!` you will get a compile error: the layout macro parser cannot handle <code>.[map_any][]()</code>. `kas::row![..]` evaluates to a complete widget; `row![..]` as an embedded layout does not.
9696

9797

9898
## Input data
@@ -131,11 +131,11 @@ But to make this *do something* we need one more concept: *messages*.
131131

132132
### Mapping data
133133

134-
We should briefly justify `.map_any()` in our example: our [`Text`] widget expects input data (of type `i32`), while `Button::label_msg` constructs a <code>[Button](https://docs.rs/kas/latest/kas/widgets/struct.Button.html)\<[AccessLabel](https://docs.rs/kas/latest/kas/widgets/struct.AccessLabel.html)\></code> expecting data of type `()`.
134+
We should briefly justify `.map_any()` in our example: our [`Text`] widget expects input data (of type `i32`), while [`Button::label_msg`] constructs a <code>[Button][]\<[AccessLabel][]\></code> expecting data of type `()`.
135135

136-
The method <code>.[map_any](https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any)()</code> maps the row of buttons to a new widget supporting (and ignoring) *any* input data.
136+
The method <code>.[map_any][]()</code> maps the row of buttons to a new widget supporting (and ignoring) *any* input data.
137137

138-
We could instead use <code>Button::new([label_any](https://docs.rs/kas/latest/kas/widgets/fn.label_any.html)("+"))</code> which serves the same purpose, but ignoring that input data much further down the tree.
138+
We could instead use <code>[Button][]::new([label_any][]("+"))</code> which serves the same purpose, but ignoring that input data much further down the tree.
139139

140140

141141
## Messages
@@ -165,7 +165,7 @@ Use of built-in types like `()` or `i32` is possible but considered bad practice
165165

166166
### Buttons
167167

168-
This should be obvious: `Button::label_msg("+", Increment(1))` constructs a [`Button`] which pushes the message `Increment(1)` when pressed.
168+
This should be obvious: `Button::label_msg("+", Increment(1))` constructs a [`Button`][Button] which pushes the message `Increment(1)` when pressed.
169169

170170
### Handling messages
171171

@@ -184,6 +184,7 @@ Adapt::new(tree, 0)
184184
```
185185
[`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!("{}"))`.
186186

187+
[map_any]: https://docs.rs/kas/latest/kas/widgets/trait.AdaptWidgetAny.html#method.map_any
187188
[`kas::prelude`]: https://docs.rs/kas/latest/kas/prelude/index.html
188189
[`kas::column!`]: https://docs.rs/kas/latest/kas/macro.column.html
189190
[`kas::row!`]: https://docs.rs/kas/latest/kas/macro.row.html
@@ -196,5 +197,8 @@ Adapt::new(tree, 0)
196197
[`kas::event`]: https://docs.rs/kas/latest/kas/event/index.html
197198
[`push`]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.push
198199
[`try_pop`]: https://docs.rs/kas/latest/kas/event/struct.EventCx.html#method.try_pop
199-
[`Button`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html
200+
[Button]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html
200201
[`Adapt::on_message`]: https://docs.rs/kas/latest/kas/widgets/struct.Adapt.html#method.on_message
202+
[AccessLabel]: https://docs.rs/kas/latest/kas/widgets/struct.AccessLabel.html
203+
[label_any]: https://docs.rs/kas/latest/kas/widgets/fn.label_any.html
204+
[`Button::label_msg`]: https://docs.rs/kas/latest/kas/widgets/struct.Button.html#method.label_msg

src/custom-widget.md

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
# Counter: a simple widget
2+
3+
*Topics: custom widgets*
4+
5+
![Counter](screenshots/counter.png)
6+
7+
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.
8+
9+
```rust
10+
# extern crate kas;
11+
use kas::prelude::*;
12+
use kas::widgets::{format_value, AccessLabel, Button, Row, Text};
13+
14+
#[derive(Clone, Debug)]
15+
struct Increment(i32);
16+
17+
impl_scope! {
18+
#[widget{
19+
layout = column![
20+
align!(center, self.display),
21+
self.buttons,
22+
];
23+
}]
24+
struct Counter {
25+
core: widget_core!(),
26+
#[widget(&self.count)]
27+
display: Text<i32, String>,
28+
#[widget]
29+
buttons: Row<Button<AccessLabel>>,
30+
count: i32,
31+
}
32+
impl Self {
33+
fn new(count: i32) -> Self {
34+
Counter {
35+
core: Default::default(),
36+
display: format_value!("{}"),
37+
buttons: Row::new([
38+
Button::label_msg("-", Increment(-1)),
39+
Button::label_msg("+", Increment(1)),
40+
]),
41+
count,
42+
}
43+
}
44+
}
45+
impl Events for Self {
46+
type Data = ();
47+
48+
fn handle_messages(&mut self, cx: &mut EventCx, data: &()) {
49+
if let Some(Increment(incr)) = cx.try_pop() {
50+
self.count += incr;
51+
cx.update(self.as_node(data));
52+
}
53+
}
54+
}
55+
}
56+
57+
fn main() -> kas::app::Result<()> {
58+
env_logger::init();
59+
60+
let theme = kas::theme::SimpleTheme::new().with_font_size(24.0);
61+
kas::app::Default::with_theme(theme)
62+
.build(())?
63+
.with(Window::new(Counter::new(0), "Counter"))
64+
.run()
65+
}
66+
```
67+
68+
## Macros
69+
70+
### `impl_scope`
71+
72+
[`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:
73+
74+
1. `impl Self` syntax (not important here, but much more useful on structs with generics)
75+
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).
76+
77+
### `#[widget]`
78+
79+
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.
80+
81+
**First**, we must apply [`#[widget]`][attr-widget] to the struct. (The `layout = ...;` argument (and `{ ... }` braces) are optional; some other arguments might also occur here.)
82+
```ignore
83+
#[widget{
84+
layout = column![
85+
align!(center, self.display),
86+
self.buttons,
87+
];
88+
}]
89+
```
90+
91+
**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!()`.
92+
```ignore
93+
core: widget_core!(),
94+
```
95+
96+
**Third**, any fields which are child widgets must be annotated with `#[widget]`. (This enables them to be configured and updated.)
97+
98+
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).
99+
```rust
100+
# extern crate kas;
101+
# use kas::impl_scope;
102+
# use kas::widgets::{AccessLabel, Button, Row, Text};
103+
# impl_scope! {
104+
# #[widget{
105+
# Data = ();
106+
# layout = "";
107+
# }]
108+
struct Counter {
109+
core: widget_core!(),
110+
#[widget(&self.count)]
111+
display: Text<i32, String>,
112+
#[widget]
113+
buttons: Row<Button<AccessLabel>>,
114+
count: i32,
115+
}
116+
# }
117+
```
118+
119+
**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].)
120+
```rust
121+
# extern crate kas;
122+
# use kas::prelude::*;
123+
# #[derive(Clone, Debug)]
124+
# struct Increment(i32);
125+
# impl_scope! {
126+
# #[widget{
127+
# layout = "";
128+
# }]
129+
# struct Counter {
130+
# core: widget_core!(),
131+
# count: i32,
132+
# }
133+
impl Events for Self {
134+
type Data = ();
135+
136+
fn handle_messages(&mut self, cx: &mut EventCx, data: &()) {
137+
if let Some(Increment(incr)) = cx.try_pop() {
138+
self.count += incr;
139+
cx.update(self.as_node(data));
140+
}
141+
}
142+
}
143+
# }
144+
```
145+
146+
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.)
147+
148+
**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:
149+
```ignore
150+
#[widget{
151+
layout = column![
152+
align!(center, self.display),
153+
self.buttons,
154+
];
155+
}]
156+
```
157+
This is macro-parsed layout syntax (not real macros). Don't use [`kas::column!`] here; it won't know what `self.display` is!
158+
159+
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.
160+
161+
### Aside: child widget type
162+
163+
Our `Counter` has two (explicit) child widgets, and we must specify the type of each:
164+
```rust
165+
# extern crate kas;
166+
# use kas::impl_scope;
167+
# use kas::widgets::{AccessLabel, Button, Row, Text};
168+
# impl_scope! {
169+
# #[widget{
170+
# Data = ();
171+
# layout = "";
172+
# }]
173+
# struct Counter {
174+
# core: widget_core!(),
175+
#[widget(&self.count)]
176+
display: Text<i32, String>,
177+
#[widget]
178+
buttons: Row<Button<AccessLabel>>,
179+
# count: i32,
180+
# }
181+
# }
182+
```
183+
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.
184+
185+
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:
186+
187+
- 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.
188+
- We could box the widget with `Box<dyn Widget<Data = ()>>`. (This is what the `layout` syntax does for embedded widgets.)
189+
- 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.
190+
- 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:
191+
192+
```rust,ignore
193+
type MyButtons = impl Widget<Data = ()>;
194+
```
195+
196+
### Aside: uses
197+
198+
Before Kas 0.14, *all* widgets were custom widgets. (Yes, this made simple things hard.)
199+
200+
In the future, custom widgets *might* become obsolete, or might at least change significantly.
201+
202+
But for now, custom widgets still have their uses:
203+
204+
- 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.
205+
- 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.
206+
- `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).
207+
- 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.
208+
209+
[`impl_scope!`]: https://docs.rs/impl-tools/latest/impl_tools/macro.impl_scope.html
210+
[`impl_anon!`]: https://docs.rs/impl-tools/latest/impl_tools/macro.impl_anon.html
211+
[attr-widget]: https://docs.rs/kas/latest/kas/attr.widget.html
212+
[`Widget`]: https://docs.rs/kas/latest/kas/trait.Widget.html
213+
[`Events`]: https://docs.rs/kas/latest/kas/trait.Events.html
214+
[`kas::column!`]: https://docs.rs/kas/latest/kas/macro.column.html
215+
[`Default`]: https://doc.rust-lang.org/stable/std/default/trait.Default.html
216+
[`Layout`]: https://docs.rs/kas/latest/kas/trait.Layout.html
217+
[impl-tools]: https://crates.io/crates/impl-tools
218+
[`CoreData`]: https://docs.rs/kas/latest/kas/struct.CoreData.html
219+
[`Label`]: https://docs.rs/kas/latest/kas/widgets/struct.Label.html
220+
[`Text`]: https://docs.rs/kas/latest/kas/widgets/struct.Text.html
221+
[`kas::resvg::Canvas`]: https://docs.rs/kas/latest/kas/resvg/struct.Canvas.html

src/data-models.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Sync-counter: data models
2+
3+
*Topics: data models and view widgets*
4+
5+
TODO:
6+
7+
- [`ListView`] and [`ListData`]
8+
- [`Driver`], including predefined impls
9+
- [`Filter`] and [`UnsafeFilteredList`]. This rather messy to use (improvable?). The latter should eventually be replaced with a safe variant.
10+
- [`MatrixView`] and [`MatrixData`]. (Will possibly gain support for row/column labels and be renamed `TableView`.)
11+
12+
For now, see the examples:
13+
14+
- [`examples/ldata-list-view.rs`](https://github.com/kas-gui/kas/blob/master/examples/data-list-view.rs) uses [`ListView`] with custom [`ListData`] and [`Driver`]
15+
- [`examples/gallery.rs`](https://github.com/kas-gui/kas/blob/master/examples/gallery.rs#L338)'s `filter_list` uses [`UnsafeFilteredList`] with a custom [`Driver`]. Less code but possibly more complex.
16+
- [`examples/times-tables.rs`](https://github.com/kas-gui/kas/blob/master/examples/times-tables.rs) uses [`MatrixView`] with custom [`MatrixData`] and [`driver::NavView`]. Probably the easiest example.
17+
18+
[`ListView`]: https://docs.rs/kas/latest/kas/view/struct.ListView.html
19+
[`ListData`]: https://docs.rs/kas/latest/kas/view/trait.ListData.html
20+
[`Driver`]: https://docs.rs/kas/latest/kas/view/trait.Driver.html
21+
[`driver::NavView`]: https://docs.rs/kas/latest/kas/view/driver/struct.NavView.html
22+
[`Filter`]: https://docs.rs/kas/latest/kas/view/filter/trait.Filter.html
23+
[`UnsafeFilteredList`]: https://docs.rs/kas/latest/kas/view/filter/struct.UnsafeFilteredList.html
24+
[`MatrixView`]: https://docs.rs/kas/latest/kas/view/struct.MatrixView.html
25+
[`MatrixData`]: https://docs.rs/kas/latest/kas/view/trait.MatrixData.html

0 commit comments

Comments
 (0)