-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add State Refs #166
base: main
Are you sure you want to change the base?
Add State Refs #166
Conversation
Implements the StateRef type for storing WidgetState derived types generated by owlkettle
This introduces the StateRef type which is a ref-type containing a reference to a Widget. It is used for the pseudo field "stateRef" for each Widget, which for any given Widget will contain a reference to said widget. This allows accessing references within Widgets and passing them back and forth. It is reactive, to allow subscribing to it and getting updated every time the reference changes.
It should not be filled in a given playground, therefore it should not show up in there in the first place.
The playground was not checking for nil if a value on a state was a ref-type. That lead to seg faults. It now does those checks.
Previously the stateRef got the observer unsubscribed and resubscribed every update. That shouldn't happen, we only want to do that when the stateRef actually change. Therefore we now check first for changes in the ref. That uncovered that we need to also do the initial subscription. This is now handled in the build hook for this field.
The last 2 commits implement an example on how to use the stateRef by implementing a feature we previously didn't have - keyCaptureWidget on search. It allows "forwarding" keypress-events that happen while another widget is in focus to the SearchEntry Widget. From what I can tell it works correctly. Some thoughts where I'd like to have your opinion:
|
Just had a chat with leorize: He suggests using the This would basically change this: ScrolledWindow:
stateRef = app.keyCaptureRef
... other stuff.... to this ScrolledWindow as app.keyCaptureRef:
... other stuff.... It'd require some finageling with the DSL I think but in my eyes this improves clarity on where a ref comes from quite a bit. Any thoughts on that or hints where I'd best look at for implementing that? |
I did some experimenting about how to implement this. ScrolledWindow as app.keyCaptureRef:
ListBox:
... other stuff ... I wrote an in-depth explanation of what I did and the reasoning behind it in the commit message if further info is required. |
…ng code from those Node-types. First it allows parsing expressions such as "<Widget> as <someStateRefVariable>". This is supposed to express an assignment of <someStateRefVariable> to the "valStateRef" Field on the <Widget> instance. The variable gets stored in the "widgetRefVar" field on "Node". There it is optional, as expressions such as "<Widget>:" without the "as <someStateRefVariable>" syntax obviously do not provide a variable to which a given signalState could be assigned. The "<Widget> as <someStateRefVariable>" syntax creates an infix expression which is part of nnkCallKinds. Therefore in parseGui I removed it from the nnkCallKinds group as it requires slightly different parsing than other nnkCallKinds based nodes. In the code-generation step, when generating a NodeWidget there now is a small check if the gui-DSL contained such an assignment. If so, the variable <someStateRefVariable> gets assigned to the Widgets "valStateRef" field. This is functionally equivalent to just having this syntax: Widget: stateRef = <someStateRefVariable> But is more readable.
d06c1cb
to
56c88cb
Compare
This looks pretty nice! How does it interact with adders and the |
The way it currently is implemented I think it doesn't interact with the insert statement at all. As for with adders, in which sense do you mean? Edit: Like so: method view(app: AppState): Widget =
let isInActiveSearch = app.text != ""
let window = gui:
ScrolledWindow as app.keyCaptureRef:
ListBox:
... lots of stuff ...
result = gui:
Window():
defaultSize = (600, 400)
HeaderBar() {.addTitlebar.}:
insert(app.toAutoFormMenu(ignoreFields = @["filteredItems"])) {.addRight.}
SearchEntry() {.addTitle, expand: true.}:
keyCaptureRef = app.keyCaptureRef
...lots of fields and events...
insert window This works as expected. |
My question is if using |
I think that such refactoring should be made in a separate PR as they are not really related to adding Refs (or given the current title of this PR, adding refs should be a separate PR). |
The Syntax "<Widget>() as <someStateRefVariable>" did not work. This was due to a parsing-oversight in parseGui. This PR corrects that.
Right now this breaks the DSL, should be fixable by adjusting some of the parsing again. |
Check for ident("as") by using eqIdent instead of comparing kind and stringified value.
This is going to interact with adder or rather pragmas in general in a really ugly way that I haven't yet found a simple way to solve. Parsing of pragmas happens in |
When using pragmas, specifically adders, the "as" syntax does not function gracefully. It will break as the infix-parsing-section of parseGUI is unfamiliar on how to deal with pragmas. This change fixes that.
@can-lehmann Fixed the interaction with adders not working, example for that is added to SearchEntry. I likely shouldn't put that there but inspiration didn't hit me on how/where else to demonstrate it. Other than that I fixed the rest of your comments. |
The stateRef field is a custom field that does not stem from assignment anyway. Therefore it is not necessary to follow the "val<Field>"/"has<Field>" convention for fields. We know it is a ref-type, just do a nil-check instead.
I recently wrote a guide on the GUI DSL. You need to add a short subsection on state refs there anyways, so maybe add it there. Of course add a simple example first and then say "To get the reference of a widget with adders, the following syntax is used: " |
Another issue you need to solve is what happens when a widget subsribes to a state ref but is deleted afterwards. Currently the observer still remains registered causing a use after free as far as I can see. |
The obvious and clean way to solve that efficiently architecturally speaking is having a destroy hook. |
let stateRefVar = case node[2].kind: | ||
of nnkPragmaExpr: node[2][0] | ||
else: node[2] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This just ignores the adder, doesn't it?
If you want to use the syntax Widget {.adder.} as ...
. Please report an error if the user tries to use Widget as ... {.adder.}
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does not in fact. Like, I didn't think deeper about that fact, but the DSL combination that gets created by Widget {.somepragma.} as ...
appears to run into the else block in line 105.
else:
error("Tried to use 'as' with invalid syntax", node)
At least when I added
ListBoxRow() {.addRow.} as app.dummyRef:
Label(text = "Static last")
To the search_entry example it blew up in my face with exactly that error message.
owlkettle/guidsl.nim
Outdated
let hasStateRef = not node.stateRef.isNil() | ||
if hasStateRef: | ||
let refVar = node.stateRef | ||
let refAssignment = quote do: | ||
`name`.stateRef = `refVar` | ||
body.add(refAssignment) | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let hasStateRef = not node.stateRef.isNil() | |
if hasStateRef: | |
let refVar = node.stateRef | |
let refAssignment = quote do: | |
`name`.stateRef = `refVar` | |
body.add(refAssignment) | |
if not node.stateRef.isNil: | |
body.add: quote do: | |
`name`.stateRef = `node.stateRef` | |
complexity left over from when you were using optionals.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
`name`.stateRef = `node.stateRef`
You can't do this replacement, this won't compile due to
/home/isofruit/dev/owlkettle/owlkettle/guidsl.nim(144, 20) Error: type mismatch: got 'array[0..-1, empty]' for '[]' but expected 'seq[Node]'
I assume this is quote do being inconsistent in its behavior if you don't have an explicit var that you want to insert.
Edit: Moving not node.stateRef.isNil
directly into the if statement I have now refactored.
Any thoughts on how a destroy-hook in owlkettle would be implemented? |
You might need to attach it to the gtk widget directly (via destroy notify I believe), as the lifetime of the |
Memo to me for later in another PR: |
The other option would be making sure that the lifetime of the gtk widget is always at least equal (or longer) than the lifetime of the WidgetState (by using g_object_ref/unref correctly). This might actually be a nice thing to do. Then you could use |
I'll claim incompetence on that one, this goes over my head on how I'd even make sure that is correct. It also conceptually makes no sense to me because in my head WidgetState persists across various iterations of Widgets, it just gets updated. So how a Widget would outlive WidgetState is beyond me. That's one of the reason why the usage of StateRef requires you assigning it to WidgetState. Edit: So sth with this syntax: renderable SomeWidget of BaseWidget:
... some fields ...
hooks:
onDestroy:
... do fascinating things ... Which means modifying the renderable macro |
Okay, so I found it rather easy to add destroy-hooks to be parseable, just add to the HookEnum and to parseHookKind and you're done. However, generating the necessary code for that I'm massively struggling with. `state`.connect(`hook`, "destroy", eventCallback) per destroy hook. But I'm hitting walls on how to do so. |
This PR is a draft to demonstrate how I plan on acting while in the DSL part of the code.
Given that macros and DSLs in general have a tendency to be pretty mean in terms of complexity, I developed a few habits over the years to structure those well enough so I can more easily get back into it after a while.
I plan on introducing some of those in the owlkettle DSL because I believe them to be beneficial to the overall project and for readability.
I wanted to open up this PR to already show you how I tend to act on every proc I touch when it comes to these to give you the chance to veto any of these habits up front.
(Sidenote, the below proc I mostly touched because I originally added a stateRef field to the generated WidgetState, then realized that's unnecessary, but at that point I'd already written myself a doc comment and it felt like a waste to throw that away)
In general:
Procs that generate NimNodes and thus Code directly I also annotate with a pattern that demonstrates what they generate (see doc comment in the code)
I just found it a ton easier to reason about what a given NimNode is if I can know for sure what NimNodeKind it is.
That's visible here in how I replaced the bits of result with var fieldName until the final code generation step