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())
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
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<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.
*/
@ -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,11 +208,13 @@ open class View {
open fun didLayout() {
subviews.forEach(View::didLayout)
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)
}
}
/**
* Called to draw this view.

View File

@ -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))

View File

@ -24,11 +24,7 @@ class ViewClickTests {
@Test
fun testClickInsideRootView() {
val mouse = CompletableFuture<Point>()
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<Boolean>()
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<Point>()
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<Boolean>()
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)
}

View File

@ -32,12 +32,7 @@ class ViewHoverTests {
@Test
fun testHoverRootView() {
val point = CompletableFuture<Point>()
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<Point>()
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)
}