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 `adw.SpinRow` widget #88

Open can-lehmann opened 9 months ago

can-lehmann commented 9 months ago

https://gnome.pages.gitlab.gnome.org/libadwaita/doc/main/class.SpinRow.html

PhilippMDoerner commented 9 months ago

Started tinkering around with this one. I'm getting Segfaults when the input is handled. That is explicitly within inputEventCallback due to nil access when casting it to SpinRowState.

  renderable SpinRow of ActionRow:
    climbRate: float = 0.0
    digits: uint = 0
    numeric: bool = false
    min: float = 0.0
    max: float = 100.0
    snapToTicks: bool = false
    updatePolicy: SpinButtonUpdatePolicy = SpinButtonUpdateAlways
    value: float = 0.0
    wrap: bool = false

    proc input(newValue: float)

    hooks:
      beforeBuild:
        let climbRate: float = if widget.hasClimbRate: widget.valClimbRate else: 0.0
        let digits: uint = if widget.hasDigits: widget.valDigits else: 0.uint
        state.internalWidget = adw_spin_row_new(GtkAdjustment(nil), climbRate.cdouble, digits.cuint)

      connectEvents:
        proc inputEventCallback(
          widget: GtkWidget,
          newValue: cdouble,
          data: ptr EventObj[proc(newValue: float)]
        ): cint {.cdecl.} =
          let x = data[].widget
          echo data == nil # false
          echo data[].widget == nil # false
          echo SpinRowState(data[].widget) == nil ## Line 740, this line segfaults according to the stacktrace
          SpinRowState(data[].widget).value = newValue.float
          data[].callback(newValue)
          data[].redraw()

          result = true.cint

        state.connect(state.input, "input", inputEventCallback)

  ... tons of property hooks that all seem to work fine even though I don't quite get what some of the properties do...

stacktrace:

false
false
Traceback (most recent call last)
/home/philipp/dev/owlkettle/examples/widgets/adw/spin_row.nim(63) spin_row
/home/philipp/dev/owlkettle/owlkettle/adw.nim(912) brew
/home/philipp/dev/owlkettle/owlkettle/mainloop.nim(96) runMainloop
/home/philipp/dev/owlkettle/owlkettle/adw.nim(740) inputEventCallback
/home/philipp/.choosenim/toolchains/nim-2.0.0/lib/system/arc.nim(238) isObjDisplayCheck
SIGSEGV: Illegal storage access. (Attempt to read from nil?)

I don't quite see why this blows up on me. I'm trying to cross reference with stuff from my Scale Example and Spinbutton but nothing really jumps out to me.

Honestly I don't even quite get the GTK Explanation of what that signal is good for:

Emitted to convert the user’s input into a double value.

Like what? The main reason I wrap this because I assume this is what gets called if you change the value in general on this widget.

Any idea what I could look into to try and solve this? I'm not even sure where to look next as the fact that it only blows up inside of inputEventCallback tells me that inputEventCallback itself is perfectly fine, just somehow the casting to SpinRowState kills me despite the widget on data not being nil.

I'm a bit flummoxed.

(Edit: On an unrelated sidenote: That PR to be able to deal with different adwaita versions paid of so fast it made my head spin. Literally merged the PR and the next issue I stare at requires me to check for Adwaita version 1.4 :laughing: )

can-lehmann commented 9 months ago

One thing I noticed is that your signal handler takes a newValue: cdouble, but GTK passes gdouble*.

can-lehmann commented 9 months ago

The idea behind input is that you can use a custom parsing function. (idk, maybe parse inputs with units like 10cm to 0.1m)

PhilippMDoerner commented 9 months ago

One thing I noticed is that your signal handler takes a newValue: cdouble, but GTK passes gdouble*.

is that not the same? Wait does that star make this a ptr cdouble?

PhilippMDoerner commented 9 months ago

I officially do not know where the actual value I type into the field comes from or is supposed to come from. It's not newValue because even unref'd whatever is in there is nowhere close whatever I type into the spinButton. It's neither adw_spin_row_get_value(widget).float because somehow that always returns 0.

If I type in 90 into that value field, it'll do a lot but not return me 90 anywhere. There's also some really weird behaviour in that it sometimes defaults back to 0 and sometimes it sticks with the value, which I assume is because the event-callback isn't getting called because it's somehow bricked for a reason I can't discern.

PhilippMDoerner commented 9 months ago

Hand --> Face

Seriously? The spinbutton wants you to use gtk_editable_get_text to fetch the userinput as text. Okay I tend to just not understand gtk sometimes, this should do it. I know that's what was written directly in the docs, it just seemed so out of pocket that I assumed it was for something else entirely and discarded that sentence.

PhilippMDoerner commented 9 months ago

Yeah I'm as stuck as I was yesterday. I updated the example on the branch a bit as well as the widget, but basically I can't get this to work properly.

Any value you input does not stick. A change to the value for some reason does not trigger the property hook that would update the gtk widget. Even if you update the gtk-widget inside of the custom eventCallback the value doesn't stick. If you update a value 3-5 times it somehow crashes the widget because suddenly SpinRowState(data[].widget) becomes an invalid object conversion. Updating the newValueHolder: ptr cdouble parameter doesn't do anything anywhere that I can see.

I'm pretty much stuck. I guess the nice thing is that either way I already did most of the typing work ?

Below the code on the widget to provide cursory glances:

when AdwVersion >= (1, 4):
  renderable SpinRow of ActionRow:
    climbRate: float = 0.0
    digits: uint = 0
    numeric: bool = false
    min: float = 0.0
    max: float = 100.0
    snapToTicks: bool = false
    updatePolicy: SpinButtonUpdatePolicy = SpinButtonUpdateAlways
    value: float = 0.0
    wrap: bool = false
    parseToFloat: (proc(input: string): float) = parseFloat

    proc input(newValue: float)

    hooks:
      beforeBuild:
        let climbRate: float = if widget.hasClimbRate: widget.valClimbRate else: 0.0
        let digits: uint = if widget.hasDigits: widget.valDigits else: 0.uint
        state.internalWidget = adw_spin_row_new(GtkAdjustment(nil), climbRate.cdouble, digits.cuint)

      connectEvents:
        proc inputEventCallback(
          widget: GtkWidget,
          newValueHolder: ptr cdouble,
          data: ptr EventObj[proc(newValue: float)]
        ): cint {.cdecl.} =          
          var newValue: float
          try:
            newValue = SpinRowState(data[].widget).parseToFloat($gtk_editable_get_text(widget))
          except ValueError as e:
            return GtkInputError

          echo "Input event callback: ", newValue
          echo "\n"

          newValueHolder[] = newValue.cdouble
          SpinRowState(data[].widget).value = newValue
          data[].callback(newValue)
          data[].redraw()

          return true.cint

        state.connect(state.input, "input", inputEventCallback) 
        # state.connect(state.output, "output", eventCallback)
        # state.connect(state.wrapped, "wrapped", eventCallback)

    hooks climbRate:
      property:
        adw_spin_row_set_climb_rate(state.internalWidget, state.climbRate.cdouble)

    hooks digits:
      property:
        adw_spin_row_set_digits(state.internalWidget, state.digits.cuint)

    hooks numeric:
      property:
        adw_spin_row_set_numeric(state.internalWidget, state.numeric.cbool)

    hooks min:
      property:
        adw_spin_row_set_range(state.internalWidget, state.min.cdouble, state.max.cdouble)

    hooks max:
      property:
        adw_spin_row_set_range(state.internalWidget, state.min.cdouble, state.max.cdouble)

    hooks snapToTicks:
      property:
        adw_spin_row_set_snap_to_ticks(state.internalWidget, state.snapToTicks.cbool)

    hooks updatePolicy:
      property:
        adw_spin_row_set_update_policy(state.internalWidget, state.updatePolicy)

    hooks value:
      property:  
        echo "Value was updated to: ", state.value
        adw_spin_row_set_value(state.internalWidget, state.value.cdouble)

    hooks wrap:
      property:
        adw_spin_row_set_wrap(state.internalWidget, state.wrap.cbool)

  export SpinRow
can-lehmann commented 9 months ago

A change to the value for some reason does not trigger the property hook that would update the gtk widget.

You currently don't have a changed/valueChanged/... callback (not sure what it is called for SpinRow), that makes a 2-way binding possible. input should only be used for parsing as far as I know.

PhilippMDoerner commented 9 months ago

A change to the value for some reason does not trigger the property hook that would update the gtk widget.

You currently don't have a changed/valueChanged/... callback (not sure what it is called for SpinRow), that makes a 2-way binding possible. input should only be used for parsing as far as I know.

Tried that just now by trying to start off simple and only doing changed. You still have to fetch the value via parseFloat($gtk_editable_get_text(widget)) because proc adw_spin_row_get_value(self: GtkWidget): cdouble nets you the old value 100% of the time after.

So with this approach, I have a widget. However that widget has the following bugs:

Here the ObjectConversionStackTrace echo'd out:

ref Exception(parent: nil, name: "ObjectConversionDefect", msg: "invalid object conversion", trace: 
@[
StackTraceEntry(procname: "spin_row", line: 61, filename: "/home/philipp/dev/owlkettle/examples/widgets/adw/spin_row.nim"), 
StackTraceEntry(procname: "brew", line: 920, filename: "/home/philipp/dev/owlkettle/owlkettle/adw.nim"), 
StackTraceEntry(procname: "runMainloop", line: 96, filename: "/home/philipp/dev/owlkettle/owlkettle/mainloop.nim"), 
StackTraceEntry(procname: "changedCallback", line: 748, filename: "/home/philipp/dev/owlkettle/owlkettle/adw.nim"), 
StackTraceEntry(procname: "sysFatal", line: 53, filename: "/home/philipp/.choosenim/toolchains/nim-2.0.0/lib/system/fatal.nim")
], 
up: nil)

I just can't explain why 0 somehow is a value that leads to ObjectConversionDefects. Nor why catching those exception still enables Segfaults elsewhere.

Below the current still completely busted approach:

when AdwVersion >= (1, 4):
  renderable SpinRow of ActionRow:
    climbRate: float = 0.0
    digits: uint = 0
    numeric: bool = false
    min: float = 0.0
    max: float = 100.0
    snapToTicks: bool = false
    updatePolicy: SpinButtonUpdatePolicy = SpinButtonUpdateAlways
    value: float = 0
    wrap: bool = false

    proc changed(newValue: float)

    hooks:
      beforeBuild:
        let climbRate: float = if widget.hasClimbRate: widget.valClimbRate else: 0.0
        let digits: uint = if widget.hasDigits: widget.valDigits else: 0.uint
        state.internalWidget = adw_spin_row_new(GtkAdjustment(nil), climbRate.cdouble, digits.cuint)

      connectEvents:        
        proc changedCallback(widget: GtkWidget, data: ptr EventObj[proc (newValue: float)]) {.cdecl.} =
          echo "Changed Signal"
          var newValue: float
          try:
            let valueString = $gtk_editable_get_text(widget)
            echo "The value: ", valueString
            newValue = parseFloat(valueString)
          except Exception as e:
            echo "Borked: ", e.repr
            newValue = 0.0

          echo data.isNil
          echo data[].widget.isNil
          SpinRowState(data[].widget).value = newValue

          data[].callback(newValue)
          data[].redraw()

        state.connect(state.changed, "changed", changedCallback)

        # state.connect(state.output, "output", eventCallback)
        # state.connect(state.wrapped, "wrapped", eventCallback)

    hooks climbRate:
      property:
        adw_spin_row_set_climb_rate(state.internalWidget, state.climbRate.cdouble)

    hooks digits:
      property:
        adw_spin_row_set_digits(state.internalWidget, state.digits.cuint)

    hooks numeric:
      property:
        adw_spin_row_set_numeric(state.internalWidget, state.numeric.cbool)

    hooks min:
      property:
        adw_spin_row_set_range(state.internalWidget, state.min.cdouble, state.max.cdouble)

    hooks max:
      property:
        adw_spin_row_set_range(state.internalWidget, state.min.cdouble, state.max.cdouble)

    hooks snapToTicks:
      property:
        adw_spin_row_set_snap_to_ticks(state.internalWidget, state.snapToTicks.cbool)

    hooks updatePolicy:
      property:
        adw_spin_row_set_update_policy(state.internalWidget, state.updatePolicy)

    hooks value:
      property:  
        echo "Value was updated to: ", state.value
        adw_spin_row_set_value(state.internalWidget, state.value.cdouble)

    hooks wrap:
      property:
        adw_spin_row_set_wrap(state.internalWidget, state.wrap.cbool)

  export SpinRow