I just merged a re-write of how widgets were implemented in Fidgetty. All of the tests have been updated, though I'm still working on the example apps.
The re-write was based on feedback from initial testing, both my own and others. It is a breaking change from the previous syntax thought the conversion is easy. However, I believe the re-write makes the widget's more robust and easier to work with.
The new system builds on a pattern where each widget has a template that creates a new Fidget node before the user's code is run. The user's code is run in the current function but in a new block. This avoids dealing with anonymous procs to handle user logic. The template calls a render proc which then runs the widget logic on the current Fidget node. This also means that render proc's are composable since they just work on the current Fidget node.
Overall this pattern makes working with events and binding variables much better. It also encourages using one-way data binding with events to update application state similar to React or Vue. Auto-complete also works with widget properties, though it gets confused a lot.
Here's an example of using the new widget system:
import fidgetty
fidgetty CounterButton:
properties:
label: string
state:
count: int
proc new*(_: typedesc[CounterButtonProps]): CounterButtonProps =
new result
fill palette.foreground
proc render*(
props: CounterButtonProps,
self: CounterButtonState
): Events =
clipContent true
cornerRadius theme.cornerRadius
onHover:
highlight palette
onClick:
self.count.inc()
text "counter button":
boxSizeOf parent
fill palette.text
characters fmt"label ({self.count})"
It can then be used:
type
AppState* = ref object
count1: int
proc exampleApp*() =
## defines a stateful app widget
frame "main":
font "IBM Plex Sans", 16, 200, 0, hCenter, vCenter
useState(AppState, self) #\
# gets an app state object that's cached in the current node
# if the Fidget node is destroyed so is the state
CounterButton:
box 3'em, 6'em, 8'em, 2'em #\
# this code modifies a "CounterButton" node
# but runs in the current proc
onClick:
# for simple events the user can use normal Fidget actions
echo "hi!"
Widgets can also provide custom events using variant types. There's a couple of syntax options:
frame "main":
...
Dropdown:
size 10'em, 2'em
defaultLabel "test"
items dropItems
selected dropIndexes[0]
finally:
# This let's the user provide a block of code
# which runs after the widget has been run
# and shares the same block and Fidget node
echo "hello from after the widget"
processEvents(ValueChange): # macro to handle widget events
Index(idx):
dropIndexes[0] = idx
refresh()
DropdownMenu:
size 10'em, 2'em
defaultLabel "test"
items dropItems
selected dropIndexes[0]
do -> ValueChange:
Index(idx):
dropIndexes[1] = idx
Action(name):
echo "menu action ", name, " was run"
...
# This is a convenience syntax that
# runs all matching `ValueChange` events
# its a bit odd at first but makes it easy to
# read how events from a widget are handled
And proof that it works (as requested by @ElegantBeef ;)).
Here's just the testGridLayout showing debugging lines when using CSS grid:
Also with this re-write done I plan to start writing more widgets again! The previous design just made binding data or using events difficult and fragile.
No I appreciate the feedback! No sense in making a library that's not useable for others.
The examples above aren't really complete and/or correct. I modified them on the fly for the post. :) But I found some issues with trying the actual countButton from the Readme when I tried compiling it outside the repo.
How did you install Fidgetty -- did you clone the repo or just use nimble? I haven't tested the nimble install route so I think it runs into export issues.
Thanks, sorry about that. I pushed a release for cssgrid for this, however, it looks like I was still on a feature branch :face-palm:. It's pushed properly now!
I'm planning to setup CI for it soon, to automate the releases a bit better and test these things.
I've been toying with a cells demo and getting "tabbing" to work.
It's still very rough, but was fairly easy to start. There's some work on how to handle non-keyboard inputs, and the blinkers, etc.
Here's the super complicated algorithm. ;) It can probably be simplified, but it's just a simple DFS. Technically Fidget allows overlapping text inputs, etc, so there needs to be some safety / sanity around that.
proc nextFocus*(parent, node: Node, foundFocus: var bool): bool =
## find the next node to focus on
for child in node.nodes:
if child.selectable:
if foundFocus:
child.setFocus = true
return true
if child == keyboard.focusNode:
foundFocus = true
else:
if nextFocus(node, child, foundFocus):
return
I'd love a "if ( key == KEY_ESCAPE and action == PRESS): w.setWindowShouldClose TRUE" option for now even if you withdrew it when you have seen your way to include a menu widget.
That sounds like a good idea! The keycode's are part of the Fidget layer, but adding the tab handling required modifying it. Adding an escape handler would be great and makes a good default. It make sense for widgets and menus too. Though, maybe making it a "double escape" at the top level?
Eventually I'd like to add a configuration panel where users could set these things. I'd like to provide basic Emacs / Vim style bindings too. Ahhh... eventually. It's starting to feel like it's progressing though. ;)