Add frame based layout

This commit is contained in:
Shadowfacts 2019-06-23 16:53:25 -04:00
parent bee2eceb10
commit bf7e7bd24c
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
6 changed files with 82 additions and 105 deletions

View File

@ -9,4 +9,8 @@ data class Point(val x: Double, val y: Double) {
constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble()) constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble())
companion object {
val ORIGIN = Point(0.0, 0.0)
}
} }

View File

@ -0,0 +1,21 @@
package net.shadowfacts.cacao.util
import kotlin.reflect.KProperty
/**
* @author shadowfacts
*/
class ObservableLateInitProperty<T: Any>(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)
}
}

View File

@ -1,13 +1,9 @@
package net.shadowfacts.cacao.view package net.shadowfacts.cacao.view
import com.mojang.blaze3d.platform.GlStateManager
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.geometry.* import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.Color import net.shadowfacts.cacao.util.*
import net.shadowfacts.cacao.util.LowestCommonAncestor
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import no.birkett.kiwi.Constraint import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver import no.birkett.kiwi.Solver
import java.util.* import java.util.*
@ -18,7 +14,7 @@ import java.util.*
* *
* @author shadowfacts * @author shadowfacts
*/ */
open class View { open class View() {
/** /**
* The constraint solver used by the [net.shadowfacts.cacao.Window] this view belongs to. * 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") 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). * Whether this view uses constraint-based layout.
* Not initialized until [didLayout] called. * 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. * 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 lateinit var bounds: Rect
@ -102,6 +110,11 @@ open class View {
*/ */
val subviews: List<View> = _subviews val subviews: List<View> = _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. * 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. * If overridden, the super-class method must be called.
*/ */
open fun createInternalConstraints() { open fun createInternalConstraints() {
if (!usesConstraintBasedLayout) return
solver.dsl { solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor) rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor) bottomAnchor equalTo (topAnchor + heightAnchor)
@ -172,7 +187,7 @@ open class View {
} }
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) { private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!this::solver.isInitialized) return if (!usesConstraintBasedLayout || !this::solver.isInitialized) return
if (old != null) { if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!) solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
@ -193,10 +208,12 @@ open class View {
open fun didLayout() { open fun didLayout() {
subviews.forEach(View::didLayout) subviews.forEach(View::didLayout)
val superviewLeft = superview?.leftAnchor?.value ?: 0.0 if (usesConstraintBasedLayout) {
val superviewTop = superview?.topAnchor?.value ?: 0.0 val superviewLeft = superview?.leftAnchor?.value ?: 0.0
frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value) val superviewTop = superview?.topAnchor?.value ?: 0.0
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value) frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value)
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value)
}
} }
/** /**

View File

@ -21,12 +21,8 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertToParent() { fun testConvertToParent() {
val a = window.addView(View().apply { val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val b = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
})
val b = a.addSubview(View().apply {
frame = 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(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)) assertEquals(Point(75.0, 75.0), b.convert(Point(50.0, 50.0), to = a))
@ -35,15 +31,9 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertToSibling() { fun testConvertToSibling() {
val root = window.addView(View().apply { val root = window.addView(View(Rect(0.0, 0.0, 200.0, 200.0)))
frame = 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)))
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)
})
assertEquals(Point(-50.0, -50.0), a.convert(Point(0.0, 0.0), to = b)) 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)) assertEquals(Point(100.0, 100.0), b.convert(Point(50.0, 50.0), to = a))
@ -52,21 +42,11 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertBetweenSubtrees() { fun testConvertBetweenSubtrees() {
val root = window.addView(View().apply { val root = window.addView(View(Rect(0.0, 0.0, 200.0, 100.0)))
frame = 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 a = root.addSubview(View().apply { val c = a.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val d = b.addSubview(View(Rect(0.0, 0.0, 50.0, 50.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)
})
assertEquals(Point(-100.0, 0.0), c.convert(Point(0.0, 0.0), to = b)) 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)) assertEquals(Point(-50.0, 50.0), c.convert(Point(50.0, 50.0), to = d))
@ -75,12 +55,8 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertBetweenTopLevelViews() { fun testConvertBetweenTopLevelViews() {
val a = window.addView(View().apply { val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val b = window.addView(View(Rect(100.0, 100.0, 100.0, 100.0)))
})
val b = window.addView(View().apply {
frame = 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(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)) assertEquals(Point(200.0, 200.0), b.convert(Point(100.0, 100.0), to = a))
@ -89,18 +65,10 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertBetweenTopLevelSubtrees() { fun testConvertBetweenTopLevelSubtrees() {
val a = window.addView(View().apply { val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0)))
frame = 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 b = window.addView(View().apply { val d = b.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
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)
})
assertEquals(Point(-50.0, -50.0), c.convert(Point(50.0, 50.0), to = d)) 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)) assertEquals(Point(100.0, 100.0), d.convert(Point(0.0, 0.0), to = c))

View File

@ -24,11 +24,7 @@ class ViewClickTests {
@Test @Test
fun testClickInsideRootView() { fun testClickInsideRootView() {
val mouse = CompletableFuture<Point>() val mouse = CompletableFuture<Point>()
window.addView(object: View() { window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
mouse.complete(point) mouse.complete(point)
} }
@ -41,11 +37,7 @@ class ViewClickTests {
@Test @Test
fun testClickOutsideRootView() { fun testClickOutsideRootView() {
val clicked = CompletableFuture<Boolean>() val clicked = CompletableFuture<Boolean>()
window.addView(object: View() { window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
clicked.complete(true) clicked.complete(true)
} }
@ -58,14 +50,8 @@ class ViewClickTests {
@Test @Test
fun testClickInsideNestedView() { fun testClickInsideNestedView() {
val mouse = CompletableFuture<Point>() val mouse = CompletableFuture<Point>()
val root = window.addView(View().apply { val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0)))
frame = Rect(50.0, 50.0, 100.0, 100.0) root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
mouse.complete(point) mouse.complete(point)
} }
@ -78,14 +64,8 @@ class ViewClickTests {
@Test @Test
fun testClickOutsideNestedView() { fun testClickOutsideNestedView() {
val clicked = CompletableFuture<Boolean>() val clicked = CompletableFuture<Boolean>()
val root = window.addView(View().apply { val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0)))
frame = Rect(50.0, 50.0, 100.0, 100.0) root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
clicked.complete(true) clicked.complete(true)
} }

View File

@ -32,12 +32,7 @@ class ViewHoverTests {
@Test @Test
fun testHoverRootView() { fun testHoverRootView() {
val point = CompletableFuture<Point>() val point = CompletableFuture<Point>()
window.addView(object: View() { window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
bounds = Rect(0.0, 0.0, 100.0, 100.0)
}
override fun drawContent(mouse: Point, delta: Float) { override fun drawContent(mouse: Point, delta: Float) {
point.complete(mouse) point.complete(mouse)
} }
@ -50,16 +45,8 @@ class ViewHoverTests {
@Test @Test
fun testHoverNestedView() { fun testHoverNestedView() {
val point = CompletableFuture<Point>() val point = CompletableFuture<Point>()
val root = window.addView(View().apply { val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0)))
frame = Rect(50.0, 50.0, 100.0, 100.0) root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.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)
}
override fun drawContent(mouse: Point, delta: Float) { override fun drawContent(mouse: Point, delta: Float) {
point.complete(mouse) point.complete(mouse)
} }