From 0913358f2167f78587898615df685b478a5b7654 Mon Sep 17 00:00:00 2001 From: Carlos Ballesteros Velasco Date: Thu, 5 Oct 2023 14:21:58 +0200 Subject: [PATCH] Stack overflow on UITreeView (#1919) --- .../common/korlibs/image/vector/Context2d.kt | 4 +- .../korlibs/korge/ui/UIContainerLayouts.kt | 27 +++++--- .../src/common/korlibs/korge/ui/UITreeView.kt | 61 +++++++++++++++---- .../common/korlibs/korge/ui/UITreeViewTest.kt | 57 +++++++++++++++++ 4 files changed, 127 insertions(+), 22 deletions(-) create mode 100644 korge/test/common/korlibs/korge/ui/UITreeViewTest.kt diff --git a/korge-core/src/common/korlibs/image/vector/Context2d.kt b/korge-core/src/common/korlibs/image/vector/Context2d.kt index aff193a4e1..2100dfa98e 100644 --- a/korge-core/src/common/korlibs/image/vector/Context2d.kt +++ b/korge-core/src/common/korlibs/image/vector/Context2d.kt @@ -280,9 +280,9 @@ open class Context2d( inline fun skew(skewX: Angle = Angle.ZERO, skewY: Angle = Angle.ZERO, block: () -> Unit) = keep { skew(skewX, skewY).also { block() } } - inline fun scale(sx: Float, sy: Float = sx, block: () -> Unit) = keep { scale(sx, sy).also { block() } } + inline fun scale(sx: Number, sy: Number = sx, block: () -> Unit) = keep { scale(sx.toDouble(), sy.toDouble()).also { block() } } inline fun rotate(angle: Angle, block: () -> Unit) = keep { rotate(angle).also { block() } } - inline fun translate(tx: Float, ty: Float, block: () -> Unit) = keep { translate(tx, ty).also { block() } } + inline fun translate(tx: Number, ty: Number, block: () -> Unit) = keep { translate(tx.toDouble(), ty.toDouble()).also { block() } } fun skew(skewX: Angle = 0.degrees, skewY: Angle = 0.degrees) { state.transform = state.transform.preskewed(skewX, skewY) diff --git a/korge/src/common/korlibs/korge/ui/UIContainerLayouts.kt b/korge/src/common/korlibs/korge/ui/UIContainerLayouts.kt index cdd38c1dd1..a9000e626a 100644 --- a/korge/src/common/korlibs/korge/ui/UIContainerLayouts.kt +++ b/korge/src/common/korlibs/korge/ui/UIContainerLayouts.kt @@ -102,7 +102,7 @@ inline fun Container.uiContainer( ) = UIContainer(size).addTo(this).apply(block) open class UIContainer(size: Size) : UIBaseContainer(size) { - override fun relayout() {} + override fun relayoutInternal() {} } abstract class UIBaseContainer(size: Size) : UIView(size) { @@ -121,7 +121,18 @@ abstract class UIBaseContainer(size: Size) : UIView(size) { relayout() } - abstract fun relayout() + private var doingRelayout = false + fun relayout() { + if (doingRelayout) return + doingRelayout = true + try { + relayoutInternal() + } finally { + doingRelayout = false + } + } + + protected abstract fun relayoutInternal() var deferredRendering: Boolean? = true //var deferredRendering: Boolean? = false @@ -155,7 +166,7 @@ open class UIVerticalStack( } } - override fun relayout() { + override fun relayoutInternal() { var y = 0.0 var bb = BoundsBuilder() forEachChild { @@ -189,7 +200,7 @@ open class UIHorizontalStack( } } - override fun relayout() { + override fun relayoutInternal() { var x = 0.0 var bb = BoundsBuilder() forEachChild { @@ -219,7 +230,7 @@ inline fun Container.uiHorizontalFill( ) = UIHorizontalFill(size).addTo(this).apply(block) open class UIHorizontalFill(size: Size = Size(128, 20)) : UIContainer(size) { - override fun relayout() { + override fun relayoutInternal() { var x = 0.0 val elementWidth = width / numChildren forEachChild { @@ -237,7 +248,7 @@ inline fun Container.uiVerticalFill( ) = UIVerticalFill(size).addTo(this).apply(block) open class UIVerticalFill(size: Size = Size(128, 128)) : UIContainer(size) { - override fun relayout() { + override fun relayoutInternal() { var y = 0.0 val elementHeight = height / numChildren forEachChild { @@ -273,7 +284,7 @@ open class UIGridFill( @ViewProperty var direction: UIDirection by UIObservable(direction) { relayout() } - override fun relayout() { + override fun relayoutInternal() { val width = width val height = height val paddingH = spacing.horizontal @@ -310,7 +321,7 @@ inline fun Container.uiFillLayeredContainer( ) = UIFillLayeredContainer(size).addTo(this).apply(block) open class UIFillLayeredContainer(size: Size = Size(128, 20)) : UIContainer(size) { - override fun relayout() { + override fun relayoutInternal() { val width = this.width val height = this.height forEachChild { diff --git a/korge/src/common/korlibs/korge/ui/UITreeView.kt b/korge/src/common/korlibs/korge/ui/UITreeView.kt index 7c9d7d03a9..b483dac653 100644 --- a/korge/src/common/korlibs/korge/ui/UITreeView.kt +++ b/korge/src/common/korlibs/korge/ui/UITreeView.kt @@ -1,11 +1,16 @@ package korlibs.korge.ui +import korlibs.datastructure.* import korlibs.datastructure.iterators.* +import korlibs.image.bitmap.* import korlibs.image.color.* +import korlibs.korge.animate.* import korlibs.korge.annotations.* import korlibs.korge.input.* +import korlibs.korge.tween.* import korlibs.korge.view.* import korlibs.math.geom.* +import kotlin.time.Duration.Companion.seconds @KorgeExperimental class UITreeViewNode(val element: T, val items: List> = emptyList()) { @@ -64,7 +69,8 @@ interface UITreeViewProvider { @KorgeExperimental private class UITreeViewVerticalListProviderAdapter(val provider: UITreeViewProvider) : UIVerticalList.Provider { - class Node(val value: T, val localIndex: Int, val indentation: Int) { + class Node(val value: T, val localIndex: Int, val indentation: Int, val parent: Node?) { + val path: List> = (parent?.path ?: emptyList()) + listOf(this) var opened: Boolean = false var openCount: Int = 0 } @@ -76,27 +82,58 @@ private class UITreeViewVerticalListProviderAdapter(val provider: UITreeViewP override val numItems: Int get() = items.size override val fixedHeight: Double get() = provider.height + companion object { + val ICON_DOWN = NativeImageContext2d(10, 10) { + val sw = 4.0 + val sh = 4.0 + translate(width * 0.5, height * 0.5 + sh * 0.5) { + stroke(Colors.WHITE, 2.0) { + moveTo(-sw, +sh) + lineTo(0.0, -sh) + lineTo(+sw, +sh) + } + } + } + } + override fun getItemHeight(index: Int): Double = fixedHeight override fun getItemView(index: Int, vlist: UIVerticalList): View { + val node = items[index] + val itemViews = vlist.extraCache("itemViewsCache") { LinkedHashMap>, View>() } + return itemViews.getOrPut(node.path) { + createItemView(index, vlist) + } + } + fun createItemView(index: Int, vlist: UIVerticalList): View { + //println("Creating new createItemView index=$index") val node = items[index] val childCount = provider.getNumChildren(node.value) val container = UIFillLayeredContainer() val background = container.solidRect(10, 10, Colors.TRANSPARENT) - val stack = container.uiHorizontalStack(padding = 2.0) + val stack = container.uiHorizontalStack(padding = 4.0) val child = provider.getViewForNode(node.value) stack.solidRect(10 * node.indentation, 10, Colors.TRANSPARENT) - val rect = stack.solidRect(10, 10, Colors.TRANSPARENT) - fun updateIcon() { - rect.color = when { + val imageContainer = stack.fixedSizeContainer(Size(10, 10)) + val icon = imageContainer.image(ICON_DOWN).centered.xy((ICON_DOWN.size * 0.5).toDouble().toVector()) + fun updateIcon(animated: Boolean) { + val isOpen: Boolean? = when { childCount > 0 -> { when { - node.opened -> Colors.GREEN - else -> Colors.RED + node.opened -> true + else -> false } } + else -> null + } - else -> Colors.TRANSPARENT + icon.visible = isOpen != null + val angle = if (isOpen == true) 90.degrees else 0.degrees + if (animated) { + icon.simpleAnimator.tween(icon::rotation[angle], time = 0.25.seconds) + } else { + icon.rotation = angle } + background.color = when { selectedNode == node -> Colors["#191034"] else -> Colors.TRANSPARENT @@ -111,13 +148,13 @@ private class UITreeViewVerticalListProviderAdapter(val provider: UITreeViewP selectedNode = node if (node.hasChildren()) { node.toggle() - updateIcon() + updateIcon(animated = true) vlist.invalidateList() } else { vlist.invalidateList() } } - updateIcon() + updateIcon(animated = false) return container } @@ -152,7 +189,7 @@ private class UITreeViewVerticalListProviderAdapter(val provider: UITreeViewP val nodeIndex = getNodeIndex(node) val children = provider.getChildrenList(node.value) node.openCount = children.size - items.addAll(nodeIndex + 1, children.mapIndexed { index, t -> Node(t, index, node.indentation + 1) }) + items.addAll(nodeIndex + 1, children.mapIndexed { index, t -> Node(t, index, node.indentation + 1, node) }) } fun toggleNode(node: Node) { @@ -162,7 +199,7 @@ private class UITreeViewVerticalListProviderAdapter(val provider: UITreeViewP fun init() { items.clear() provider.getChildrenList(null).fastForEachWithIndex { index, value -> - items.add(Node(value, index, 0)) + items.add(Node(value, index, 0, null)) } } } diff --git a/korge/test/common/korlibs/korge/ui/UITreeViewTest.kt b/korge/test/common/korlibs/korge/ui/UITreeViewTest.kt new file mode 100644 index 0000000000..880dd7c875 --- /dev/null +++ b/korge/test/common/korlibs/korge/ui/UITreeViewTest.kt @@ -0,0 +1,57 @@ +package korlibs.korge.ui + +import korlibs.korge.annotations.* +import korlibs.korge.tests.* +import kotlin.test.* + +@OptIn(KorgeExperimental::class) +class UITreeViewTest : ViewsForTesting() { + @Test + fun test() = viewsTest { + uiTooltipContainer { tooltips -> + uiTreeView( + UITreeViewList( + listOf( + UITreeViewNode("hello"), + UITreeViewNode( + "world", + UITreeViewNode("test"), + UITreeViewNode( + "demo", + UITreeViewNode("demo"), + UITreeViewNode("demo"), + UITreeViewNode( + "demo", + UITreeViewNode("demo") + ), + ), + ), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + UITreeViewNode("hello"), + ), height = 16.0, genView = { + UIText("$it").tooltip(tooltips, "Tooltip for $it") + }) + ) + } + } +}