Cacao: Add layout guides

This commit is contained in:
Shadowfacts 2021-03-20 11:40:00 -04:00
parent f375d157b0
commit f0fe1e4a3d
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
3 changed files with 90 additions and 4 deletions

View File

@ -1,5 +1,6 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.util.LayoutGuide
import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Variable
@ -9,10 +10,23 @@ import no.birkett.kiwi.Variable
*
* @author shadowfacts
*/
class LayoutVariable(val owner: View, val property: String): Variable("LayoutVariable") {
class LayoutVariable(
val view: View?,
val layoutGuide: LayoutGuide?,
val property: String,
): Variable("LayoutVariable") {
override fun getName() = "$owner.$property"
constructor(view: View, property: String): this(view, null, property)
constructor(layoutGuide: LayoutGuide, property: String): this(null, layoutGuide, property)
init {
if ((view == null) == (layoutGuide == null)) {
throw RuntimeException("LayoutVariable must be constructed with either a view or layout guide")
}
}
override fun getName() = "${view ?: layoutGuide}.$property"
override fun toString() = "LayoutVariable(name=$name, value=$value)"
}
}

View File

@ -0,0 +1,33 @@
package net.shadowfacts.cacao.util
import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.view.View
/**
* A layout guide is a non-view object that represents a rectangular area for the purposes of constraint-based layout.
* It allows you to define complex layouts without needing empty container views.
*
* A layout guide is always owned by a view. The owning view's dimensions are not necessarily tied to the layout guide's.
*
* To create a layout guide, call [View.addLayoutGuide] on the owning view.
*
* @author shadowfacts
*/
class LayoutGuide(
val owningView: View,
) {
val leftAnchor: LayoutVariable = LayoutVariable(this, "left")
val rightAnchor: LayoutVariable = LayoutVariable(this, "right")
val topAnchor: LayoutVariable = LayoutVariable(this, "top")
val bottomAnchor: LayoutVariable = LayoutVariable(this, "bottom")
val widthAnchor: LayoutVariable = LayoutVariable(this, "width")
val heightAnchor: LayoutVariable = LayoutVariable(this, "height")
val centerXAnchor: LayoutVariable = LayoutVariable(this, "centerX")
val centerYAnchor: LayoutVariable = LayoutVariable(this, "centerY")
val frame: Rect
get() = Rect(leftAnchor.value - owningView.leftAnchor.value, topAnchor.value - owningView.topAnchor.value, widthAnchor.value, heightAnchor.value)
}

View File

@ -84,6 +84,17 @@ open class View(): Responder {
*/
val centerYAnchor = LayoutVariable(this, "centerY")
private val _layoutGuides = LinkedList<LayoutGuide>()
/**
* All the layout guides attached to this view.
*
* To add a layout guide, call [addLayoutGuide].
*
* @see LayoutGuide
*/
val layoutGuides: List<LayoutGuide> = _layoutGuides
/**
* 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.
@ -229,7 +240,7 @@ open class View(): Responder {
for (b in a + 1 until variables.size) {
// if the variable views have no common ancestor after the removed view's superview is unset,
// the constraint crossed the this<->view boundary and should be removed
val ancestor = LowestCommonAncestor.find(variables[a].owner, variables[b].owner, View::superview)
val ancestor = LowestCommonAncestor.find(variables[a].viewOrLayoutGuideView, variables[b].viewOrLayoutGuideView, View::superview)
if (ancestor == null) {
return@filter true
}
@ -247,6 +258,18 @@ open class View(): Responder {
// view.wasRemoved()
}
/**
* Creates and returns a new layout guide with this view as its owner.
*/
fun addLayoutGuide(): LayoutGuide {
val guide = LayoutGuide(this)
_layoutGuides.add(guide)
if (hasSolver) {
guide.attachTo(solver)
}
return guide
}
/**
* Removes this view from its superview, if it has one.
*/
@ -283,6 +306,10 @@ open class View(): Responder {
open fun wasAdded() {
createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
layoutGuides.forEach {
it.attachTo(solver)
}
}
/**
@ -463,3 +490,15 @@ open class View(): Responder {
}
}
private fun LayoutGuide.attachTo(solver: Solver) {
solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
private val LayoutVariable.viewOrLayoutGuideView: View
get() = view ?: layoutGuide!!.owningView