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 the ability to have circular references to Widgets (or at the very least Window) #115

Open PhilippMDoerner opened 8 months ago

PhilippMDoerner commented 8 months ago

For Features such as CaptureWidget on SearchEntry (as discussed here ) and I think others as well, it is mandatory to be able to have circular references.

Without those implemented, the full featureset of all GTK widgets can not be provided.

I'd like to have an issue about this simply so we can keep this in mind and have somewhere to track possible progress against etc.

Though honestly, how this could possibly work is entirely beyond me.

PhilippMDoerner commented 7 months ago

During my waiting times I'm looking into this and I'm wondering whether the first step for this ticket isn't just that we need the ability to query the gui tree we construct via the gui-dsl. Basically javascripts "getElementByTagName".

That way you could be able to fetch any Widget from a given gui tree. Imo that'd be step 1, step 2 is be able to check if a given Widget already has a corresponding GtkWidget and fetch that if it does.

Particularly the second step I have absolutely 0 idea how we could achieve that.

can-lehmann commented 7 months ago

React solves this issue using "refs": https://react.dev/learn/manipulating-the-dom-with-refs Something similar will work for owlkettle as well.

can-lehmann commented 7 months ago

The basic API might look like this:

let entry = Ref[EntryState]()
gui:
  Window:
    Box:
      Entry:
        reference = entry
      Button:
        proc clicked() =
          entry.moveCursor(0)
          entry.focus()
PhilippMDoerner commented 7 months ago

I never used react so I'm not fully familiar with the concept. From what I can understand, basically you create a ref first, pass that on to the widget and that "fills" the ref for you?

Edit: You were faster with the example :smile: . But yeah, given the code it's pretty much exactly that. Thanks for the explainer!

Given that BaseWidget is the easiest way to make this accessible for most widgets at once, that'd mean add a field "owlRef" or whatever to it and in the beforebuild hook, if hasOwlRef = true, then fill it with... good question. A ref to widgetState? Would make sense I guess since that nets you the entire widget + access to the associated GtkWidget

PhilippMDoerner commented 7 months ago

I tried to play around a bit with this (basically passing WidgetState into the Ref and not making it a generic). Question: Wouldn't a "Ref" approach like this need to sort of work like an observable in rxjs or the like? By that I mean that you should be able to register callbacks and they fire every time the value of the Ref changes.

I've come to this conclusion mostly because I very quickly run into scenarios with the StackSwitcher/StackSidebar things where I want to use what is inside the Ref, but it does not yet have a value assigned to it, because GtkWidgets/WidgetStates only get instantiated once you insert them.

can-lehmann commented 7 months ago

Even though I did not originally plan this, having it be observable sounds like a good solution to the StackSwitcher issue.

PhilippMDoerner commented 2 weeks ago

As a first approach for a solution we could create a type WidgetRef = ref GtkWidget or type WidgetRef = ref WidgetState and give BaseWidget a field widgetRef that will assign the constructed GtkWidget or WidgetState to that ref.

Then you can share that around and unref it for access.

It's a bit clunky and we can see how well we can disguise that, but its the only idea I can come up with (and likely one you already thought of, but I really couldn't come up with anything better). We could look into making that type observable-ish as well. I've already implemented an observable/subject pattern in https://github.com/minamorl/rex , though using a full implementation like that might be overkill, maybe we can get away with something a lot simpler, if at all.

I assume you'd want us not depend on the lib to reduce external dependencies which I'd agree with, given we don't plan on going all in with observable.

can-lehmann commented 2 weeks ago

I think you proposed the idea of making refs observable to allow them to be used in scenarios where a widget needs to be known by another widget that is not one of its ancestors in the widget tree (e.g. Stack Switcher). For this we do not need any reactive stream logic or combinators.

I think the a ref might end up looking like this:

type
  WidgetRef*[T] = ref object
    state*: T
    observers: HashSet[proc(state: T)]

where T is some WidgetState.

PhilippMDoerner commented 2 weeks ago

I don't think that's possible. The main reason being that you'd want to implement it roughly like this:

#widgetutils.nim
type
  StateRef*[T: WidgetState] = ref object
    state: T
    observers: HashSet[proc(state: T)]

proc hasRef*[T: WidgetState](stateRef: StateRef[T]): bool = not stateRef.state.isNil()

proc newRef*[T: WidgetState](): StateRef[T] = 
  StateRef[T](observers: initHashSet[proc(state: T)]())

proc unwrap*[T: WidgetState](stateRef: StateRef[T]): T =
  stateRef.state

proc setRef*[T: WidgetState](stateRef: StateRef[T], state: T) =
  stateRef.state = state
  for observer in stateRef.observers:
    observer(stateRef.state)

proc widget*[T: WidgetState](stateRef: StateRef[T]): Option[GtkWidget] = 
  when T is Renderable: # This doesn't work so far for some reason 
    some stateRef.state.internalWidget
  else:
    none(GtkWidget)

proc subscribe*[T: WidgetState](stateRef: StateRef[T], observer: proc(state: T)) =
  stateRef.observers.incl(observer)

proc unsubscribe*[T: WidgetState](stateRef: StateRef[T], observer: proc(state: T)) =
  stateRef.observers.excl(observer)

#widgets.nim
renderable Box of BaseWidget:
  ...
  stateRef: StateRef[BoxWidgetState]

  hooks:
    ...
    afterBuild:
      if not state.stateRef.isNil():
        state.stateRef.setRef(state)

Now this is a problem, BoxState doesn't exist yet as it has not yet been created so it can't be used in a type-declaration. Therefore it'd have to be Renderable so you can have access to at least GtkWidget, which is the key.

Edit: Or you generate this field inside the DSL, as well as the code for the afterBuild hook

Edit2: I think generating this as part of the DSL is the way to go after mulling it over a bit.