I'm still figuring out how Nim handles passing procs around, and from what I've read, and from testing, this works:
type
BaseButton* = ref object of Widget
# ...
action*:proc(text:string)
proc some_proc(text:string) =
echo text
method init_button*(b:BaseButton, id, x, y:int) {.base.} =
b.action = some_proc
# testing
b.action("callback working")
but maybe I'd like to keep a sequence of callbacks, in case the button should do more than one thing, and this was the only way I could make that part work:
type
Callback = proc(text:string)
BaseButton* = ref object of Widget
# ...
actions*:seq[Callback]
method init_button*(b:BaseButton, id, x, y:int) {.base.} =
b.actions.add(test)
# testing
b.actions[0]("callback working")
(alternatively I could use the first method and have two or three action vars, and thus have a limited set of actions per button, and that would be fine too)
The problem I'm having is that in either of those ways I still have to know in advance the signature of the callback, (text:string), but from all I know it could be anything -- it's supposed to be a generic button -- it could even need to know the (id:int) of the button being pressed. Since Nim isn't dynamically typed, I'm at a loss on how to get around this...
I don't know how this kind of thing is usually implemented in staticaly typed languages. On discord @Araq told me this is usually done using closures, but I'm not exactly seeing how that goes. (I'm only sort of familliar with closures in Lua.)
Here's how I did this in the Godot Engine, and basically it's what I'm trying to reproduce:
# BaseButton.gd
var actions = []
var args = []
# (...)
func add_action( action, arg=null ):
# 'action' is a funcref - reference to a function
# 'arg' can be a variant of any type, or an array of variants
actions.append( action )
args.append( arg ) # even if null
# when button gets pressed
func on_hit():
for i in range( actions.size() ):
if args[i] != null: actions[i].call_func( args[i] )
else: actions[i].call_func()
I don't know how this kind of thing is usually implemented in staticaly typed languages. On discord @Araq told me this is usually done using closures, but I'm not exactly seeing how that goes. (I'm only sort of familliar with closures in Lua.)
In Nim, a closure is a procedure which captures variables from outside of it's scope.
proc execute(f: proc()) =
f()
proc someproc =
let x = 2
execute(proc() = echo x) # 2
# we can also captures variables and then mutate them
var y = 4
execute(proc() = y *= 2)
echo y # 8
someproc()
We can use this to "wrap" the procs together with all the context they need into a closure. They are many ways to go from here. Just to give you an example, here I implemented something similar to your Godotscript example:
type
Callback = proc(text:string)
BaseButton* = ref object of Widget
# ...
actions*:seq[Callback]
method init_button*(b:BaseButton, id, x, y:int) {.base.} =
b.actions.add(test)
# testing
b.actions[0]("callback working")
proc add_action*[T](b: BaseButton, p: proc(arg: T, text: string), arg: T) =
b.actions.add(proc(text: string) = p(arg, text))
You might also want to leave the creation of the closure to the user so they can do what they want as long as the signature matches.
Thanks for the reply. I ended up doing it like this though (I don't quite understand that stuff with the T yet)
method add_action*(b:BaseButton, fn:Callback) {.base.} =
b.actions.add(fn)
method on_hit*(b:BaseButton) {.base.} =
for i in 0..<len(b.actions):
b.actions[i]()
# and then elsewhere in another module
proc test_proc(id:int) =
echo "callback is working - button: ", id
proc init() =
var button = new_button()
button.add_action( proc() = test_proc(button.id) )
So I can just pass in a proc that knows how to execute the intended proc. I suppose this way I can execute practically any function with any arguments.
Are making your on UI library or trying to integrate with GoDot?
If you are making your own UI lib I highly recommend not using OOP and using Immediate Mode UI.
I am also working on a UI library: https://github.com/treeform/fidget
This is how my code looks:
rectangle "inc":
box 920, 0, 40, 40
fill "#AEB5C0"
onHover:
fill "#FF4400"
onClick:
inc bar
if bar > 40: bar = 40
First we draw a rectangle with size and color. Then you have the onHover and onClick are event handlers but they don’t need any fancy callbacks, inheritance or message passing. In fact it’s just a template around an if statement:
template onClick*(inner: untyped) =
if mouse.click and mouse.pos.inside(current.screenBox):
inner
I think immediate mode is a much simpler UI paradigm that keeps UI code clean.
I'm trying to port a roguelike project I've been making in godot, where I've made my own UI so it deals with integer grid coords only (and mouse on grid coords too).
I've been meaning to try the Nim-Godot bindings, but haven't yet.
Thanks for the links. I'll take a look at it.