Support inherited signals #33

Open
opened 2024-06-01 06:12:05 +02:00 by lambdaclan · 10 comments
lambdaclan commented 2024-06-01 06:12:05 +02:00 (Migrated from github.com)

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.

### Is your feature request related to a problem? Please describe. I would like to use the `changed` signal with the [EntryRow](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/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`](https://docs.gtk.org/gtk4/signal.Editable.changed.html) signal on the `Adw.EntryRow` that comes from [`GtkEditable`](https://docs.gtk.org/gtk4/iface.Editable.html). 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](https://github.com/AparokshaUI/adwaita-swift/assets/47409392/a39eba52-d678-42c8-8472-b614cab749bf) ![image](https://github.com/AparokshaUI/adwaita-swift/assets/47409392/0ba5ff02-6885-4c38-8b29-53a400f47f14) 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`](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/entryrow/onsubmit(_:)) and the more generic [`onUpdate`](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/entryrow/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.
david-swift commented 2024-06-01 11:43:24 +02:00 (Migrated from github.com)

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
    }
}
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](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/entryrow/init(_:text:)) 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`](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/binding/onset(_:)): ```swift 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`](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/idle): ```swift Task { // Something asynchronous let result = try await someAsyncFunction() Idle { // Update the UI self.result = result } } ```
lambdaclan commented 2024-06-03 08:04:56 +02:00 (Migrated from github.com)

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.

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.
david-swift commented 2024-06-03 10:10:33 +02:00 (Migrated from github.com)

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!

> 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](https://github.com/AparokshaUI/Adwaita.docc) 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!
lambdaclan commented 2024-06-10 04:38:54 +02:00 (Migrated from github.com)

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.

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](https://docs.gtk.org/glib/main-loop.html#the-main-event-loop) 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](https://github.com/AparokshaUI/Adwaita.docc) 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 commented 2024-06-11 11:05:46 +02:00 (Migrated from github.com)

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!

I have another question regarding [onSet](https://aparokshaui.github.io/adwaita-swift/documentation/adwaita/binding/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 ```swift @State("isValid", forceUpdates:True) private var isValid = false ..... on button press load View B and pass isValid ``` View B ```swift 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!
david-swift commented 2024-06-11 13:39:00 +02:00 (Migrated from github.com)

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.

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): ```swift 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.
lambdaclan commented 2024-06-12 05:32:04 +02:00 (Migrated from github.com)

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!

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!
david-swift added the
enhancement
label 2024-10-07 12:15:40 +02:00
Contributor

Are there any plans on implementing signals? They're a fundamental part of the Adwaita (and by extension Gnome) ecosystem, and a lot of things are outright impossible through property bindings. (For example map and state_flags_changed among many others). Many of these signals are essential to apps that involves more logic than pressing buttons.

I realise this is a big ask, but I'd thought I'd ask before starting a sizeable project with the library and running into missing essential features :D

Otherwise, the underlying API seems fantastic, excellent work! Thank you!

Are there any plans on implementing signals? They're a fundamental part of the Adwaita (and by extension Gnome) ecosystem, and a lot of things are outright impossible through property bindings. (For example [`map`](https://docs.gtk.org/gtk4/signal.Widget.map.html) and [`state_flags_changed`](https://docs.gtk.org/gtk4/signal.Widget.state-flags-changed.html) among many others). Many of these signals are essential to apps that involves more logic than pressing buttons. I realise this is a big ask, but I'd thought I'd ask before starting a sizeable project with the library and running into missing essential features :D Otherwise, the underlying API seems fantastic, excellent work! Thank you!

@Sylphrena There is support for signals. For example, take a look at the auto-generated implementation of the toggled signal on toggle buttons:

ToggleButton("Test", isOn: $isOn)
    .toggled {
        print("Clicked!")
    }

That said, the current autogeneration script doesn't handle GLib's inheritance system very well. As a result, signals like Gtk.Widget:map aren’t available, since they're defined on Gtk.Widget rather than on each individual widget.

Improving support for GLib’s inheritance system is definitely on my radar, but it’s not a small task. I’ll look into it, as it would be a significant improvement, but I can’t give any concrete timeline just yet.

Thanks again for the kind words! If you need support for specific signals in the meantime, feel free to let me know - I can add them manually until the generation process is more robust.

@Sylphrena There is support for signals. For example, take a look at the auto-generated implementation of the [`toggled` signal on toggle buttons](https://docs.gtk.org/gtk4/signal.ToggleButton.toggled.html): ```swift ToggleButton("Test", isOn: $isOn) .toggled { print("Clicked!") } ``` That said, the current autogeneration script doesn't handle GLib's inheritance system very well. As a result, signals like `Gtk.Widget:map` aren’t available, since they're defined on `Gtk.Widget` rather than on each individual widget. Improving support for GLib’s inheritance system is definitely on my radar, but it’s not a small task. I’ll look into it, as it would be a significant improvement, but I can’t give any concrete timeline just yet. Thanks again for the kind words! If you need support for specific signals in the meantime, feel free to let me know - I can add them manually until the generation process is more robust.
Contributor

Wonderful!

I'll give this library a whirl then and report back!

Once again, thank you for your work!

Wonderful! I'll give this library a whirl then and report back! Once again, thank you for your work!
Sign in to join this conversation.
No Milestone
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: aparoksha/adwaita-swift#33
No description provided.