From bf7e7bd24ccb402fcf9952a5825b310b614c8a27 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sun, 23 Jun 2019 16:53:25 -0400 Subject: [PATCH] Add frame based layout --- .../net/shadowfacts/cacao/geometry/Point.kt | 4 ++ .../cacao/util/ObservableLateInitProperty.kt | 21 ++++++ .../kotlin/net/shadowfacts/cacao/view/View.kt | 47 +++++++++----- .../cacao/CoordinateConversionTests.kt | 64 +++++-------------- .../shadowfacts/cacao/view/ViewClickTests.kt | 32 ++-------- .../shadowfacts/cacao/view/ViewHoverTests.kt | 19 +----- 6 files changed, 82 insertions(+), 105 deletions(-) create mode 100644 src/main/kotlin/net/shadowfacts/cacao/util/ObservableLateInitProperty.kt diff --git a/src/main/kotlin/net/shadowfacts/cacao/geometry/Point.kt b/src/main/kotlin/net/shadowfacts/cacao/geometry/Point.kt index 754e2f5..9058c49 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/geometry/Point.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/geometry/Point.kt @@ -9,4 +9,8 @@ data class Point(val x: Double, val y: Double) { constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble()) + companion object { + val ORIGIN = Point(0.0, 0.0) + } + } \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/util/ObservableLateInitProperty.kt b/src/main/kotlin/net/shadowfacts/cacao/util/ObservableLateInitProperty.kt new file mode 100644 index 0000000..d9246c4 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/cacao/util/ObservableLateInitProperty.kt @@ -0,0 +1,21 @@ +package net.shadowfacts.cacao.util + +import kotlin.reflect.KProperty + +/** + * @author shadowfacts + */ +class ObservableLateInitProperty(val observer: (T) -> Unit) { + + lateinit var storage: T + + operator fun getValue(thisRef: Any, property: KProperty<*>): T { + return storage + } + + operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) { + storage = value + observer(value) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt index 5dc650e..3b8a7ff 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt @@ -1,13 +1,9 @@ package net.shadowfacts.cacao.view -import com.mojang.blaze3d.platform.GlStateManager import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.geometry.* -import net.shadowfacts.cacao.util.Color -import net.shadowfacts.cacao.util.LowestCommonAncestor -import net.shadowfacts.cacao.util.MouseButton -import net.shadowfacts.cacao.util.RenderHelper +import net.shadowfacts.cacao.util.* import no.birkett.kiwi.Constraint import no.birkett.kiwi.Solver import java.util.* @@ -18,7 +14,7 @@ import java.util.* * * @author shadowfacts */ -open class View { +open class View() { /** * The constraint solver used by the [net.shadowfacts.cacao.Window] this view belongs to. @@ -60,13 +56,25 @@ open class View { val centerYAnchor = LayoutVariable(this, "centerY") /** - * The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview). - * Not initialized until [didLayout] called. + * Whether this view uses constraint-based layout. + * If `false`, the view's `frame` must be set manually and the layout anchors may not be used. + * Note: some views (such as [StackView] require arranged subviews to use constraint based layout. + * + * Default is `true`. */ - lateinit var frame: Rect + var usesConstraintBasedLayout = true + + /** + * The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview). + * If using constraint based layout, this property is not initialized until [didLayout] called. + * Otherwise, this must be set manually. + * Setting this property updates the [bounds]. + */ + var frame: Rect by ObservableLateInitProperty { this.bounds = Rect(Point.ORIGIN, it.size) } /** * The rectangle for this view in its own coordinate system. - * Not initialized until [didLayout] called. + * If using constraint based layout, this property is not initialized until [didLayout] called. + * Otherwise, this will be initialized when [frame] is set. */ lateinit var bounds: Rect @@ -102,6 +110,11 @@ open class View { */ val subviews: List = _subviews + constructor(frame: Rect): this() { + this.usesConstraintBasedLayout = false + this.frame = frame + } + /** * Helper method for retrieve the anchor for a specific position on the given axis. */ @@ -163,6 +176,8 @@ open class View { * If overridden, the super-class method must be called. */ open fun createInternalConstraints() { + if (!usesConstraintBasedLayout) return + solver.dsl { rightAnchor equalTo (leftAnchor + widthAnchor) bottomAnchor equalTo (topAnchor + heightAnchor) @@ -172,7 +187,7 @@ open class View { } private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) { - if (!this::solver.isInitialized) return + if (!usesConstraintBasedLayout || !this::solver.isInitialized) return if (old != null) { solver.removeConstraint(intrinsicContentSizeWidthConstraint!!) @@ -193,10 +208,12 @@ open class View { open fun didLayout() { subviews.forEach(View::didLayout) - val superviewLeft = superview?.leftAnchor?.value ?: 0.0 - val superviewTop = superview?.topAnchor?.value ?: 0.0 - frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value) - bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value) + if (usesConstraintBasedLayout) { + val superviewLeft = superview?.leftAnchor?.value ?: 0.0 + val superviewTop = superview?.topAnchor?.value ?: 0.0 + frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value) + bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value) + } } /** diff --git a/src/test/kotlin/net/shadowfacts/cacao/CoordinateConversionTests.kt b/src/test/kotlin/net/shadowfacts/cacao/CoordinateConversionTests.kt index 45bd5e7..41a0388 100644 --- a/src/test/kotlin/net/shadowfacts/cacao/CoordinateConversionTests.kt +++ b/src/test/kotlin/net/shadowfacts/cacao/CoordinateConversionTests.kt @@ -21,12 +21,8 @@ class CoordinateConversionTests { @Test fun testConvertToParent() { - val a = window.addView(View().apply { - frame = Rect(0.0, 0.0, 100.0, 100.0) - }) - val b = a.addSubview(View().apply { - frame = Rect(25.0, 25.0, 50.0, 50.0) - }) + val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0))) + val b = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0))) assertEquals(Point(25.0, 25.0), b.convert(Point(0.0, 0.0), to = a)) assertEquals(Point(75.0, 75.0), b.convert(Point(50.0, 50.0), to = a)) @@ -35,15 +31,9 @@ class CoordinateConversionTests { @Test fun testConvertToSibling() { - val root = window.addView(View().apply { - frame = Rect(0.0, 0.0, 200.0, 200.0) - }) - val a = root.addSubview(View().apply { - frame = Rect(25.0, 25.0, 50.0, 50.0) - }) - val b = root.addSubview(View().apply { - frame = Rect(75.0, 75.0, 50.0, 50.0) - }) + val root = window.addView(View(Rect(0.0, 0.0, 200.0, 200.0))) + val a = root.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0))) + val b = root.addSubview(View(Rect(75.0, 75.0, 50.0, 50.0))) assertEquals(Point(-50.0, -50.0), a.convert(Point(0.0, 0.0), to = b)) assertEquals(Point(100.0, 100.0), b.convert(Point(50.0, 50.0), to = a)) @@ -52,21 +42,11 @@ class CoordinateConversionTests { @Test fun testConvertBetweenSubtrees() { - val root = window.addView(View().apply { - frame = Rect(0.0, 0.0, 200.0, 100.0) - }) - val a = root.addSubview(View().apply { - frame = Rect(0.0, 0.0, 100.0, 100.0) - }) - val b = root.addSubview(View().apply { - frame = Rect(100.0, 0.0, 100.0, 100.0) - }) - val c = a.addSubview(View().apply { - frame = Rect(0.0, 0.0, 50.0, 50.0) - }) - val d = b.addSubview(View().apply { - frame = Rect(0.0, 0.0, 50.0, 50.0) - }) + val root = window.addView(View(Rect(0.0, 0.0, 200.0, 100.0))) + val a = root.addSubview(View(Rect(0.0, 0.0, 100.0, 100.0))) + val b = root.addSubview(View(Rect(100.0, 0.0, 100.0, 100.0))) + val c = a.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0))) + val d = b.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0))) assertEquals(Point(-100.0, 0.0), c.convert(Point(0.0, 0.0), to = b)) assertEquals(Point(-50.0, 50.0), c.convert(Point(50.0, 50.0), to = d)) @@ -75,12 +55,8 @@ class CoordinateConversionTests { @Test fun testConvertBetweenTopLevelViews() { - val a = window.addView(View().apply { - frame = Rect(0.0, 0.0, 100.0, 100.0) - }) - val b = window.addView(View().apply { - frame = Rect(100.0, 100.0, 100.0, 100.0) - }) + val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0))) + val b = window.addView(View(Rect(100.0, 100.0, 100.0, 100.0))) assertEquals(Point(0.0, 0.0), a.convert(Point(100.0, 100.0), to = b)) assertEquals(Point(200.0, 200.0), b.convert(Point(100.0, 100.0), to = a)) @@ -89,18 +65,10 @@ class CoordinateConversionTests { @Test fun testConvertBetweenTopLevelSubtrees() { - val a = window.addView(View().apply { - frame = Rect(0.0, 0.0, 100.0, 100.0) - }) - val b = window.addView(View().apply { - frame = Rect(100.0, 100.0, 100.0, 100.0) - }) - val c = a.addSubview(View().apply { - frame = Rect(25.0, 25.0, 50.0, 50.0) - }) - val d = b.addSubview(View().apply { - frame = Rect(25.0, 25.0, 50.0, 50.0) - }) + val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0))) + val b = window.addView(View(Rect(100.0, 100.0, 100.0, 100.0))) + val c = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0))) + val d = b.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0))) assertEquals(Point(-50.0, -50.0), c.convert(Point(50.0, 50.0), to = d)) assertEquals(Point(100.0, 100.0), d.convert(Point(0.0, 0.0), to = c)) diff --git a/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt b/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt index f035418..40b5844 100644 --- a/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt +++ b/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt @@ -24,11 +24,7 @@ class ViewClickTests { @Test fun testClickInsideRootView() { val mouse = CompletableFuture() - window.addView(object: View() { - init { - frame = Rect(50.0, 50.0, 100.0, 100.0) - } - + window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) { override fun mouseClicked(point: Point, mouseButton: MouseButton) { mouse.complete(point) } @@ -41,11 +37,7 @@ class ViewClickTests { @Test fun testClickOutsideRootView() { val clicked = CompletableFuture() - window.addView(object: View() { - init { - frame = Rect(50.0, 50.0, 100.0, 100.0) - } - + window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) { override fun mouseClicked(point: Point, mouseButton: MouseButton) { clicked.complete(true) } @@ -58,14 +50,8 @@ class ViewClickTests { @Test fun testClickInsideNestedView() { val mouse = CompletableFuture() - val root = window.addView(View().apply { - frame = Rect(50.0, 50.0, 100.0, 100.0) - }) - root.addSubview(object: View() { - init { - frame = Rect(25.0, 25.0, 50.0, 50.0) - } - + val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0))) + root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) { override fun mouseClicked(point: Point, mouseButton: MouseButton) { mouse.complete(point) } @@ -78,14 +64,8 @@ class ViewClickTests { @Test fun testClickOutsideNestedView() { val clicked = CompletableFuture() - val root = window.addView(View().apply { - frame = Rect(50.0, 50.0, 100.0, 100.0) - }) - root.addSubview(object: View() { - init { - frame = Rect(25.0, 25.0, 50.0, 50.0) - } - + val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0))) + root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) { override fun mouseClicked(point: Point, mouseButton: MouseButton) { clicked.complete(true) } diff --git a/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt b/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt index db9c370..1815198 100644 --- a/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt +++ b/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt @@ -32,12 +32,7 @@ class ViewHoverTests { @Test fun testHoverRootView() { val point = CompletableFuture() - window.addView(object: View() { - init { - frame = Rect(50.0, 50.0, 100.0, 100.0) - bounds = Rect(0.0, 0.0, 100.0, 100.0) - } - + window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) { override fun drawContent(mouse: Point, delta: Float) { point.complete(mouse) } @@ -50,16 +45,8 @@ class ViewHoverTests { @Test fun testHoverNestedView() { val point = CompletableFuture() - val root = window.addView(View().apply { - frame = Rect(50.0, 50.0, 100.0, 100.0) - bounds = Rect(0.0, 0.0, 100.0, 100.0) - }) - root.addSubview(object: View() { - init { - frame = Rect(25.0, 25.0, 50.0, 50.0) - bounds = Rect(0.0, 0.0, 50.0, 50.0) - } - + val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0))) + root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) { override fun drawContent(mouse: Point, delta: Float) { point.complete(mouse) }