Closed PhilippMDoerner closed 8 months ago
I've got it to work! Mostly with the approach you had in mind I think:
import owlkettle
import owlkettle/[dataentries, adw, widgetutils]
import std/[options, json, macros, strutils]
type A = object
name: string
viewable App:
value: float64 = 100.0
orient: Orient = OrientX
inverted: bool = false
bla: A = A(name: "lala")
text: string = "Something"
macro getField*(someType: untyped, fieldName: static string): untyped =
nnkDotExpr.newTree(someType, ident(fieldName))
proc toFormField(state: auto, fieldName: static string, typ: typedesc[SomeFloat]): Widget =
return gui:
NumberEntry(value = state.getField(fieldName)):
proc changed(value: float) =
state.getField(fieldName) = value
proc toFormField(state: auto, fieldName: static string, typ: typedesc[SomeInteger]): Widget =
return gui:
NumberEntry(value = state.getField(fieldName).float):
proc changed(value: float) =
state.getField(fieldName) = value.int
proc toFormField(state: auto, fieldName: static string, typ: typedesc[string]): Widget =
return gui:
Entry(text = state.getField(fieldName)):
proc changed(text: string) =
state.getField(fieldName) = text
proc toFormField(state: auto, fieldName: static string, typ: typedesc[bool]): Widget =
return gui:
Switch(state = state.getField(fieldName)):
proc changed(newVal: bool) =
state.getField(fieldName) = newVal
proc toFormField(state: auto, fieldName: static string, typ: typedesc[object]): Widget =
return gui:
Entry(text = $ %*state.getField(fieldName)):
proc changed(text: string) =
try:
state.getField(fieldName) = text.parseJson().to(typ)
except Exception: discard
proc toFormField(state: auto, fieldName: static string, typ: typedesc[enum]): Widget =
return gui:
Entry(text = $state.getField(fieldName)):
proc changed(text: string) =
try:
state.getField(fieldName) = parseEnum[typ](text)
except Exception: discard
method view(app: AppState): Widget =
var fieldWidgets: seq[tuple[name: string, field: AlignedChild[Widget]]] = @[]
for name, value in app[].fieldPairs:
when name notin ["app", "viewed"]:
let fieldWidget = app.toFormField(name, value.type)
if fieldWidget != nil:
let alignedWidget = AlignedChild[Widget](widget: fieldWidget)
fieldWidgets.add((name, alignedWidget))
result = gui:
Window:
Box(orient = OrientY, spacing = 6, margin = 12):
for field in fieldWidgets:
ActionRow {.expand: false.}:
margin = 6
title = field.name
suffixes = @[field.field]
owlkettle.brew(gui(App()))
Sadly couldn't figure out how to get past the need for the third field as "discriminator" between the various toFormField
procs.
I decided to create a BoxWidget via a generic proc and use insert
to insert it into the example gui-tree.
The main reason for me not making a viewable instead was honestly that I've never seen a generic viewable before and felt that would be too much hassle to troubleshoot for something that is mostly anyway a QoL addition. Particularly since a generic viewable likely would also require a generic viewable method and IIRC generic methods are fundamentally broken, making it doubly not worth the effort to try and go in that direction.
Also given that I have no clue where to put it, I put it into its own file for now. I'll open a PR for this once the Scale PR is merged as I directly depend on that example for this ticket and for my testing.
Sidenote, this issue will encompass 2 PRs that I'll open one after another (because 1) depends on the Scale PR and 2) depends on 1)): 1) Add the feature of autoform in general (this branch) 2) Add Autoform to all examples under examples/widgets (this branch)
That is in order to keep the individual PRs smaller, as I thought you might find that preferable over one larger PR.
Also I opened them as Draft PRs already since I have time right now and this way I can basically do everything I can right now, as particularly writing PR descriptions takes me some time typically that I might not have later.
The usecase
For all the example widgets I would like to have a generic proc or template or so that I can just hand any kind of
<WidgetName>State
and that generates me a Form where I have DataEntry fields for every field on said state (except for app and viewed as those are owlkettle internal I think?).This would be useful to let the user see what happens under various circumstances if they manipulate the state of a widget as they please.
It could also be useful if users want to generate a form to manipulate data on the fly without having to write it by hand.
The concept of a solution
Conceptually, I'd have assumed any approach would simply need to iterate over the fields of
<WidgetName>State
type, transform the values of the fields into an enum-type and then use fieldName and the enum-type to associate the field on<WidgetName>State
with a DataEntry widget. Which DataEntry widget is chosen is determined by the enum-type.This normally wouldn't be toooooo hard without but the combination of any State type having
app
andviewed
fields that somehow cause compiler crashes when accessed by the loop as well as the guidsl tolerating neither when statements (which would allow me to avoid those) nor macros that enable unrolling astatic: seq[string]
for-loop in the gui section makes all my typical non-macro approaches impossible.Prior approach (Placeholder for like 3 other similar ones I all attempted)
Removed, it is irrelevant as I found a solution