Newer
Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
## Background
So I'm working on improving the focus management and I came across an idea. However, not one that I felt comfortable just dropping a PR for without opening it up to discussion first (since it could just be a horrendous idea). So here's the problem I found and a possible solution I came up with.
## Problem
A lot of internal stuff is exposed to the end user that either doesn't need to be or shouldn't be. At best it can be confusing or misleading to users, and at worst it can cause some users to use code in ways they shouldn't (i.e., unintentional hacks that are unstable and could break between versions).
For example, it seems wrong that a user can do this: `self.set_id(Index::default())`. And while this might not be that big of an issue ( "users should just *not* do that" ), what if we want to add other functionality onto widgets in the future? Maybe we add the ability to give widgets their own classes for theming. This might be confusing for the user who thinks that `self.add_class("foo")` should work on its own (but might not since the context also needs to be made aware of the change).
Additionally, `KayakContext`, has a lot of functionality that doesn't need to be exposed within the context of a widget, but needs to elsewhere (such as the `KayakContext::render` and `KayakContext::process_events` methods). Methods like these need to be public, but they're dangerous to use within a widget's render method.
So in summary the two main issues are:
1. Internal widget data is exposed to the user in the render function in a confusing manner
2. Error-prone `KayakContext` methods are also exposed to the user in the render function when they shouldn't be
## Solution
I think the widget system could be restructured in a way that creates a layer of abstraction between the widgets and their context.
Let's assume we have this widget:
```rust
#[widget]
fn MyWidget(name: String, fill: Option<Color>) {
// ...
}
```
### Props Extraction
To get around the first issue of the widget having too much mutable access to its required widget data, we could separate the render function from the widget itself by placing the actual logic inside an associated method.
This would remove the access to a mutable `self`. But we still need access to the actual props. To solve this issue we can generate a new struct, `MyWidgetProps`, to contain the prop data. We can then pass this into the render function.
And now we have something that looks a bit more like this:
```rust
struct MyWidget {
pub id: Index,
pub children: Children,
pub props: MyWidgetProps,
// ...
}
struct MyWidgetProps {
pub name: String,
pub fill: Option<Color>,
}
impl Widget for MyWidget {
fn render(&mut self, context: &mut KayakContext) {
Self::render(self.props, context);
}
}
impl MyWidget {
fn render(props: MyWidgetProps, context: &mut KayakContext) {
// Generated logic
}
}
```
### Widget Context
Now comes the second issue of direct access to `KayakContext`. I think a good way of solving this would be to generate an interface struct. This struct would act as a middleman between the render function and `KayakContext`, forwarding only select methods to the context.
There are two ways of going about this.
1. Create a local struct:
```rust
struct MyWidgetContext<'a> {
context: &'a mut KayakContext,
widget: &'a mut MyWidget
}
```
2. Create a one-size-fits all struct
```rust
struct MyWidgetContext<'a> {
context: &'a mut KayakContext,
widget: &'a mut dyn Widget
}
```
Though option 1 results in more code being generated, I actually like it better since it allows for greater customization in cases where the widget is written out manually instead of via cookie-cutter macro.
Using this interface struct, we can then replace the direct `KayakContext` access:
```rust
impl MyWidget {
fn render(mut this: MyWidgetContext) {
// Generated logic
}
}
```
> We can of course call the parameter `context` or something. I'm just being cheeky by calling it `this` here ;)
And of course we'll need some code for it to be useable:
```rust
/// We could also create a trait for common methods, but this is probably fine
/// since we'll likely only ever use it internally within a particular widget
/// (in other words, not as a passable trait object)
impl<'a> MyWidgetContext<'a> {
pub fn props(&self) -> &MyWidgetProps {
&self.widget.props
}
pub fn set_styles(&mut self, styles: Option<Style>) {
self.widget.styles = styles;
}
// ...
}
impl MyWidget {
fn render(mut this: MyWidgetContext) {
let MyWidgetProps {name, fill} = this.props();
// Generated logic
}
}
```
And again, the purpose here is to hide internals so the user isn't confused and doesn't code in a hacky way:
```rust
impl MyWidget {
fn render(mut this: MyWidgetContext) {
// It's now impossible to change a widget's ID internally
self.set_id(Index::default());
// You can no longer render-ception
context.render();
// This might seem like a hack to dispatch events but it's an error
context.process_events(vec![InputEvent::MouseLeftPress]);
// Now we can make this properly notify the context
this.add_class("foo");
}
}
```
### TL;DR
1. Generate `MyWidgetProps`
2. Move rendering to associated function
3. Create `MyWidgetContext` to interface between render function and `MyWidget` and `KayakContext`
4. ???
5. Profit
## Alternatives
### Faith
The biggest alternative, again, is just to trust that the developer isn't going to cause any issues and that we make it clear what code *could* cause issues. But I think we can still do something to help. And coming up with a solution also improves the overall experience and API.
### Data-less Widgets
One big alternative to a portion of this is to just not store data on the widget itself. All relevant data for a widget will be stored in the `KayakContext`. This forces all code that needs a bit of data about a widget to also need access to the current context.
This could make code outside the functional component much more cumbersome to use.
Additionally, we run into the issue of storing the widget's ID, since that data does need to exist on the widget regardless.
### Context Setters
One of the issues I briefly mentioned was about the idea that updating a widget's own data does not directly alert the context. This could be solved by just having users write the notification code:
```rust
#[widget]
fn Foo(context: &mut KayakContext) {
self.add_class("bar");
context.notify(KayakNotify::ClassAdded); // or however it's implemented
}
```
From an API perspective, this isn't great. Using the Widget Context idea, we can move that required code into the interface method:
```rust
impl<'a> FooContext<'a> {
pub fn add_class(&mut self, class: &str) {
self.widget.add_class(class);
context.notify(KayakNotify::ClassAdded);
}
}
#[widget]
fn Foo() {
this.add_class("bar");
}
```
Also note that we can't do this:
```rust
#[widget]
fn Foo(context: &mut KayakContext) {
context.add_class("bar");
}
```
Since this requires that `Foo` have a public method `add_class` for the context to set. And the alternatives for those issues go back to the ones listed above.
### Move Render Function into Widget Context
One potential alternative to what's mentioned above would be to move the associated render function from `MyWidget` into the `MyWidgetContext`. This would allow `self` to be used as normal and even give direct access to `context` and `widget` if needed, while still granting the separation that allows us to add custom logic.
## Sample Generated Output
<details>
<summary><em>Show/Hide Code</em></summary>
```rust
use crate::OnEvent;
use crate::render_command::RenderCommand;
use crate::styles::{Style, StyleProp};
mod my_widget_module {
use derivative::Derivative;
use crate::{Color, Index, KayakContext, OnEvent, Widget};
use crate::styles::Style;
#[derive(Derivative)]
#[derivative(Default, Debug, PartialEq)]
pub struct MyWidget {
pub id: Index,
pub props: MyWidgetProps,
pub styles: Option<Style>,
#[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
pub on_event: Option<crate::OnEvent>,
#[derivative(Default(value = "None"), Debug = "ignore", PartialEq = "ignore")]
pub children: crate::Children,
}
#[derive(Derivative)]
#[derivative(Default, Debug, PartialEq)]
pub struct MyWidgetProps {
pub name: String,
pub fill: Option<Color>,
}
pub struct MyWidgetContext<'a> {
context: &'a mut KayakContext,
widget: &'a mut MyWidget,
}
impl<'a> MyWidgetContext<'a> {
pub fn set_styles(&mut self, styles: Option<Style>) {
self.widget.styles = styles;
}
pub fn on_event(&mut self, handler: Option<OnEvent>) {
self.widget.on_event = handler;
}
pub fn props(&self) -> &MyWidgetProps {
&self.widget.props
}
}
impl MyWidget {
pub fn generate_context<'a>(&'a mut self, context: &'a mut KayakContext) -> MyWidgetContext<'a> {
MyWidgetContext {
widget: self,
context,
}
}
}
impl Widget for MyWidget {
fn get_id(&self) -> Index {
self.id
}
fn focusable(&self) -> bool {
true
}
fn set_id(&mut self, id: Index) {
self.id = id;
}
fn get_styles(&self) -> Option<Style> {
self.styles.clone()
}
fn get_name(&self) -> String {
String::from("MyWidget")
}
fn on_event(&mut self, context: &mut KayakContext, event: &mut crate::Event) {
let this = self.generate_context(context);
if let Some(on_event) = this.widget.on_event.as_mut() {
if let Ok(mut on_event) = on_event.0.write() {
on_event(this.context, event);
}
}
}
fn render(&mut self, context: &mut KayakContext) {
Self::render(self.generate_context(context));
}
}
}
use my_widget_module::{MyWidget, MyWidgetContext, MyWidgetProps};
// Outside the module so it doesn't have access to the internals
impl MyWidget {
pub fn render(mut this: MyWidgetContext) {
let MyWidgetProps { fill, name } = this.props();
this.set_styles(Some(Style {
render_command: StyleProp::Value(RenderCommand::Empty),
..Default::default()
}));
this.on_event(Some(OnEvent::new(move |_, evt| {
println!("Event fired!");
})));
// ...
}
}
```
</details>
## Feedback
This is all just me thinking of ways to improve on these issues. Are they worth the trouble? And would we stand to gain from implementing a solution like this? I think so, but I could be totally and completely wrong. There could also be other, better solutions than the one I came up with.
Anyways, just wanted to bring this up and see what people thought. So let me know!
## Addendum
### Tracking Changes
An additional feature this might allow is tracking changes that can't be tracked during the render phase:
```rust
#[widget]
fn MyWidget(disabled: bool) {
this.set_focusable(disabled);
// ...
}
```
And in `MyWidgetContext`:
```rust
pub fn set_focusable(&mut self, focusable: bool) {
self.widget.focusable = focusable;
// If we need to track this change, we could do something like:
self.edits.insert(WidgetEdit::Focus);
}
```
Then if we need to handle any changes we can just iterate through the set of changes and handle them individually.
```rust
impl Widget for MyWidget {
// ...
fn on_event(&mut self, context: &mut KayakContext, event: &mut crate::Event) {
let mut this = self.generate_context(context);
if let Some(mut on_event) = this.widget.on_event.clone() {
if let Ok(mut on_event) = on_event.0.write() {
on_event(this, event);
}
}
// Maybe something like this? (assuming it all compiles like it does in my head)
let edits = this.edits;
context.process_changes(edits);
}
// ...
}
```