adwaita-swift icon indicating copy to clipboard operation
adwaita-swift copied to clipboard

Support inherited signals

Open lambdaclan opened this issue 1 year ago • 7 comments
trafficstars

Is your feature request related to a problem? Please describe.

I would like to use the changed signal with the EntryRow widget.

Describe the solution you'd like

Most widgets seem to only support some of the available signals available to them. It would be nice to add support for more signals that are inherited from their ancestors and interface implementations. For my specific use case I would like to use the changed signal on the Adw.EntryRow that comes from GtkEditable. On closer inspection, seems like there are even more signals available provided by GtkEditable such as insert-text and delete-text which also could be useful.

image

image

I am not sure how much work is required to add support for more signals especially for all the widgets but something to consider going forwards!

Thank you again for all your work on this wonderful library ;)

PS1: I tried the newly added custom CSS support and is working great!

PS2: I am still relatively new to Swift, but eventually I would like to help out by contributing to your library. Maybe when we have some simple good first issue type of requests I will dive in ;)

Describe alternatives you've considered

I tried experimenting with the currently available signals for EntryRow such as onSubmit and the more generic onUpdate but unfortunately none of them provide the desired behaviour.

Basically I would like to be able to respond to changes to the entry field as the user is interacting with the widget such as inserting or deleting text.

Additional context

I am not sure if it is related in any way, but my goal is to call an async function when the signal is emitted. As mentioned above, I tried experimenting with the onUpdate signal and while I had some success the program eventually crashes. From my debugging, I believe the onUpdate signal is called is much more rapidly than I actually need it to and combining it with async leads to all kinds of issues.

lambdaclan avatar Jun 01 '24 04:06 lambdaclan

Thanks for opening the request. Cool that you're interested in contributing!

The support for inherited signals is definitely something sensible to add. I'll work on this.

In your specific case, you can also work with the text binding instead of explicitly calling the signal as the text is updated when the changed signal is emitted. The most elegant way might be using binding's onSet:

struct TestView: View {

    @State private var text = ""

    var view: Body {
        EntryRow("Title", text: $text.onSet { print($0) })
    }

}

About the async problem: If I understand you correctly, you're updating the UI from an asynchronous context. For this, you should wrap the call updating the UI (e.g. an assignment to a state variable) with Idle:

Task {
    // Something asynchronous
    let result = try await someAsyncFunction()
    Idle {
        // Update the UI
        self.result = result
    }
}

david-swift avatar Jun 01 '24 09:06 david-swift

Hello David thank you very much for the detailed response.

I was not aware of the onSet functionality. After adjusting my entry rows to use that callback I was able to achieve the desired effect including the async part, so now all is working great!

I guess this onSet function should work with any widget that uses a binding of some form so it might be a good idea to make it a bit easier to discover. The current documentation does not mention this callback (in the widget pages), and you must specifically search about bindings to find it.

One easy way might be to add an example in the Demo app. Maybe a more general SignalDemo by showing how we can react to various widget events.

I will be happy to help out with this so if you think it makes sense just let me know!

Thank you again for your work and support.

lambdaclan avatar Jun 03 '24 06:06 lambdaclan

I was able to achieve the desired effect including the async part, so now all is working great!

Did you use Idle for this? It seems that often, it works also without it, but then it crashes sometimes randomly, so I would really wrap it with Idle.

I will be happy to help out with this so if you think it makes sense just let me know!

I think in general some articles should be added to the docs as well, covering useful shortcuts such as onSet, how to correctly update from asynchronous contexts, etc. A page in the demo app would also be cool!

david-swift avatar Jun 03 '24 08:06 david-swift

Hello David,

Did you use Idle for this? It seems that often, it works also without it, but then it crashes sometimes randomly, so I would really wrap it with Idle.

I tried both approaches, and although I could not actually replicate any of the previous crashes I did end up using Idle just to be safe. I did some reading up on the main GTK docs about the main event loop, and using Idle seems like a reasonable thing to do.

I think in general some articles should be added to the docs as well, covering useful shortcuts such as onSet, how to correctly update from asynchronous contexts, etc. A page in the demo app would also be cool!

OK cool, I will see what I can do! I will work on this and ping you in a PR.

lambdaclan avatar Jun 10 '24 02:06 lambdaclan

I have another question regarding onSet. I am not sure if it is a limitation of the current implementation or if I am doing something wrong but seems like @Binding variables do not get updated in the context of the onSet function. Is that the case or?

I will give a brief example to help you understand.

  • I have a view B with an EntryField that uses the text binding onSet
  • The view has a @Binding for a bool, let us just call it isValid
  • The isValid variable is set as @State under a different view A and is set to use a folder for storing its state
  • The application preferences allow updating the value of isValid through a toggle

Now, when I load view B as a debug method I am printing the value of isValid and that is correct based on the JSON value from the state folder param. Next, If open the preferences dialog and update its value, upon exiting (preferences window) the view B correctly updates the value automatically.

The problem is the EntryField's text on set:

View A

@State("isValid", forceUpdates:True)
private var isValid = false
.....

on button press load View B and pass isValid

View B

struct TestView: View {

    @Binding 
    var isValid
    @State 
    private var text = ""

    var view: Body {
        Text(String(isValid)) --> correctly displays the value even after preferences window updates it
        EntryRow("Title", text: $text.onSet { print(String(isValid)) }) --> displays the initial value correctly but updating via preferences still displays the old value
    }

}

Seems like some sort of window refresh is needed for the new value to get picked up. Again as a debug test, I added a sort option to move widgets around, doing so refreshes the window after which if I try the EntryField the correct value is displayed.

Sorry for the long and convoluted example, but it is a bit difficult to explain.

Essentially, what I want to know is if there is a way to update @Binding variables within the onSet function or maybe a way to force refresh a window or something.

Thank you for your help in advance!

lambdaclan avatar Jun 11 '24 09:06 lambdaclan

Very strange problem! Thanks for reporting!

seems like @Binding variables do not get updated in the context of the onSet function.

I'm not able to reproduce this using a simple example. It should work because the onSet closure is reloaded in each update. The example I tried (I modified the counter demo):

private struct CountButton: View {

    @Binding var count: Int
    @State private var test = ""
    var icon: Icon.DefaultIcon
    var action: (inout Int) -> Void

    var view: Body {
        Button(icon: .default(icon: icon)) {
            action(&count)
        }
        .circular()
        EntryRow("Test", text: $test.onSet { _ in print(count) })
    }
}

when I load view B as a debug method

Unfortunately, I don't understand what you mean by loading a view. This might be important for solving the problem, as it did work for me in a "normal" context.

david-swift avatar Jun 11 '24 11:06 david-swift

Hello David.

Thank you very much for following up.

seems like @Binding variables do not get updated in the context of the onSet function.

I'm not able to reproduce this using a simple example. It should work because the onSet closure is reloaded in each update.

Indeed, you are correct. After some more testing I verified that @Binding variables do get updated in the onSet closure but unfortunately my issue still persists. I will need to do some more testing, but I believe it might have something to do with the number of times the property is being passed to child views. I will try to offer some more context just in case you see something that might be the cause.

  • The data used in the onSet closure is part of a complex object Struct so I am accessing it as follows: $complex.isValid
  • The data is initialised in View A and is passed to multiple children (View A(init)->View B ->View C->View D(EntryField)) before reaching the view that includes the EntryField.
  • Testing results with EntryField's onSet update
    • Updating and displaying a @State variable both simple and complex(struct) declared in View D --> WORKS
    • Updating and displaying a @Binding variable both simple and complex(struct) declared in View D --> WORKS
    • Updating and displaying a @State variable both simple and complex(struct) declared in View C passed as a @Binding in View D --> WORKS
    • Recreating the problematic complex object in View C and passing that fresh copy to View D as a @Binding --> WORKS
      • so instead of A->B->C->D is just C->D using the exact same object struct
    • Recreating the problematic complex object in View B and passing that fresh copy to View D as a @Binding --> WORKS
      • so instead of A->B->C->D is just B->C->D using the exact same object struct
    • Passing complex object from A to D (4 times) -> DOES NOT WORK
  • One more thing that I noticed is that unless you explicitly use forceUpdates: true on @State properties that are deeply shared (maybe >3 times?) fail to update in all places not only in onSet closures.

when I load view B as a debug method

Unfortunately, I don't understand what you mean by loading a view. This might be important for solving the problem, as it did work for me in a "normal" context.

Apologies for the confusion, what I meant by load view B is basically passing the property to a child view. In our example above we just used 2 views but in my actual app as described here is passing the property 4 times.

I will try to recreate the issue in a sample app of sorts and maybe provide a small demo so that we can look into it.

Thank you once more for your time!

lambdaclan avatar Jun 12 '24 03:06 lambdaclan

This issue has moved to https://git.aparoksha.dev/aparoksha/adwaita-swift/issues/33

david-swift avatar Oct 17 '24 19:10 david-swift