I've got what feels like a minimal but useable widget API around Fidget. While Fidget is fantastic for one off UI's it didn't really support pre-made widgets. So I added it. :)
There's support for both stateful and non-stateful widgets.
Stateful widgets are divided into either appWidget`s or `statefulWidget`s. App widgets have explicit state and are best used for encapsulating application state. Generally these will be near the top level, with the state object stored in a top level proc or as a global. Stateful widgets support all the other UI widgets like `dropdown which require some internal state like whether the dropdown is open or not. The main difference is that the internal state can be more transitory.
Here's a basic app widget demo:
import button
import progressBar
loadFont("IBM Plex Sans", "IBMPlexSans-Regular.ttf")
proc exampleApp*(
myName {.property: name.}: string,
) {.appWidget.} =
## defines a stateful app widget
##
## `exampleApp` will be transformed to also take the basic
## widget parameters: `self: ExampleApp = nil; setup: proc() = nil; post: proc() = nil`
properties:
## this creates a new ref object type name using the
## capitalized proc name which is `ExampleApp` in this example.
## This will be customizable in the future.
count: int
value: UnitRange
frame "main":
setTitle(fmt"Fidget Animated Progress Example - {myName}")
group "center":
box 50, 0, 100.Vw - 100, 100.Vh
orgBox 50, 0, 100.Vw, 100.Vw
# Use progress bar widget
self.value = (self.count.toFloat * 0.10) mod 1.0
progressBar(self.value) do:
# this is progress bar's setup proc -- used to set box size, override colors, etc
box 10.WPerc, 20, 80.WPerc, 2.Em
Horizontal:
# creates an horizontal spacing box
box 90.WPerc - 16.Em, 100, 8.Em, 2.Em
itemSpacing 0.Em
# Click to make the bar increase
# basic syntax just calling a proc
if button(fmt"Clicked1: {self.count:4d}"):
self.count.inc()
# Alternate format using `Widget` macro that enables
# a YAML like syntax using property labels
# (see parameters on `button` widget proc)
Widget button:
text: fmt"Clicked2: {self.count:4d}"
onClick: self.count.inc()
# current limit on Widget macros is that all args
# must be called as properties, no mix and match
#
# i.e. this doesn't work (yet):
# Widget button(fmt"Clicked2: {self.count:4d}"):
# onClick: self.count.inc()
Vertical:
# creates a vertical spacing box
box 10.WPerc, 160, 8.Em, 2.Em
itemSpacing 1.Em
Button:
# default alias of `Widget button`
# only created for non-stateful widgets
text: fmt"Clicked3: {self.count:4d}"
setup: size 8.Em, 2.Em
onClick: self.count.inc()
Widget button:
text: fmt"Clicked4: {self.count:4d}"
setup: size 8.Em, 2.Em
onClick: self.count.inc()
var state = ExampleApp(count: 0, value: 0.33)
const callform {.intdefine.} = 2
proc drawMain() =
frame "main":
when callform == 1:
# we call exampleApp with a pre-made state
# the `statefulWidget` always takes a `self` paramter
# that that widgets state reference
exampleApp("basic widgets", state)
elif callform == 2:
Widget exampleApp:
name: "basic widgets"
self: state
startFidget(drawMain, uiScale=2.0)
It's been edited down from the full version you can find at: https://github.com/elcritch/fidget/blob/devel/tests/widgets/basicwidget.nim
There's a demo for widgets with internal state. This is the trickiest part. It's possible to use standard proc's and have the end user manually create the state object for widgets, but that becomes tedious pretty quickly. I also wanted to avoid a more object oriented design.
The approach I've taken is to have widgets create ref object's that can be stored in the Fidget node tree. Alternatively, the ref object can be passed in as a parameter. This allows user control of the hidden state when desired.
See progress bar widget example here: https://github.com/elcritch/fidget/blob/160d00f6155a3aa0846e9d3e1259f157413c97b3/tests/widgets/dropdown.nim#L128
dropdown(dropItems, dropIndexes[0], dstate)
dropdown(dropItems, dropIndexes[1], nil)
text "desc":
size 100.WPerc, 1.Em
fill "#000d00"
characters "linked dropdowns: "
dropdown(dropItems, dropIndexes[2])
Widget dropdown:
items: dropItems
dropSelected: dropIndexes[2]
setup: box 0, 0, 12.Em, 2.Em
I'm currently looking for the right tool to implement a desktop app with. Fidget looked like a great option but it has a certain abandonware vibe around it.
However, this changes everything™. If there's a ready to use library of UI components for Fidget, it becomes more or less a production ready solution.
If there's anything I can help you with, feel free to delegate issues.
As the creator of Fidget, this look really cool! I fully support this development.
My goal was to allow people to create any kind of widget from Figma, but its cool that you are taking it in a different direction and providing ready made widgets.
Thank you!
Fidget does have abandonware vibe around it right now. Sorry! I have been trying to button down the foundations with pixie, boxy and windy.
However, this changes everything™. If there's a ready to use library of UI components for Fidget, it becomes more or less a production ready solution.
That's my thinking too! Fidget is really just missing widgets. There's been a bit of polishing needed in a couple of spots (like say input text offset) that have needed mostly one-line fixes. The core has been pretty solid and hackable =)
Also @treeform has done a fantastic job of some of the hard things like just getting a basic window with opengl running on all the major platforms. That's normally where these kind of projects break.
As a side note I feel like I'm having fun just writing software I've wanted for a long time but was too tedious in C/C++/Rust. Nim makes it so much easier to write systems-level things like GUI libraries. ;)
If there's anything I can help you with, feel free to delegate issues.
Awesome! I'll spend some time to re-organize the widgets into their own folder and try and get a list of basic widgets in a README. Looks like you're @moigagoo on GitHub as well. I'll tag you on a GitHub issue.
Aside from just a basic set of widget, there are a few outstanding things that'd be good for real "production grade" widgets. Things like refining how styles "cascade" and widget defaults. Other details like having the dropdown's go up when they're at the bottom of the window matter too (maybe z-index support is needed). It'd be fun to add a "theme" system too.
There are also interactions between widgets to consider, and more documentation about how the widget system works. Though what I wrote above goes a fair bit of the way towards that. I tried to keep it KISS for the widgets but also borrow from React, Qt, HTML, Wx, etc where it made sense.
Other helpful items would be some testing on Windows. I also want to follow up and use @treeform's unit testing to take screenshots. That'd be a must for a stable widget library.
As the creator of Fidget, this look really cool! I fully support this development.
Awesome!
My goal was to allow people to create any kind of widget from Figma, but its cool that you are taking it in a different direction and providing ready made widgets.
It is a bit of a different direction but I think those goals can overlap nicely. Fidget itself is simple and flexible, which makes writing re-usable widgets simpler too.
I also like the idea that it should be easy for others to make custom widgets and/or tweak the UI around the core widgets. I want to make it easy to tweak the "standard" widgets while handling some of the minutiae of writing a good UI.
Thank you!
You're welcome and thank you for all the work on Fidget and really Pixie/Boxy/Windy! I've never done OpenGL stuff so it's been awesome to build on your work.
Fidget does have abandonware vibe around it right now. Sorry! I have been trying to button down the foundations with pixie, boxy and windy.
When you get back to working on Fidget feel free to DM me or tag me on GitHub. It'd be great to discuss merging my fork at some point. I've changed enough it wouldn't be a simple merge now. I imagine some features/changes may require rework but I'd be up for it.
Maybe we can even get a whole Fidget and Fidgets ecosystem going. =)
GIFs for the curious
basicwidgets.nim
dropdown.nim
@ErikWDev That's awesome!
I've run into a problem trying to tie in animation actions. The general problem is really how to trigger user defined events associated with a widget and its state.
It'd be nice to not have to explicitly know or pass the widgets internal state. Something like this:
Widget progressBar:
value: self.value
setup: box 10.WPerc, 20, 80.WPerc, 2.Em
actionSlideToValue: triggerValue
... later in the current widget ...
if button("Click me"):
triggerValue(newVal) # this will cause progressBar to animate sliding to new value
The problem is how to bind the triggerValue name? It could be a variable already declared by the user, but they'd need to know the type, etc.
In some cases the action could just setting a boolean value that the sub-widget would use when rendering. Similar to how onClick can be use to highlights buttons.
Suggestions?
Notice that the dropdowns go up when too close to the bottom!
This demo shows the results of some tweaking I did yesterday to add z-index (ZLevel) support in OpenGL Fidgets. It's limited to only 4 layers due to the way it's implemented. Here's the layers:
ZLevel* = enum
## The z-index for widget interactions
ZLevelBottom
ZLevelDefault
ZLevelRaised
ZLevelOverlay
Essentially each z-index layer is rendered in order but only items visible on that layer are drawn, though layouts are fully calculated. I haven't run benchmarks to test the overhead, but it hasn't noticeably affected rendering speed. CPU usage hasn't noticeably changed either.
Furthermore mouse/keyboard interactions follow the ZLevel precedents where lower levels are overshadowed. This simplifies making the dropdown code above and avoids annoying "overlapping nodes both got a click". It also simplifies worrying about the order widgets are drawn, which makes it possible to use Horizontal and Vertical blocks without worrying.
generally it depends. if it's an immediate mode gui, then you pass those state variables every time you draw, and the internal system checks if drawing is needed.
Figet's implementation roughly follows that. However, I also tied it into Nim's async event system which enables nicer integration with out-of-band events.
What I'd like to figure out is a nice API avoid having to explicitly pass around explicit state variables. In larger UI's you often end up wanting/needing to send events into unrelated modules. Plumbing variables requires tons of extra arguments being passed around. Though that's basically what React Redux does.
Here's a draft API I've come up with after some tinkering:
# Trigger an animation on animatedProgress below
Widget button:
text: fmt"Clicked2: {self.count:4d}"
onClick:
self.count.inc()
trigger("pb1") <- gotoValue(self.count*0.1)
Widget animatedProgress:
signals:
"pbc1" = gotoValue(target: float32)
delta: 0.1'f32
setup: box 10.WPerc, 20, 80.WPerc, 2.Em
This would require a _little magic where the signals macro would need to register some hidden state for the current node's scope. Though the macro could check that the signal is valid for the target widget though.
The trigger("pb1") macro would activate the event by storing it in the hidden state and calling refresh. It'd require 1-2 refresh's (redraw in Fidget parlance) in the general case. What I'm unsure of implementation wise in this setup is how to avoid infinite refresh cycles.
Though thinking about this again, I'm wondering if a global event queue (Table?) might solve that. The struggle with that is keeping this event in the scope it's used in (e.g. we don't want every widget of a given type triggering). We also don't want widgets to care that the same name "pb1" was used in an unrelated widget.
ImGUI seems to handle this by requiring the programmer to set unique id's for widgets. Then it uses those id's to communicate out-of-band with widgets (e.g. to set their background color). It's easy to mess up and get weird interactions.
Though thinking about this again, I'm wondering if a global event queue (Table?) might solve that. The struggle with that is keeping this event in the scope it's used in (e.g. we don't want every widget of a given type triggering). We also don't want widgets to care that the same name "pb1" was used in an unrelated widget.
What about a fully qualified name -> screen_name.widget_name (and shortcuts when the widget is in the same screen)
What about a fully qualified name -> screen_name.widget_name (and shortcuts when the widget is in the same screen)
Not sure that you'd work too well given that the fully qualified names are based on the Node tree path. You need that because the name given in the code isn't unique and there can be lots of instances of a given Widget (aka Node). Unfortunately generating the full name is also somewhat expensive and not stable.
Got an initial event setup that seems to have potential. It looks to be scalable from a single widget to global or in between without too much work.
type
IncrementBar = object
target: float
proc animatedProgress*(delta: float32 = 0.1) {.statefulFidget.} =
properties:
value: float
self.value = self.value + delta
## handle events
var v {.inject.}: Variant
if not current.hookEvents.isNil and
current.hookEvents.pop(current.code, v):
variantMatch case v as evt
of IncrementBar:
echo "pbar event: ", evt.repr()
self.value = self.value + evt.target
refresh()
else:
echo "dont know what v is"
...
Then:
proc exampleApp*(myName {.property: name.}: string) {.appFidget.} =
## defines a stateful app widget
properties:
count1: int
count2: int
value: UnitRange
if current.hookEvents.isNil:
current.hookEvents = newTable[string, Variant]()
let currEvents = current.hookEvents
...
# Trigger an animation on animatedProgress below
Widget button:
text: fmt"Animate {self.count2:4d}"
onClick:
self.count2.inc()
currEvents["pbc1"] = newVariant(IncrementBar(target: 0.02))
Widget animatedProgress:
delta: delta
setup:
box 0'em, 0'em, 14'em, 2.Em
current.code = "pbc1"
current.hookEvents = currEvents
Widget button:
text: fmt"Animate2 {self.count2:4d}"
onClick:
self.count2.inc()
currEvents["pbc1"] = newVariant(IncrementBar(target: 0.02))
Got an initial event API setup for Fidgets. It uses Patty to create ADT's so Fidgets can statically check their event inputs. Here's the results using an async hook:
Here's an example usage for defining the events:
proc animatedProgress*(
delta: float32 = 0.1,
): AnimatedProgress {.statefulFidget.} =
init:
box 0, 0, 100.WPerc, 2.Em
properties:
value: float
cancelTicks: bool
ticks: Future[void] = emptyFuture()
events(AnimatedEvents):
IncrementBar(increment: float)
JumpToValue(target: float)
CancelJump
onEvents:
IncrementBar(increment):
self.value = self.value + increment
refresh()
Then events can be used in other Fidgets:
proc exampleApp*(myName {.property: name.}: string) {.appFidget.} =
render:
let currEvents = useEvents() # bind local events name, mimics React hooks
Widget button:
text: fmt"Animate"
onClick:
self.count2.inc()
currEvents["pbc1"] = JumpToValue(target = 0.02) # trigger event
let ap1 = Widget animatedProgress:
setup:
bindEvents "pbc1", currEvents
box 0'em, 0'em, 14'em, 2.Em
text "data":
size 90'vw, 2'em
fill "#000000"
characters: "AnimatedProgress value: " & repr(ap1.value)