-
Notifications
You must be signed in to change notification settings - Fork 5.7k
[JEWEL-921] Migrate .composed Calls to Modifier.Node API #3423
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,160 +1,249 @@ | ||
| package org.jetbrains.jewel.foundation.modifier | ||
|
|
||
| import androidx.compose.foundation.focusGroup | ||
| import androidx.compose.runtime.DisposableEffect | ||
| import androidx.compose.runtime.Immutable | ||
| import androidx.compose.runtime.Stable | ||
| import androidx.compose.runtime.derivedStateOf | ||
| import androidx.compose.runtime.getValue | ||
| import androidx.compose.runtime.mutableStateOf | ||
| import androidx.compose.runtime.remember | ||
| import androidx.compose.runtime.setValue | ||
| import androidx.compose.runtime.structuralEqualityPolicy | ||
| import androidx.compose.ui.Modifier | ||
| import androidx.compose.ui.composed | ||
| import androidx.compose.ui.focus.onFocusChanged | ||
| import androidx.compose.ui.modifier.ModifierLocalConsumer | ||
| import androidx.compose.ui.modifier.ModifierLocalProvider | ||
| import androidx.compose.ui.modifier.ModifierLocalReadScope | ||
| import androidx.compose.ui.focus.FocusEventModifierNode | ||
| import androidx.compose.ui.focus.FocusState | ||
| import androidx.compose.ui.modifier.ModifierLocalModifierNode | ||
| import androidx.compose.ui.modifier.ProvidableModifierLocal | ||
| import androidx.compose.ui.modifier.modifierLocalMapOf | ||
| import androidx.compose.ui.modifier.modifierLocalOf | ||
| import androidx.compose.ui.modifier.modifierLocalProvider | ||
| import androidx.compose.ui.node.ModifierNodeElement | ||
| import androidx.compose.ui.node.ObserverModifierNode | ||
| import androidx.compose.ui.node.observeReads | ||
| import androidx.compose.ui.platform.InspectorInfo | ||
| import androidx.compose.ui.platform.InspectorValueInfo | ||
| import androidx.compose.ui.platform.debugInspectorInfo | ||
| import java.awt.Component | ||
| import java.awt.Window | ||
| import java.awt.event.FocusEvent | ||
| import java.awt.event.FocusListener | ||
| import java.awt.event.WindowAdapter | ||
| import java.awt.event.WindowEvent | ||
|
|
||
| @Suppress("ModifierComposed") // To fix in JEWEL-921 | ||
| public fun Modifier.trackWindowActivation(window: Window): Modifier = | ||
| composed( | ||
| debugInspectorInfo { | ||
| name = "activateRoot" | ||
| properties["window"] = window | ||
| /** | ||
| * Tracks the activation state of the provided AWT [Window]. | ||
| * | ||
| * This modifier listens to the window's "activated" and "deactivated" events. When the window is active, it provides | ||
| * `true` to the [ModifierLocalActivated] local, allowing child modifiers (like [onActivated]) to react to the window's | ||
| * state. | ||
| * | ||
| * @param window The AWT Window to observe. | ||
| */ | ||
| @Stable | ||
| public fun Modifier.trackWindowActivation(window: Window): Modifier = this then TrackWindowActivationModifier(window) | ||
|
|
||
| /** | ||
| * Tracks the focus/activation state of a native AWT [Component]. | ||
| * | ||
| * It listens to the component's focus events and provides the activation state to the Compose hierarchy. | ||
| * | ||
| * @param awtParent The parent AWT Component to observe for focus events. | ||
| */ | ||
| @Stable | ||
| public fun Modifier.trackComponentActivation(awtParent: Component): Modifier = | ||
| this then TrackComponentActivationModifier(awtParent) | ||
|
|
||
| /** | ||
| * Tracks activation based on the focus state of this modifier's children. | ||
| * | ||
| * This modifier applies a [focusGroup] to its content. It considers itself "activated" if the parent is activated AND | ||
| * any child within this focus group currently holds focus. | ||
| */ | ||
| @Stable public fun Modifier.trackActivation(): Modifier = this.focusGroup().then(TrackActivationModifier) | ||
|
|
||
| /** | ||
| * A callback modifier that triggers whenever the activation state changes. | ||
| * | ||
| * This modifier consumes the value provided by [ModifierLocalActivated] (set by [trackWindowActivation], | ||
| * [trackActivation] or [trackWindowActivation]). When that value changes, the [onChanged] lambda is invoked. | ||
| * | ||
| * @param enabled Whether this callback is active. If `false`, the modifier is effectively a no-op. | ||
| * @param onChanged A lambda called with the new activation state (`true` for active, `false` for inactive). | ||
| */ | ||
| public fun Modifier.onActivated(enabled: Boolean = true, onChanged: (Boolean) -> Unit): Modifier = | ||
| if (enabled) { | ||
| this then ActivateChangedModifier(onChanged) | ||
| } else { | ||
| this | ||
| } | ||
|
|
||
| @Immutable | ||
| private data class TrackWindowActivationModifier(val window: Window) : | ||
| ModifierNodeElement<TrackWindowActivationNode>() { | ||
| override fun create() = TrackWindowActivationNode(window) | ||
|
|
||
| override fun update(node: TrackWindowActivationNode) { | ||
| node.update(window) | ||
| } | ||
|
|
||
| override fun InspectorInfo.inspectableProperties() { | ||
| name = "trackWindowActivation" | ||
| properties["window"] = window | ||
| } | ||
| } | ||
|
|
||
| private class TrackWindowActivationNode(var window: Window) : Modifier.Node(), ModifierLocalModifierNode { | ||
| override val providedValues = modifierLocalMapOf(ModifierLocalActivated to false) | ||
|
|
||
| private val listener = | ||
| object : WindowAdapter() { | ||
| override fun windowActivated(e: WindowEvent?) { | ||
| provide(ModifierLocalActivated, true) | ||
| } | ||
|
|
||
| override fun windowDeactivated(e: WindowEvent?) { | ||
| provide(ModifierLocalActivated, false) | ||
| } | ||
| } | ||
| ) { | ||
| var parentActivated by remember { mutableStateOf(false) } | ||
|
|
||
| DisposableEffect(window) { | ||
| val listener = | ||
| object : WindowAdapter() { | ||
| override fun windowActivated(e: WindowEvent?) { | ||
| parentActivated = true | ||
| } | ||
|
|
||
| override fun windowDeactivated(e: WindowEvent?) { | ||
| parentActivated = false | ||
| } | ||
| } | ||
|
|
||
| override fun onAttach() { | ||
| super.onAttach() | ||
| window.addWindowListener(listener) | ||
| provide(ModifierLocalActivated, window.isActive) | ||
| } | ||
|
|
||
| override fun onDetach() { | ||
| super.onDetach() | ||
| window.removeWindowListener(listener) | ||
| } | ||
|
|
||
| fun update(newWindow: Window) { | ||
| if (window != newWindow) { | ||
| window.removeWindowListener(listener) | ||
| window = newWindow | ||
| window.addWindowListener(listener) | ||
| onDispose { window.removeWindowListener(listener) } | ||
| provide(ModifierLocalActivated, window.isActive) | ||
| } | ||
| Modifier.modifierLocalProvider(ModifierLocalActivated) { parentActivated } | ||
| } | ||
| } | ||
|
|
||
| @Suppress("ModifierComposed") // To fix in JEWEL-921 | ||
| public fun Modifier.trackComponentActivation(awtParent: Component): Modifier = | ||
| composed( | ||
| debugInspectorInfo { | ||
| name = "activateRoot" | ||
| properties["parent"] = awtParent | ||
| } | ||
| ) { | ||
| var parentActivated by remember { mutableStateOf(false) } | ||
|
|
||
| DisposableEffect(awtParent) { | ||
| val listener = | ||
| object : FocusListener { | ||
| override fun focusGained(e: FocusEvent?) { | ||
| parentActivated = true | ||
| } | ||
|
|
||
| override fun focusLost(e: FocusEvent?) { | ||
| parentActivated = false | ||
| } | ||
| } | ||
| awtParent.addFocusListener(listener) | ||
| onDispose { awtParent.removeFocusListener(listener) } | ||
| } | ||
| @Immutable | ||
| private data object TrackActivationModifier : ModifierNodeElement<TrackActivationNode>() { | ||
| override fun create() = TrackActivationNode() | ||
|
|
||
| Modifier.modifierLocalProvider(ModifierLocalActivated) { parentActivated } | ||
| override fun update(node: TrackActivationNode) { | ||
| // no-op | ||
| } | ||
|
|
||
| @Suppress("ModifierComposed") // To fix in JEWEL-921 | ||
| @Stable | ||
| public fun Modifier.trackActivation(): Modifier = | ||
| composed(debugInspectorInfo { name = "trackActivation" }) { | ||
| val activatedModifierLocal = remember { ActivatedModifierLocal() } | ||
| Modifier.focusGroup() | ||
| .onFocusChanged { | ||
| if (it.hasFocus) { | ||
| activatedModifierLocal.childGainedFocus() | ||
| } else { | ||
| activatedModifierLocal.childLostFocus() | ||
| } | ||
| } | ||
| .then(activatedModifierLocal) | ||
| override fun InspectorInfo.inspectableProperties() { | ||
| name = "trackActivation" | ||
| } | ||
| } | ||
|
|
||
| private class ActivatedModifierLocal : ModifierLocalProvider<Boolean>, ModifierLocalConsumer { | ||
| private var parentActivated: Boolean by mutableStateOf(false) | ||
| private class TrackActivationNode : | ||
| Modifier.Node(), FocusEventModifierNode, ModifierLocalModifierNode, ObserverModifierNode { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We have to use both It's basically the equivalent of You can find a sample of ObserverModifier Node here |
||
| override val providedValues = modifierLocalMapOf(ModifierLocalActivated to false) | ||
|
|
||
| private var hasFocus: Boolean by mutableStateOf(false) | ||
| private var parentActivated = false | ||
| private var isFocused = false | ||
|
|
||
| override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) { | ||
| with(scope) { parentActivated = ModifierLocalActivated.current } | ||
| override fun onAttach() { | ||
| super.onAttach() | ||
| observeReads { fetchParentActivation() } | ||
| } | ||
|
|
||
| override val key: ProvidableModifierLocal<Boolean> = ModifierLocalActivated | ||
| override val value: Boolean by derivedStateOf(structuralEqualityPolicy()) { parentActivated && hasFocus } | ||
| override fun onObservedReadsChanged() { | ||
| observeReads { fetchParentActivation() } | ||
| } | ||
|
|
||
| override fun onFocusEvent(focusState: FocusState) { | ||
| if (isFocused != focusState.isFocused) { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to clarify - are My initial assumption was that a To verify, I put together a minimal reproducer simulating private data object SimulatedWindowActivationElement : ModifierNodeElement<SimulatedWindowActivationNode>() {
override fun create() = SimulatedWindowActivationNode()
override fun update(node: SimulatedWindowActivationNode) = Unit
override fun InspectorInfo.inspectableProperties() { name = "simulatedWindowActivation" }
}
private class SimulatedWindowActivationNode : Modifier.Node(), ModifierLocalModifierNode {
override val providedValues = modifierLocalMapOf(ModifierLocalActivated to true)
}
fun main(): Unit = application {
Window(onCloseRequest = ::exitApplication) {
IntUiTheme(theme = JewelTheme.lightThemeDefinition(), styling = ComponentStyling.default()) {
var activationReceived by remember { mutableStateOf(false) }
Column(Modifier.then(SimulatedWindowActivationElement).trackActivation()) {
Text(
"Activation received: $activationReceived",
modifier = Modifier.onActivated { activationReceived = it },
)
OutlinedButton(onClick = {}) { Text("Click to focus") }
}
}
}
}I also added logging inside override fun onFocusEvent(focusState: FocusState) {
println("onFocusEvent: isFocused=${focusState.isFocused} hasFocus=${focusState.hasFocus}")
println("node.isAttached=$isAttached")
println("focusState=$focusState")
println("focusState::class=${focusState::class}")
if (isFocused != focusState.isFocused) {
isFocused = focusState.isFocused
updateProvidedValue()
}
}
private fun updateProvidedValue() {
println("updateProvidedValue: parentActivated=$parentActivated isFocused=$isFocused result=${parentActivated && isFocused}")
provide(ModifierLocalActivated, parentActivated && isFocused)
}Which produced the following output when clicking the button:
Is that the right understanding? If so,
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. tl;dr but isFocused == this node is the one owning the focus, hasFocus == this node or any of its children have the focus
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In this case we could use Given the nature of the code (active if the parent or children have focus),
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mine understanding was that the node |
||
| isFocused = focusState.isFocused | ||
| updateProvidedValue() | ||
| } | ||
| } | ||
|
|
||
| fun childLostFocus() { | ||
| hasFocus = false | ||
| private fun fetchParentActivation() { | ||
| parentActivated = ModifierLocalActivated.current | ||
| updateProvidedValue() | ||
| } | ||
|
|
||
| fun childGainedFocus() { | ||
| hasFocus = true | ||
| private fun updateProvidedValue() { | ||
| provide(ModifierLocalActivated, parentActivated && isFocused) | ||
| } | ||
| } | ||
|
|
||
| public val ModifierLocalActivated: ProvidableModifierLocal<Boolean> = modifierLocalOf { false } | ||
| @Immutable | ||
| private data class TrackComponentActivationModifier(val awtParent: Component) : | ||
| ModifierNodeElement<TrackComponentActivationNode>() { | ||
| override fun create() = TrackComponentActivationNode(awtParent) | ||
|
|
||
| public fun Modifier.onActivated(enabled: Boolean = true, onChanged: (Boolean) -> Unit): Modifier = | ||
| this then | ||
| if (enabled) { | ||
| ActivateChangedModifierElement( | ||
| onChanged, | ||
| debugInspectorInfo { | ||
| name = "onActivated" | ||
| properties["onChanged"] = onChanged | ||
| }, | ||
| ) | ||
| } else { | ||
| Modifier | ||
| override fun update(node: TrackComponentActivationNode) { | ||
| node.update(awtParent) | ||
| } | ||
|
|
||
| override fun InspectorInfo.inspectableProperties() { | ||
| name = "trackComponentActivation" | ||
| properties["awtParent"] = awtParent | ||
| } | ||
| } | ||
|
|
||
| private class TrackComponentActivationNode(var awtParent: Component) : Modifier.Node(), ModifierLocalModifierNode { | ||
| override val providedValues = modifierLocalMapOf(ModifierLocalActivated to false) | ||
|
|
||
| val listener = | ||
| object : FocusListener { | ||
| override fun focusGained(e: FocusEvent?) { | ||
| provide(ModifierLocalActivated, true) | ||
| } | ||
|
|
||
| override fun focusLost(e: FocusEvent?) { | ||
| provide(ModifierLocalActivated, false) | ||
| } | ||
| } | ||
|
|
||
| private class ActivateChangedModifierElement( | ||
| private val onChanged: (Boolean) -> Unit, | ||
| inspectorInfo: InspectorInfo.() -> Unit, | ||
| ) : ModifierLocalConsumer, InspectorValueInfo(inspectorInfo) { | ||
| override fun equals(other: Any?): Boolean { | ||
| if (this === other) return true | ||
| if (other !is ActivateChangedModifierElement) return false | ||
| override fun onAttach() { | ||
| super.onAttach() | ||
| awtParent.addFocusListener(listener) | ||
| provide(ModifierLocalActivated, awtParent.hasFocus()) | ||
| } | ||
|
|
||
| if (onChanged != other.onChanged) return false | ||
| override fun onDetach() { | ||
| super.onDetach() | ||
| awtParent.removeFocusListener(listener) | ||
| } | ||
|
|
||
| return true | ||
| fun update(newAwtParent: Component) { | ||
| if (awtParent != newAwtParent) { | ||
| awtParent.removeFocusListener(listener) | ||
| awtParent = newAwtParent | ||
| awtParent.addFocusListener(listener) | ||
| provide(ModifierLocalActivated, awtParent.hasFocus()) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| override fun hashCode(): Int = onChanged.hashCode() | ||
| @Immutable | ||
| private data class ActivateChangedModifier(val onChanged: (Boolean) -> Unit) : | ||
| ModifierNodeElement<ActivateChangedNode>() { | ||
| override fun create() = ActivateChangedNode(onChanged) | ||
|
|
||
| override fun update(node: ActivateChangedNode) { | ||
| node.onChanged = onChanged | ||
| } | ||
|
|
||
| override fun InspectorInfo.inspectableProperties() { | ||
| name = "onActivated" | ||
| properties["onChanged"] = onChanged | ||
| } | ||
| } | ||
|
|
||
| private class ActivateChangedNode(var onChanged: (Boolean) -> Unit) : | ||
| Modifier.Node(), ModifierLocalModifierNode, ObserverModifierNode { | ||
| private var currentActivated = false | ||
|
|
||
| override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) { | ||
| with(scope) { | ||
| override fun onAttach() { | ||
| super.onAttach() | ||
| fetchActivatedValue() | ||
| } | ||
|
|
||
| override fun onObservedReadsChanged() { | ||
| fetchActivatedValue() | ||
| } | ||
|
|
||
| private fun fetchActivatedValue() { | ||
| observeReads { | ||
| val activated = ModifierLocalActivated.current | ||
| if (activated != currentActivated) { | ||
| currentActivated = activated | ||
|
|
@@ -163,3 +252,5 @@ private class ActivateChangedModifierElement( | |
| } | ||
| } | ||
| } | ||
|
|
||
| public val ModifierLocalActivated: ProvidableModifierLocal<Boolean> = modifierLocalOf { false } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Detekt do be detekting