diff --git a/src/main/kotlin/net/shadowfacts/cacao/LayoutVariable.kt b/src/main/kotlin/net/shadowfacts/cacao/LayoutVariable.kt index 3161853..3499002 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/LayoutVariable.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/LayoutVariable.kt @@ -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)" -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/shadowfacts/cacao/util/LayoutGuide.kt b/src/main/kotlin/net/shadowfacts/cacao/util/LayoutGuide.kt new file mode 100644 index 0000000..b4a6642 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/cacao/util/LayoutGuide.kt @@ -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) + +} diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt index 8db74f9..b07cebe 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt @@ -84,6 +84,17 @@ open class View(): Responder { */ val centerYAnchor = LayoutVariable(this, "centerY") + private val _layoutGuides = LinkedList() + + /** + * All the layout guides attached to this view. + * + * To add a layout guide, call [addLayoutGuide]. + * + * @see LayoutGuide + */ + val layoutGuides: List = _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