Repository: compose4gtk/compose-4-gtk
Pull request #29: Add Grouping to CheckButton with an example
Author: login: Calvicii
Created: 2025-05-08T01:17:12Z | Merged: 2025-05-17T11:26:32Z

I don't really like the fact that we have to create a org.gnome.gtk.CheckButton in order to group them. If you have a better idea please let me know, but for now I think this works.


https://github.com/user-attachments/assets/1fda5187-76cc-4c36-a389-caafd2f88efc




Diff:
diff --git a/src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt b/src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
index 581c590..82f3220 100644
--- a/src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
+++ b/src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
@@ -1,16 +1,59 @@
 package io.github.compose4gtk.gtk.components
 
 import androidx.compose.runtime.*
-import io.github.jwharm.javagi.gobject.SignalConnection
 import io.github.compose4gtk.GtkApplier
 import io.github.compose4gtk.SingleChildComposeNode
 import io.github.compose4gtk.modifier.Modifier
+import io.github.jwharm.javagi.gobject.SignalConnection
 import org.gnome.gtk.CheckButton
 
-private class GtkCheckButton(gObject: CheckButton) : SingleChildComposeNode<CheckButton>(gObject, { child = it }) {
+private class GtkCheckButtonComposeNode(gObject: CheckButton) :
+    SingleChildComposeNode<CheckButton>(gObject, { child = it }) {
     var toggled: SignalConnection<CheckButton.ToggledCallback>? = null
 }
 
+sealed interface RadioGroupState
+
+private class RadioGroupStateImpl : RadioGroupState {
+    private val checkButtons = mutableListOf<GtkCheckButtonComposeNode>()
+    fun registerCheckButton(checkButton: GtkCheckButtonComposeNode) {
+        checkButtons.add(checkButton)
+        updateGroup()
+    }
+
+    fun unregisterCheckButton(checkButton: GtkCheckButtonComposeNode) {
+        checkButtons.remove(checkButton)
+        updateGroup()
+    }
+
+    private fun updateGroup() {
+        if (checkButtons.isEmpty()) return
+
+        var prev: GtkCheckButtonComposeNode? = null
+        for (checkButton in checkButtons) {
+            checkButton.widget.setGroup(prev?.widget)
+            prev = checkButton
+        }
+    }
+
+    fun blockToggledListeners() {
+        for (node in checkButtons) {
+            node.toggled?.block()
+        }
+    }
+
+    fun unblockToggledListeners() {
+        for (node in checkButtons) {
+            node.toggled?.unblock()
+        }
+    }
+}
+
+@Composable
+fun rememberRadioGroupState(): RadioGroupState {
+    return remember { RadioGroupStateImpl() }
+}
+
 /**
  * Internal base function for creating a GTK [CheckButton]. Used by public overloads.
  *
@@ -22,46 +65,62 @@ private class GtkCheckButton(gObject: CheckButton) : SingleChildComposeNode<Chec
  * @param label Optional text label.
  * @param useUnderline Whether to use an underscore in the label for mnemonic activation.
  * @param child Optional custom composable content.
- * @param onToggle Callback invoked when the check button is toggled.
+ * @param onActiveRequested Callback invoked when the check button is toggled.
  */
 @Composable
 private fun BaseCheckButton(
     modifier: Modifier = Modifier,
     active: Boolean,
+    state: RadioGroupState? = null,
     inconsistent: Boolean = false,
     label: String? = null,
     useUnderline: Boolean = false,
     child: @Composable () -> Unit = {},
-    onToggle: () -> Unit,
+    onActiveRequested: (active: Boolean) -> Unit,
 ) {
     var pendingChange by remember { mutableStateOf(0) }
-    ComposeNode<GtkCheckButton, GtkApplier>(
-        factory = {
-            GtkCheckButton(CheckButton.builder().build())
-        },
+    val state: RadioGroupStateImpl? = when (state) {
+        is RadioGroupStateImpl -> state
+        null -> null
+    }
+    val checkButtonNode = remember { GtkCheckButtonComposeNode(CheckButton.builder().build()) }
+
+    ComposeNode<GtkCheckButtonComposeNode, GtkApplier>(
+        factory = { checkButtonNode },
         update = {
             set(modifier) { this.applyModifier(it) }
             set(active to pendingChange) { (active, _) ->
                 this.toggled?.block()
+                state?.blockToggledListeners()
                 this.widget.active = active
                 this.toggled?.unblock()
+                state?.unblockToggledListeners()
             }
             set(inconsistent) { this.widget.inconsistent = it }
             set(label) { this.widget.label = it }
             set(useUnderline) { this.widget.useUnderline = it }
-            set(onToggle) {
+            set(onActiveRequested) {
                 this.toggled?.disconnect()
                 this.toggled = this.widget.onToggled {
                     pendingChange += 1
-                    it()
+                    it(this.widget.active)
                     this.toggled?.block()
+                    state?.blockToggledListeners()
                     this.widget.active = active
                     this.toggled?.unblock()
+                    state?.unblockToggledListeners()
                 }
             }
         },
-        content = child
+        content = child,
     )
+
+    DisposableEffect(Unit) {
+        state?.registerCheckButton(checkButtonNode)
+        onDispose {
+            state?.unregisterCheckButton(checkButtonNode)
+        }
+    }
 }
 
 /**
@@ -71,8 +130,7 @@ private fun BaseCheckButton(
  * @param active Whether the check button is currently active.
  * @param inconsistent Whether the button should display an inconsistent (partially active) state.
  * @param useUnderline Whether to use an underscore in the label for mnemonic activation.
- * @param enabled Whether the check button is enabled for interaction.
- * @param onToggle Callback invoked when the check button is toggled.
+ * @param onActiveRequested Callback invoked when the check button is toggled.
  */
 @Composable
 fun CheckButton(
@@ -80,15 +138,14 @@ fun CheckButton(
     active: Boolean,
     inconsistent: Boolean = false,
     useUnderline: Boolean = false,
-    enabled: Boolean = true,
-    onToggle: () -> Unit,
+    onActiveRequested: (active: Boolean) -> Unit,
 ) {
     BaseCheckButton(
         modifier = modifier,
         active = active,
         inconsistent = inconsistent,
         useUnderline = useUnderline,
-        onToggle = onToggle,
+        onActiveRequested = onActiveRequested,
     )
 }
 
@@ -100,7 +157,7 @@ fun CheckButton(
  * @param label Text label.
  * @param inconsistent Whether the button should display an inconsistent (partially active) state.
  * @param useUnderline Whether to use an underscore in the label for mnemonic activation.
- * @param onToggle Callback invoked when the check button is toggled.
+ * @param onActiveRequested Callback invoked when the check button is toggled.
  */
 @Composable
 fun CheckButton(
@@ -109,7 +166,7 @@ fun CheckButton(
     label: String,
     inconsistent: Boolean = false,
     useUnderline: Boolean = false,
-    onToggle: () -> Unit,
+    onActiveRequested: (active: Boolean) -> Unit,
 ) {
     BaseCheckButton(
         modifier = modifier,
@@ -117,7 +174,7 @@ fun CheckButton(
         inconsistent = inconsistent,
         label = label,
         useUnderline = useUnderline,
-        onToggle = onToggle,
+        onActiveRequested = onActiveRequested,
     )
 }
 
@@ -129,7 +186,7 @@ fun CheckButton(
  * @param child Custom composable content.
  * @param inconsistent Whether the button should display an inconsistent (partially active) state.
  * @param useUnderline Whether to use an underscore in the label for mnemonic activation.
- * @param onToggle Callback invoked when the check button is toggled.
+ * @param onActiveRequested Callback invoked when the check button is toggled.
  */
 @Composable
 fun CheckButton(
@@ -138,7 +195,7 @@ fun CheckButton(
     child: @Composable () -> Unit,
     inconsistent: Boolean = false,
     useUnderline: Boolean = false,
-    onToggle: () -> Unit,
+    onActiveRequested: (active: Boolean) -> Unit,
 ) {
     BaseCheckButton(
         modifier = modifier,
@@ -146,6 +203,31 @@ fun CheckButton(
         inconsistent = inconsistent,
         child = child,
         useUnderline = useUnderline,
-        onToggle = onToggle,
+        onActiveRequested = onActiveRequested,
     )
-}
\ No newline at end of file
+}
+
+@Composable
+fun RadioButton(
+    modifier: Modifier = Modifier,
+    state: RadioGroupState,
+    active: Boolean,
+    label: String,
+    inconsistent: Boolean = false,
+    useUnderline: Boolean = false,
+    onSelect: () -> Unit,
+) {
+    BaseCheckButton(
+        modifier = modifier,
+        state = state,
+        active = active,
+        inconsistent = inconsistent,
+        useUnderline = useUnderline,
+        label = label,
+        onActiveRequested = { active ->
+            if (active) {
+                onSelect()
+            }
+        },
+    )
+}
diff --git a/src/test/kotlin/CheckButton.kt b/src/test/kotlin/CheckButton.kt
index 5b87a6e..35b5656 100644
--- a/src/test/kotlin/CheckButton.kt
+++ b/src/test/kotlin/CheckButton.kt
@@ -25,8 +25,8 @@ fun main(args: Array<String>) {
 
                             CheckButton(
                                 modifier = Modifier.alignment(Align.START), active = isChecked, label = "Change me!"
-                            ) {
-                                isChecked = !isChecked
+                            ) { active ->
+                                isChecked = active
                             }
 
                             CheckButton(
@@ -52,8 +52,8 @@ fun main(args: Array<String>) {
                                         Label("Custom child")
                                     }
                                 },
-                            ) {
-                                isChecked = !isChecked
+                            ) {active ->
+                                isChecked = active
                             }
 
                             CheckButton(
@@ -78,7 +78,7 @@ fun main(args: Array<String>) {
                                 active = allChecked(),
                                 inconsistent = someChecked() && !allChecked(),
                                 label = "Select all"
-                            ) {
+                            ) {active ->
                                 val newState = !someChecked()
                                 for (i in checkedStates.indices) {
                                     checkedStates[i] = newState
@@ -90,8 +90,8 @@ fun main(args: Array<String>) {
                                     modifier = Modifier.alignment(Align.START),
                                     active = isChecked,
                                     label = "Option ${index + 1}"
-                                ) {
-                                    checkedStates[index] = !checkedStates[index]
+                                ) { active ->
+                                    checkedStates[index] = active
                                 }
                             }
                         }
diff --git a/src/test/kotlin/CheckButtonGroup.kt b/src/test/kotlin/CheckButtonGroup.kt
new file mode 100644
index 0000000..f72f0a6
--- /dev/null
+++ b/src/test/kotlin/CheckButtonGroup.kt
@@ -0,0 +1,45 @@
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import io.github.compose4gtk.adw.application
+import io.github.compose4gtk.adw.components.ApplicationWindow
+import io.github.compose4gtk.adw.components.HeaderBar
+import io.github.compose4gtk.adw.components.StatusPage
+import io.github.compose4gtk.gtk.components.Button
+import io.github.compose4gtk.gtk.components.RadioButton
+import io.github.compose4gtk.gtk.components.VerticalBox
+import io.github.compose4gtk.gtk.components.rememberRadioGroupState
+import io.github.compose4gtk.modifier.Modifier
+import io.github.compose4gtk.modifier.cssClasses
+
+private val OPTIONS = listOf("Calls", "Missed", "Friends", "Inconsistent")
+
+fun main(args: Array<String>) {
+    application("my.example.hello-app", args) {
+        ApplicationWindow(title = "Radio Buttons", onClose = ::exitApplication) {
+            VerticalBox {
+                HeaderBar(modifier = Modifier.cssClasses("flat"))
+                StatusPage(title = "Radio Buttons", description = "Check buttons grouped to make radio-style buttons") {
+                    VerticalBox {
+                        var selection: String? by remember { mutableStateOf(null) }
+                        val radioState = rememberRadioGroupState()
+
+                        OPTIONS.forEach { option ->
+                            RadioButton(
+                                active = option == selection,
+                                inconsistent = option == selection && option == "Inconsistent",
+                                state = radioState,
+                                label = option,
+                            ) { selection = option }
+                        }
+
+                        Button("Clear") {
+                            selection = null
+                        }
+                    }
+                }
+            }
+        }
+    }
+}


Reviews:
user: login: mrlem
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-08T05:21:45Z
user: login: mrlem
body: Some more comments, but this looks better to me!
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:32:53Z
user: login: Calvicii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:51:17Z
user: login: Calvicii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:54:50Z
user: login: Calvicii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T18:56:37Z
user: login: Calvicii
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T19:04:52Z
user: login: mrlem
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-09T19:23:02Z

Review comments:
diff_hunk: @@ -0,0 +1,40 @@
+import androidx.compose.runtime.mutableStateListOf
+import androidx.compose.runtime.remember
+import io.github.compose4gtk.adw.application
+import io.github.compose4gtk.adw.components.ApplicationWindow
+import io.github.compose4gtk.adw.components.HeaderBar
+import io.github.compose4gtk.adw.components.StatusPage
+import io.github.compose4gtk.gtk.components.CheckButton
+import io.github.compose4gtk.gtk.components.VerticalBox
+import io.github.compose4gtk.modifier.Modifier
+import io.github.compose4gtk.modifier.alignment
+import io.github.compose4gtk.modifier.cssClasses
+import org.gnome.gtk.Align
+
+fun main(args: Array<String>) {
+    application("my.example.hello-app", args) {
+        ApplicationWindow(title = "Radio Buttons", onClose = ::exitApplication) {
+            VerticalBox {
+                HeaderBar(modifier = Modifier.cssClasses("flat"))
+
+                StatusPage(title = "Radio Buttons", description = "Check buttons grouped to make radio-style buttons") {
+                    VerticalBox {
+                        val checkedStates = remember { mutableStateListOf(false, false, false, false, false) }
+                        val radioGroup = org.gnome.gtk.CheckButton()
+
+                        checkedStates.forEachIndexed { index, isChecked ->
+                            CheckButton(
+                                modifier = Modifier.alignment(Align.START),
+                                active = isChecked,
+                                label = "Option $index",
+                                group = radioGroup
+                            ) {
+                                checkedStates[index] = !checkedStates[index]
+                            }
path: src/test/kotlin/CheckButtonGroup.kt
user: login: mrlem
body: This approach does not behave well on recomposition (though this is probably unnoticeable to the user), as it will recreate a new `radioGroup` at every occurrence. You could remember it to avoid reinstanciation on recomposition, but I suspect this would lead to issues if a checkbox disappears (it would remain linked the group).

I'd personally try a more declarative approach with something like:
```kotlin
CheckButtonGroup {
  CheckButton(/* ... */)
  CheckButton(/* ... */)
  /* ... */
}
```

Where:
* CheckButton is the only allowed child for CheckButtonGroup (this can be enforced using the scope)
* CheckButton composable doesn't have a group parameter
* CheckButtonGroup handles child addition/removal by setting CheckButton group attribute internally

But maybe @MMarco94 will think of a better approach.
created_at: 2025-05-08T05:21:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,27 @@
 package io.github.compose4gtk.gtk.components
 
 import androidx.compose.runtime.*
-import io.github.jwharm.javagi.gobject.SignalConnection
 import io.github.compose4gtk.GtkApplier
 import io.github.compose4gtk.SingleChildComposeNode
 import io.github.compose4gtk.modifier.Modifier
+import io.github.jwharm.javagi.gobject.SignalConnection
 import org.gnome.gtk.CheckButton
 
 private class GtkCheckButton(gObject: CheckButton) : SingleChildComposeNode<CheckButton>(gObject, { child = it }) {
     var toggled: SignalConnection<CheckButton.ToggledCallback>? = null
 }
 
+private val LocalCheckButtonGroupLeader = compositionLocalOf<MutableState<CheckButton?>?> { null }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: mrlem
body: Why is the mutability required?
I'd suggest just:
```suggestion
private val LocalCheckButtonGroupLeader = compositionLocalOf<CheckButton?> { null }
```
created_at: 2025-05-09T18:26:22Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,27 @@
 package io.github.compose4gtk.gtk.components
 
 import androidx.compose.runtime.*
-import io.github.jwharm.javagi.gobject.SignalConnection
 import io.github.compose4gtk.GtkApplier
 import io.github.compose4gtk.SingleChildComposeNode
 import io.github.compose4gtk.modifier.Modifier
+import io.github.jwharm.javagi.gobject.SignalConnection
 import org.gnome.gtk.CheckButton
 
 private class GtkCheckButton(gObject: CheckButton) : SingleChildComposeNode<CheckButton>(gObject, { child = it }) {
     var toggled: SignalConnection<CheckButton.ToggledCallback>? = null
 }
 
+private val LocalCheckButtonGroupLeader = compositionLocalOf<MutableState<CheckButton?>?> { null }
+
+@Composable
+fun CheckButtonGroup(content: @Composable () -> Unit) {
+    val groupLeader = remember { mutableStateOf<CheckButton?>(null) }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: mrlem
body: ```suggestion
    val groupLeader = remember { CheckButton() }
```
?
created_at: 2025-05-09T18:27:07Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,9 +46,20 @@ private fun BaseCheckButton(
     onToggle: () -> Unit,
 ) {
     var pendingChange by remember { mutableStateOf(0) }
+    val groupLeaderState = LocalCheckButtonGroupLeader.current
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: mrlem
body: This is not really a state: just `groupLeader`?
created_at: 2025-05-09T18:28:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,9 +46,20 @@ private fun BaseCheckButton(
     onToggle: () -> Unit,
 ) {
     var pendingChange by remember { mutableStateOf(0) }
+    val groupLeaderState = LocalCheckButtonGroupLeader.current
+
     ComposeNode<GtkCheckButton, GtkApplier>(
         factory = {
-            GtkCheckButton(CheckButton.builder().build())
+            val cb = CheckButton.builder().build()
+            if (groupLeaderState != null) {
+                if (groupLeaderState.value != null) {
+                    cb.setGroup(groupLeaderState.value)
+                } else {
+                    groupLeaderState.value = cb
+                    cb.setGroup(cb)
+                }
+            }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: mrlem
body: ```suggestion
            groupLeader?.let {
                cb.setGroup(groupLeader)
            }
```
?
created_at: 2025-05-09T18:29:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,9 +46,20 @@ private fun BaseCheckButton(
     onToggle: () -> Unit,
 ) {
     var pendingChange by remember { mutableStateOf(0) }
+    val groupLeaderState = LocalCheckButtonGroupLeader.current
+
     ComposeNode<GtkCheckButton, GtkApplier>(
         factory = {
-            GtkCheckButton(CheckButton.builder().build())
+            val cb = CheckButton.builder().build()
+            if (groupLeaderState != null) {
+                if (groupLeaderState.value != null) {
+                    cb.setGroup(groupLeaderState.value)
+                } else {
+                    groupLeaderState.value = cb
+                    cb.setGroup(cb)
+                }
+            }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: Calvicii
body: This is because if the groupLeader is not set yet it needs to be. So if there is no group leader is assigns the current checkButton as the group leader and if there is one already it uses it to set the group.
created_at: 2025-05-09T18:51:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,9 +46,20 @@ private fun BaseCheckButton(
     onToggle: () -> Unit,
 ) {
     var pendingChange by remember { mutableStateOf(0) }
+    val groupLeaderState = LocalCheckButtonGroupLeader.current
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: Calvicii
body: Since I'm using a mutable state I thought it was less confusing since I check if the MutableState is null and then if it's value is null.

``` kotlin
if (groupLeader != null) {
    if (groupLeader.value != null) {
        cb.setGroup(groupLeader.value)
    } else {
        groupLeader.value = cb
        cb.setGroup(cb)
    }
}
```

I just think it's less confusing but we could use a better name.
created_at: 2025-05-09T18:54:50Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,27 @@
 package io.github.compose4gtk.gtk.components
 
 import androidx.compose.runtime.*
-import io.github.jwharm.javagi.gobject.SignalConnection
 import io.github.compose4gtk.GtkApplier
 import io.github.compose4gtk.SingleChildComposeNode
 import io.github.compose4gtk.modifier.Modifier
+import io.github.jwharm.javagi.gobject.SignalConnection
 import org.gnome.gtk.CheckButton
 
 private class GtkCheckButton(gObject: CheckButton) : SingleChildComposeNode<CheckButton>(gObject, { child = it }) {
     var toggled: SignalConnection<CheckButton.ToggledCallback>? = null
 }
 
+private val LocalCheckButtonGroupLeader = compositionLocalOf<MutableState<CheckButton?>?> { null }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: Calvicii
body: Because I change its value here:

``` kotlin
if (groupLeaderState != null) {
    if (groupLeaderState.value != null) {
        cb.setGroup(groupLeaderState.value)
    } else {
           groupLeaderState.value = cb
        cb.setGroup(cb)
    }
}
```
created_at: 2025-05-09T18:56:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,27 @@
 package io.github.compose4gtk.gtk.components
 
 import androidx.compose.runtime.*
-import io.github.jwharm.javagi.gobject.SignalConnection
 import io.github.compose4gtk.GtkApplier
 import io.github.compose4gtk.SingleChildComposeNode
 import io.github.compose4gtk.modifier.Modifier
+import io.github.jwharm.javagi.gobject.SignalConnection
 import org.gnome.gtk.CheckButton
 
 private class GtkCheckButton(gObject: CheckButton) : SingleChildComposeNode<CheckButton>(gObject, { child = it }) {
     var toggled: SignalConnection<CheckButton.ToggledCallback>? = null
 }
 
+private val LocalCheckButtonGroupLeader = compositionLocalOf<MutableState<CheckButton?>?> { null }
+
+@Composable
+fun CheckButtonGroup(content: @Composable () -> Unit) {
+    val groupLeader = remember { mutableStateOf<CheckButton?>(null) }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: Calvicii
body: https://github.com/compose4gtk/compose-4-gtk/pull/29#discussion_r2082348283
created_at: 2025-05-09T19:04:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -35,9 +46,20 @@ private fun BaseCheckButton(
     onToggle: () -> Unit,
 ) {
     var pendingChange by remember { mutableStateOf(0) }
+    val groupLeaderState = LocalCheckButtonGroupLeader.current
+
     ComposeNode<GtkCheckButton, GtkApplier>(
         factory = {
-            GtkCheckButton(CheckButton.builder().build())
+            val cb = CheckButton.builder().build()
+            if (groupLeaderState != null) {
+                if (groupLeaderState.value != null) {
+                    cb.setGroup(groupLeaderState.value)
+                } else {
+                    groupLeaderState.value = cb
+                    cb.setGroup(cb)
+                }
+            }
path: src/main/kotlin/io/github/compose4gtk/gtk/components/CheckButton.kt
user: login: mrlem
body: Ok, I think there was a misconception on my side on gtk4 CheckButton usage. I thought the group was a distinct CheckButton dedicated to this role.
created_at: 2025-05-09T19:23:02Z
author_association: CONTRIBUTOR

PR comments:
user: login: Calvicii
created_at: 2025-05-09T15:40:41Z
body: I have made the proposed changes. To be honest I'm new to Compose and related concepts so I could not figure out how to only allow CheckButton inside a group. Thank you for your feedback and if you have any useful links/resources that could help me I would really appreciate it.
author_association: CONTRIBUTOR
user: login: mrlem
created_at: 2025-05-09T18:06:49Z
body: @Calvicii retrospectively:
> * CheckButton is the only allowed child for CheckButtonGroup (this can be enforced using the scope)

... this was a bad idea. Other composables might help positioning check buttons, so there's no need for such constraint.
author_association: CONTRIBUTOR
user: login: mrlem
created_at: 2025-05-09T18:46:08Z
body: Also, checking the PR, I noticed CheckButton() composable "enabled" parameter doesn't seem to be used.
author_association: CONTRIBUTOR
user: login: Calvicii
created_at: 2025-05-09T19:00:43Z
body: I must have added `enabled` by mistake while typing away. Good find!
author_association: CONTRIBUTOR
user: login: MMarco94
created_at: 2025-05-10T13:00:53Z
body: The same effect can be achieved by adding the `radio` CSS class to the `CheckButton`, and using whatever logic do decide which is `active`.

Given how Composable handles state, I don't think this is necessary. Maybe we could have some `CheckRadioButton` that applies the `radio` class automatically, and nothing more
author_association: COLLABORATOR
user: login: Calvicii
created_at: 2025-05-11T16:25:50Z
body: I've tried adding the `radio` class and it did not change the appearance of the CheckButton. I also haven't found anything online about this class. There is `selection-mode` but that's not it's purpose. I agree that we should let the user handle the logic though.
author_association: CONTRIBUTOR
user: login: Calvicii
created_at: 2025-05-11T20:12:49Z
body: I asked in the GTK matrix group and someone said that it isn't possible unless we create our own widget. The only way is for the CheckButton to have another CheckButton as group. We could add a `radio` parameter to the function and make it `false` by default (or it could be an entirely new `CheckRadioButton` like you mentioned). If `true` than we create a CheckButton that we assign to this button's group but is not used by any other buttons. This would make it so it looks like a radio button but it is independent.
author_association: CONTRIBUTOR
user: login: MMarco94
created_at: 2025-05-12T07:36:10Z
body: Ah, interesting. I got the `radio` class from the GTK documentation, I didn't check to see if it actually worked.

I'm afraid that creating an hidden CheckButton might interfere with some screen readers, and in general, if buttons are not linked together, it might lose some semantic necessary for accessibility. Even the Android compose library has the `modifier.selectableGroup()` for this reason.

To be honest, I don't know what's the best path forward here, I'll need to think about it. 
author_association: COLLABORATOR
user: login: Calvicii
created_at: 2025-05-12T17:25:15Z
body: I think for now this solution is the best we got. I modified the example a bit to look like the one from the Android doc.
author_association: CONTRIBUTOR
user: login: MMarco94
created_at: 2025-05-13T20:05:24Z
body: The approach has some issues:
1. Once set, the leader will never change; this might cause memory leaks, or usability problems
2. The UI state and compose state can conflict. For example, try to set `val radioOptions = listOf("Calls", "Missed", "Friends", "Friends")` and choose "Friends", and see what happens :)
3. The implementation assumes that the `CheckButton` are in the same place in the UI tree. While reasonable, that assumption does not exist in GTK


I suggest to change the approach a little bit:
- Create a very simple interface `RadioGroupState<T>`, and a `rememberRadioGroupState<T>(selected: T?) : CheckGroupState<T>` function to create it. The interface won't have much, just which item is selected (this can be backed by a `MutableState` in the implementation
- the only implementation of this interface (can be private) has also functions to register and de-register a `org.gnome.gtk.CheckButton`. This will take care of assigning a group leader, and changing the leader if it is de-registered
- Create new `@Composable` functions alongside the various existing `CheckButton` called `RadioButton<T>`. These functions are similar, but, instead of accepting a `active: Boolean`, they want an `item:T, state: RadioGroupState<T>`. They'll be implicitly active when `item == state.selected`. These function will register/deregister the GTK CheckButton on the state using Composable side-effect primitives (e.g. `DisposableEffect`) or similar.

What do you think? 
author_association: COLLABORATOR
user: login: MMarco94
created_at: 2025-05-13T20:06:17Z
body: PS: thanks for taking the time to tackle this, it is a challenging and important feature
author_association: COLLABORATOR
user: login: Calvicii
created_at: 2025-05-15T20:58:24Z
body: I like your approach and it is indeed a big challenge. I tried following your instructions and I think I'm on the right track, but unfortunately it keeps crashing when I select a radio button. I have no clue why and was wondering if I could get a bit of help here. Thanks for the review!
author_association: CONTRIBUTOR
user: login: MMarco94
created_at: 2025-05-17T11:28:03Z
body: Yeah, not an easy fix. I spent some time to try and fix it. I merged the result. Let me know if it works for you.
author_association: COLLABORATOR
user: login: Calvicii
created_at: 2025-05-18T06:30:25Z
body: Works perfectly!
author_association: CONTRIBUTOR