|
| 1 | +# Counter: a simple widget |
| 2 | + |
| 3 | +*Topics: custom widgets* |
| 4 | + |
| 5 | + |
| 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 |
0 commit comments