Skip to content

Commit cc508f3

Browse files
authored
UIComponent: Rewrite floating API
The old API is difficult to work with because it will continue to render floating components even if they've been removed from the component tree. Additionally it can CME if components are removed from the floating list during rendering, further complicating the workarounds required. This new API fixes the issue by tracking when components are removed/added from/to the tree and updating its internal floating list accordingly. It also allows setting the floating state at any time, even before the component has a parent, another thing the old API did not support. The order in which floating components appear also differs in the new API. While the old API showed floating components in the order in which they were set to be floating, this often isn't all too useful when the order in which components are added/removed to/from the tree is not particularly well defined. As such, the new API chooses to instead order floating components in exactly the same way as they appear in the component tree (pre-order tree traversal, i.e. first parent, then children). This results in consistent ordering and is generally the order you want for nested floating components to behave in a useful way. This has been implemented as a new, completely separate API instead of an ElementaVersion primarily to easy migration (the new API can be used even with Windows still on older ElementaVersions; both APIs can be used at the same time) but also because there isn't anything reasonable the old-API methods in `Window` could do in the new version, they really should have been internal to begin with. GitHub: #154
1 parent d9cb647 commit cc508f3

File tree

5 files changed

+92
-115
lines changed

5 files changed

+92
-115
lines changed

api/Elementa.api

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public abstract class gg/essential/elementa/UIComponent : java/util/Observable,
100100
public fun insertChildAt (Lgg/essential/elementa/UIComponent;I)Lgg/essential/elementa/UIComponent;
101101
public fun insertChildBefore (Lgg/essential/elementa/UIComponent;Lgg/essential/elementa/UIComponent;)Lgg/essential/elementa/UIComponent;
102102
public fun isChildOf (Lgg/essential/elementa/UIComponent;)Z
103+
public final fun isFloating ()Z
103104
public fun isHovered ()Z
104105
protected final fun isInitialized ()Z
105106
public fun isPointInside (FF)Z
@@ -144,6 +145,7 @@ public abstract class gg/essential/elementa/UIComponent : java/util/Observable,
144145
public final fun setFontProvider (Lgg/essential/elementa/font/FontProvider;)Lgg/essential/elementa/UIComponent;
145146
public final fun setHeight (Lgg/essential/elementa/constraints/HeightConstraint;)Lgg/essential/elementa/UIComponent;
146147
protected final fun setInitialized (Z)V
148+
public final fun setIsFloating (Z)V
147149
public final fun setLastDraggedMouseX (Ljava/lang/Double;)V
148150
public final fun setLastDraggedMouseY (Ljava/lang/Double;)V
149151
public final fun setMouseScrollListeners (Ljava/util/List;)V
@@ -1150,7 +1152,6 @@ public final class gg/essential/elementa/components/inspector/Inspector : gg/ess
11501152
public fun <init> (Lgg/essential/elementa/UIComponent;Ljava/awt/Color;Ljava/awt/Color;F)V
11511153
public fun <init> (Lgg/essential/elementa/UIComponent;Ljava/awt/Color;Ljava/awt/Color;FLgg/essential/elementa/constraints/HeightConstraint;)V
11521154
public synthetic fun <init> (Lgg/essential/elementa/UIComponent;Ljava/awt/Color;Ljava/awt/Color;FLgg/essential/elementa/constraints/HeightConstraint;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
1153-
public fun animationFrame ()V
11541155
public fun draw (Lgg/essential/universal/UMatrixStack;)V
11551156
}
11561157

src/main/kotlin/gg/essential/elementa/UIComponent.kt

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,11 @@ abstract class UIComponent : Observable(), ReferenceHolder {
5353

5454
private var childrenLocked = 0
5555
init {
56-
children.addObserver { _, _ -> requireChildrenUnlocked() }
57-
children.addObserver { _, event -> setWindowCacheOnChangedChild(event) }
56+
children.addObserver { _, event ->
57+
requireChildrenUnlocked()
58+
setWindowCacheOnChangedChild(event)
59+
updateFloatingComponentsOnChangedChild(event)
60+
}
5861
}
5962

6063
open lateinit var parent: UIComponent
@@ -110,7 +113,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
110113
private var heldReferences = mutableListOf<Any>()
111114

112115
protected var isInitialized = false
113-
private var isFloating = false
116+
private var isLegacyFloating = false
114117

115118
private var didCallBeforeDraw = false
116119
private var warnedAboutBeforeDraw = false
@@ -479,7 +482,7 @@ abstract class UIComponent : Observable(), ReferenceHolder {
479482
val parentWindow = Window.of(this)
480483

481484
this.forEachChild { child ->
482-
if (child.isFloating) return@forEachChild
485+
if (child.isLegacyFloating || child.isFloating) return@forEachChild
483486

484487
// If the child is outside the current viewport, don't waste time drawing
485488
if (!this.alwaysDrawChildren() && !parentWindow.isAreaVisible(
@@ -980,8 +983,65 @@ abstract class UIComponent : Observable(), ReferenceHolder {
980983
* Floating API
981984
*/
982985

986+
@set:JvmName("setIsFloating") // `setFloating` is taken by the old API
987+
var isFloating: Boolean = false
988+
set(value) {
989+
if (value == field) return
990+
field = value
991+
recomputeFloatingComponents()
992+
}
993+
994+
internal var floatingComponents: List<UIComponent>? = null // only allocated if used
995+
996+
private fun recomputeFloatingComponents() {
997+
val result = mutableListOf<UIComponent>()
998+
if (isFloating) {
999+
result.add(this)
1000+
}
1001+
for (child in children) {
1002+
child.floatingComponents?.let { result.addAll(it) }
1003+
}
1004+
if ((floatingComponents ?: emptyList()) == result) {
1005+
return // unchanged
1006+
}
1007+
floatingComponents = result.takeUnless { it.isEmpty() }
1008+
1009+
if (this is Window) {
1010+
if (hoveredFloatingComponent !in result) {
1011+
hoveredFloatingComponent = null
1012+
}
1013+
} else if (hasParent) {
1014+
parent.recomputeFloatingComponents()
1015+
}
1016+
}
1017+
1018+
private fun updateFloatingComponentsOnChangedChild(possibleEvent: Any) {
1019+
@Suppress("UNCHECKED_CAST")
1020+
when (val event = possibleEvent as? ObservableListEvent<UIComponent> ?: return) {
1021+
is ObservableAddEvent -> {
1022+
val (_, child) = event.element
1023+
if (child.floatingComponents != null) {
1024+
recomputeFloatingComponents()
1025+
}
1026+
}
1027+
is ObservableRemoveEvent -> {
1028+
val (_, child) = event.element
1029+
if (child.floatingComponents != null) {
1030+
recomputeFloatingComponents()
1031+
}
1032+
}
1033+
is ObservableClearEvent -> {
1034+
if (floatingComponents != null) {
1035+
recomputeFloatingComponents()
1036+
}
1037+
}
1038+
}
1039+
}
1040+
1041+
@Deprecated("The legacy floating API does not behave well when a component is removed from the tree.", ReplaceWith("isFloating = floating"))
1042+
@Suppress("DEPRECATION")
9831043
fun setFloating(floating: Boolean) {
984-
isFloating = floating
1044+
isLegacyFloating = floating
9851045

9861046
if (floating) {
9871047
Window.of(this).addFloatingComponent(this)

src/main/kotlin/gg/essential/elementa/components/Window.kt

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ class Window @JvmOverloads constructor(
2525
private var systemTime = -1L
2626
private var currentMouseButton = -1
2727

28-
private var floatingComponents = mutableListOf<UIComponent>()
28+
private var legacyFloatingComponents = mutableListOf<UIComponent>()
2929

3030
var hoveredFloatingComponent: UIComponent? = null
3131
var focusedComponent: UIComponent? = null
@@ -98,7 +98,7 @@ class Window @JvmOverloads constructor(
9898

9999
hoveredFloatingComponent = null
100100
val (mouseX, mouseY) = getMousePosition()
101-
for (component in floatingComponents.reversed()) {
101+
for (component in allFloatingComponentsInReverseOrder()) {
102102
if (component.isPointInside(mouseX, mouseY)) {
103103
hoveredFloatingComponent = component
104104
break
@@ -159,7 +159,7 @@ class Window @JvmOverloads constructor(
159159
fun drawFloatingComponents(matrixStack: UMatrixStack) {
160160
requireMainThread()
161161

162-
val it = floatingComponents.iterator()
162+
val it = legacyFloatingComponents.iterator()
163163
while (it.hasNext()) {
164164
val component = it.next()
165165
if (ofOrNull(component) == null) {
@@ -168,6 +168,9 @@ class Window @JvmOverloads constructor(
168168
}
169169
component.drawCompat(matrixStack)
170170
}
171+
for (component in floatingComponents ?: emptyList()) {
172+
component.drawCompat(matrixStack)
173+
}
171174
}
172175

173176
override fun mouseScroll(delta: Double) {
@@ -178,7 +181,7 @@ class Window @JvmOverloads constructor(
178181
requireMainThread()
179182

180183
val (mouseX, mouseY) = getMousePosition()
181-
for (floatingComponent in floatingComponents.reversed()) {
184+
for (floatingComponent in allFloatingComponentsInReverseOrder()) {
182185
if (floatingComponent.isPointInside(mouseX, mouseY)) {
183186
floatingComponent.mouseScroll(delta)
184187
return
@@ -211,7 +214,7 @@ class Window @JvmOverloads constructor(
211214
}
212215
}
213216

214-
for (floatingComponent in floatingComponents.reversed()) {
217+
for (floatingComponent in allFloatingComponentsInReverseOrder()) {
215218
if (floatingComponent.isPointInside(mouseX.toFloat(), mouseY.toFloat())) {
216219
floatingComponent.mouseClick(mouseX, mouseY, button)
217220
dealWithFocusRequests()
@@ -335,29 +338,36 @@ class Window @JvmOverloads constructor(
335338
* Floating API
336339
*/
337340

341+
private fun allFloatingComponentsInReverseOrder(): Sequence<UIComponent> =
342+
(floatingComponents ?: emptyList()).asReversed().asSequence() +
343+
// Note: needs to be copied to guard against CME and for backwards compatibility
344+
legacyFloatingComponents.reversed()
345+
346+
@Deprecated("Internal API.", replaceWith = ReplaceWith("component.setFloating(true)"))
338347
fun addFloatingComponent(component: UIComponent) {
339348
if (isInitialized) {
340349
requireMainThread()
341350
}
342351

343-
if (floatingComponents.contains(component)) return
352+
if (legacyFloatingComponents.contains(component)) return
344353

345-
floatingComponents.add(component)
354+
legacyFloatingComponents.add(component)
346355
}
347356

357+
@Deprecated("Internal API.", replaceWith = ReplaceWith("component.setFloating(false)"))
348358
fun removeFloatingComponent(component: UIComponent) {
349359
if (isInitialized) {
350360
requireMainThread()
351361
}
352362

353-
floatingComponents.remove(component)
363+
legacyFloatingComponents.remove(component)
354364
}
355365

356366
/**
357367
* Overridden to including floating components.
358368
*/
359369
override fun hitTest(x: Float, y: Float): UIComponent {
360-
for (component in floatingComponents.reversed()) {
370+
for (component in allFloatingComponentsInReverseOrder()) {
361371
if (component.isPointInside(x, y)) {
362372
return component.hitTest(x, y)
363373
}

src/main/kotlin/gg/essential/elementa/components/inspector/Inspector.kt

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ class Inspector @JvmOverloads constructor(
4545
height = ChildBasedSizeConstraint()
4646
}
4747

48+
isFloating = true
49+
4850
container = UIBlock(backgroundColor).constrain {
4951
width = ChildBasedMaxSizeConstraint()
5052
height = ChildBasedSizeConstraint()
@@ -230,28 +232,7 @@ class Inspector @JvmOverloads constructor(
230232
}
231233
}
232234

233-
private fun UIComponent.isMounted(): Boolean =
234-
parent == this || (this in parent.children && parent.isMounted())
235-
236-
override fun animationFrame() {
237-
super.animationFrame()
238-
239-
// Make sure we are the top-most component (last to draw and first to receive input)
240-
Window.enqueueRenderOperation {
241-
setFloating(false)
242-
if (isMounted()) { // only if we are still mounted
243-
setFloating(true)
244-
}
245-
}
246-
}
247-
248235
override fun draw(matrixStack: UMatrixStack) {
249-
// If we got removed from our parent, we need to un-float ourselves
250-
if (!isMounted()) {
251-
Window.enqueueRenderOperation { setFloating(false) }
252-
return
253-
}
254-
255236
separator1.setWidth(container.getWidth().pixels())
256237
separator2.setWidth(container.getWidth().pixels())
257238

unstable/layoutdsl/src/main/kotlin/gg/essential/elementa/layoutdsl/containers.kt

Lines changed: 4 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ package gg.essential.elementa.layoutdsl
44

55
import gg.essential.elementa.UIComponent
66
import gg.essential.elementa.components.ScrollComponent
7-
import gg.essential.elementa.components.UIBlock
8-
import gg.essential.elementa.components.Window
97
import gg.essential.elementa.constraints.ChildBasedMaxSizeConstraint
108
import gg.essential.elementa.constraints.ChildBasedSizeConstraint
119
import gg.essential.elementa.constraints.WidthConstraint
@@ -18,8 +16,6 @@ import gg.essential.elementa.common.HollowUIContainer
1816
import gg.essential.elementa.common.constraints.AlternateConstraint
1917
import gg.essential.elementa.common.constraints.SpacedCramSiblingConstraint
2018
import gg.essential.elementa.state.v2.*
21-
import gg.essential.universal.UMatrixStack
22-
import java.awt.Color
2319
import kotlin.contracts.ExperimentalContracts
2420
import kotlin.contracts.InvocationKind
2521
import kotlin.contracts.contract
@@ -168,80 +164,9 @@ fun LayoutScope.floatingBox(
168164
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
169165
}
170166

171-
fun UIComponent.isMounted(): Boolean =
172-
parent == this || (this in parent.children && parent.isMounted())
173-
174-
// Elementa's floating system is quite tricky to work with because components that are floating are added into a
175-
// persistent list but will not automatically be removed from that list when they're removed from the component
176-
// tree, and as such will continue to render.
177-
// This class tries to work around that by canceling `draw` and automatically un-floating itself in such cases,
178-
// as well as automatically adding itself back to the floating list when it is put back into the component tree.
179-
class FloatableContainer : UIBlock(Color(0, 0, 0, 0)) {
180-
val shouldBeFloating: Boolean
181-
get() = floating.get()
182-
183-
// Keeps track of the current floating state because the parent field of the same name is private
184-
@set:JvmName("setFloating_")
185-
var isFloating: Boolean = false
186-
set(value) {
187-
if (field == value) return
188-
field = value
189-
setFloating(value)
190-
}
191-
192-
override fun animationFrame() {
193-
// animationFrame is called from the regular tree traversal, so it's safe to directly update the floating
194-
// list from here
195-
isFloating = shouldBeFloating
196-
197-
super.animationFrame()
198-
}
199-
200-
override fun draw(matrixStack: UMatrixStack) {
201-
// If we're no longer mounted in the component tree, we should no longer draw
202-
if (!isMounted()) {
203-
// and if we're still floating (likely the case because that'll be why we're still drawing), then
204-
// we also need to un-float ourselves
205-
if (isFloating) {
206-
// since this is likely called from the code that iterates over the floating list to draw each
207-
// component, modifying the floating list here would result in a CME, so we need to delay this.
208-
Window.enqueueRenderOperation {
209-
// Note: we must not assume that our shouldBe state hasn't changed since we scheduled this
210-
isFloating = shouldBeFloating && isMounted()
211-
}
212-
}
213-
return
214-
}
215-
216-
// If we should be floating but aren't right now, then this isn't being called from the floating draw loop
217-
// and it should be safe for us to immediately set us as floating.
218-
// Doing so will add us to the floating draw loop and thereby allow us to draw later.
219-
if (shouldBeFloating && !isFloating) {
220-
isFloating = true
221-
return
222-
}
223-
224-
// If we should not be floating but are right now, then this is similar to the no-longer-mounted case above
225-
// i.e. we want to un-float ourselves.
226-
// Except we're still mounted so we do still want to draw the content (this means it'll be floating for one
227-
// more frame than it's supposed to but there isn't anything we can really do about that because the regular
228-
// draw loop has already concluded by this point).
229-
if (!shouldBeFloating && isFloating) {
230-
Window.enqueueRenderOperation { isFloating = shouldBeFloating }
231-
super.draw(matrixStack)
232-
return
233-
}
234-
235-
// All as it should be, can just draw it
236-
super.draw(matrixStack)
237-
}
238-
}
239-
240-
val container = FloatableContainer().apply {
241-
componentName = "floatingBox"
242-
setWidth(ChildBasedSizeConstraint())
243-
setHeight(ChildBasedSizeConstraint())
167+
val box = box(modifier, block)
168+
effect(box) {
169+
box.isFloating = floating()
244170
}
245-
container.addChildModifier(Modifier.alignBoth(Alignment.Center))
246-
return container(modifier = modifier, block = block)
171+
return box
247172
}

0 commit comments

Comments
 (0)