can-lehmann / owlkettle

A declarative user interface framework based on GTK 4
https://can-lehmann.github.io/owlkettle/README
MIT License
367 stars 14 forks source link

Add a "Form-Generating"-feature to Owlkettle #93

Closed PhilippMDoerner closed 8 months ago

PhilippMDoerner commented 9 months ago

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 and viewed 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 a static: 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

PhilippMDoerner commented 9 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.

PhilippMDoerner commented 9 months ago

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.

PhilippMDoerner commented 9 months ago

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.

can-lehmann commented 8 months ago

Closed by https://github.com/can-lehmann/owlkettle/commit/eba600b841d3fe66dd2424f6cca4323e821cccb9