@@ -20,6 +20,11 @@ import gg.essential.elementa.state.v2.State as StateV2
2020val UIComponent .hasWindow: Boolean
2121 get() = this is Window || hasParent && parent.hasWindow
2222
23+ inline fun <reified T : Effect > UIComponent.get () =
24+ effects.firstNotNullOfOrNull { it as ? T }
25+ inline fun <reified T : Effect > UIComponent.getOrPut (init : () -> T ) =
26+ get<T >() ? : init ().also { enableEffect(it) }
27+
2328fun <T > UIComponent.pollingState (initialValue : T ? = null, getter : () -> T ): State <T > {
2429 val state = BasicState (initialValue ? : getter())
2530 enableEffect(object : Effect () {
@@ -122,98 +127,50 @@ fun UIComponent.onAnimationFrame(block: () -> Unit) =
122127
123128/* *
124129 * Returns a state representing whether this UIComponent is hovered
125- *
126- * [hitTest] will perform a hit test to make sure the user is actually hovered over this component
127- * as compared to the mouse just being within its content bounds while being hovered over another
128- * component rendered above this.
129- *
130- * [layoutSafe] will delay the state change until a time in which it is safe to make layout changes.
131- * This option will induce an additional delay of one frame because the state is updated during the next
132- * [Window.enqueueRenderOperation] after the hoverState changes.
133130 */
134- fun UIComponent.hoveredStateV2 (hitTest : Boolean = true, layoutSafe : Boolean = true): StateV2 <Boolean > {
135- // "Unsafe" means that it is not safe to depend on this for layout changes
136- val unsafeHovered = mutableStateOf(false )
131+ fun UIComponent.hoveredStateV2 (): StateV2 <Boolean > {
132+ return getOrPut { ComponentHoveredState () }.state
133+ }
134+
135+ private class WindowHoveredState : Effect () {
136+ private var oldHovered = listOf<UIComponent >()
137137
138- // "Safe" because layout changes can directly happen when this changes (ie in onSetValue)
139- val safeHovered = mutableStateOf(false )
138+ init { addUpdateFunc { _, _ -> update() } }
139+ private fun update () {
140+ val window = boundComponent as Window
140141
141- // Performs a hit test based on the current mouse x / y
142- fun hitTestHovered (): Boolean {
143142 // Positions the mouse in the center of pixels so isPointInside will
144143 // pass for items 1 pixel wide objects. See ElementaVersion v2 for more details
145144 val halfPixel = 0.5f / UResolution .scaleFactor.toFloat()
146145 val mouseX = UMouse .Scaled .x.toFloat() + halfPixel
147146 val mouseY = UMouse .Scaled .y.toFloat() + halfPixel
148- return if (isPointInside(mouseX, mouseY)) {
149147
150- val window = Window .of(this )
151- val hit = (window.hoveredFloatingComponent?.hitTest(mouseX, mouseY)) ? : window.hitTest(mouseX, mouseY)
152-
153- hit.isComponentInParentChain(this ) || hit == this
154- } else {
155- false
156- }
157- }
158-
159- if (hitTest) {
160- // It's possible the animation framerate will exceed that of the actual frame rate
161- // Therefore, in order to avoid redundantly performing the hit test multiple times
162- // in the same frame, this boolean is used to ensure that hit testing is performed
163- // at most only a single time each frame
164- var registerHitTest = true
165-
166- onAnimationFrame {
167- if (registerHitTest) {
168- registerHitTest = false
169- Window .enqueueRenderOperation {
170- // The next animation frame should register another renderOperation
171- registerHitTest = true
172-
173- // It is possible that this component or a component in its parent tree
174- // was removed from the component tree between the last call to animationFrame
175- // and this evaluation in enqueueRenderOperation. If that is the case, we should not
176- // perform the hit test because it will throw an exception.
177- if (! this .isInComponentTree()) {
178- // Unset the hovered state because a component can no longer
179- // be hovered if it is not in the component tree
180- unsafeHovered.set(false )
181- return @enqueueRenderOperation
182- }
183-
184- // Since enqueueRenderOperation will keep polling the queue until there are no more items,
185- // the forwarding of any update to the safeHovered state will still happen this frame
186- unsafeHovered.set(hitTestHovered())
187- }
148+ val hit = (window.hoveredFloatingComponent?.hitTest(mouseX, mouseY)) ? : window.hitTest(mouseX, mouseY)
149+ val newHovered = hit.selfAndParents().toList()
150+ for (component in oldHovered) {
151+ if (component !in newHovered) {
152+ component.get<ComponentHoveredState >()?.state?.set(false )
188153 }
189154 }
190- }
191- onMouseEnter {
192- if (hitTest) {
193- unsafeHovered.set(hitTestHovered())
194- } else {
195- unsafeHovered.set(true )
155+ for (component in newHovered) {
156+ if (component !in oldHovered) {
157+ component.get<ComponentHoveredState >()?.state?.set(true )
158+ }
196159 }
160+ oldHovered = newHovered
197161 }
162+ }
198163
199- onMouseLeave {
200- unsafeHovered.set(false )
201- }
164+ private class ComponentHoveredState : Effect () {
165+ val state = mutableStateOf(false )
202166
203- return if (layoutSafe) {
204- unsafeHovered.onChange(this ) { hovered ->
205- Window .enqueueRenderOperation {
206- safeHovered.set(hovered)
207- }
208- }
209- safeHovered
210- } else {
211- unsafeHovered
167+ override fun setup () {
168+ Window .of(boundComponent).getOrPut { WindowHoveredState () }
212169 }
213170}
214171
215- fun UIComponent.hoveredState (hitTest : Boolean = true, layoutSafe : Boolean = true ): State <Boolean > =
216- hoveredStateV2(hitTest, layoutSafe ).toV1(this )
172+ fun UIComponent.hoveredState (): State <Boolean > =
173+ hoveredStateV2().toV1(this )
217174
218175/* * Marker effect for [makeHoverScope]/[hoverScope]. */
219176private class HoverScope (val state : StateV2 <Boolean >) : Effect()
0 commit comments