commonMain.io.nacular.doodle.layout.ConstraintLayout.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core-jvm Show documentation
Show all versions of core-jvm Show documentation
A pure Kotlin, UI framework for the Web and Desktop
@file:Suppress("NestedLambdaShadowedImplicitParameter")
package io.nacular.doodle.layout
import io.nacular.doodle.core.Display
import io.nacular.doodle.core.Layout
import io.nacular.doodle.core.PositionableContainer
import io.nacular.doodle.core.View
import io.nacular.doodle.geometry.Rectangle
import kotlin.math.max
import kotlin.math.min
/**
* Created by Nicholas Eddy on 11/1/17.
*/
public val center : (Constraints.() -> Unit) = { center = parent.center }
public val fill : (Constraints.() -> Unit) = { top = parent.top; left = parent.left; width = parent.width; height = parent.height }
public fun fill(insets: Insets): (Constraints.() -> Unit) = {
top = parent.top + insets.top
left = parent.left + insets.left
right = parent.right - insets.right
bottom = parent.bottom - insets.bottom
}
public abstract class ConstraintLayout: Layout {
public abstract fun constrain(a: View, block: ConstraintBlockContext.(Constraints ) -> Unit): ConstraintLayout
public abstract fun constrain(a: View, b: View, block: ConstraintBlockContext.(Constraints, Constraints ) -> Unit): ConstraintLayout
public abstract fun constrain(a: View, b: View, c: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints ) -> Unit): ConstraintLayout
public abstract fun constrain(a: View, b: View, c: View, d: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints, Constraints ) -> Unit): ConstraintLayout
public abstract fun constrain(a: View, b: View, c: View, d: View, e: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints, Constraints, Constraints) -> Unit): ConstraintLayout
public abstract fun unconstrain(vararg views: View): ConstraintLayout
}
private class ConstraintLayoutImpl(vararg constraints: ConstraintsImpl): ConstraintLayout() {
private val constraints by lazy { constraints.fold(mutableMapOf()) { s, r -> s[r.target] = r; s } }
private val processed by lazy { mutableSetOf() }
private val processing by lazy { mutableSetOf() }
private var displayConstraints: DisplayConstraints? = null
fun constrain(view: View, within: Rectangle, block: ConstraintBlockContext.(Constraints) -> Unit) {
constraints(view, within).apply { block(ConstraintBlockContextImpl(parent), this) }
constraints[view]?.let { layoutChild(view, it) }
}
override fun constrain(a: View, block: ConstraintBlockContext.(Constraints) -> Unit): ConstraintLayout {
constraints(a).let { (a) -> block(ConstraintBlockContextImpl(a.parent), a)
return this
}
}
override fun constrain(a: View, b: View, block: ConstraintBlockContext.(Constraints, Constraints) -> Unit): ConstraintLayout {
constraints(a, b).let { (a, b) -> block(ConstraintBlockContextImpl(a.parent), a, b)
return this
}
}
override fun constrain(a: View, b: View, c: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints) -> Unit): ConstraintLayout {
constraints(a, b, c).let { (a, b, c) -> block(ConstraintBlockContextImpl(a.parent), a, b, c)
return this
}
}
override fun constrain(a: View, b: View, c: View, d: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints, Constraints) -> Unit): ConstraintLayout {
constraints(a, b, c, d).let { (a, b, c, d) -> block(ConstraintBlockContextImpl(a.parent), a, b, c, d)
return this
}
}
override fun constrain(a: View, b: View, c: View, d: View, e: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints, Constraints, Constraints) -> Unit): ConstraintLayout {
constraints(a, b, c, d, e).let { (a, b, c, d, e) -> block(ConstraintBlockContextImpl(a.parent), a, b, c, d, e)
return this
}
}
override fun unconstrain(vararg views: View): ConstraintLayout {
views.forEach { constraints.remove(it) }
return this
}
private fun process(constraint: Constraint): Double? {
if (constraint.default) {
return null
}
constraint.dependencies.forEach { child ->
if (child !in processed) {
constraints[child]?.let { layoutChild(child, it) }
}
}
return constraint()
}
private fun layoutChild(child: View, constraints: Constraints) {
if (child in processing) {
throw Exception("Circular dependency")
}
processing += child
var top = process(constraints.top )
var height = process(constraints.height )
val middle = process(constraints.centerY)
val bottom = process(constraints.bottom )
top = top ?: when {
middle != null && height != null -> middle - height / 2
middle != null && bottom != null -> bottom - (bottom - middle) * 2
height != null && bottom != null -> bottom - height
middle != null -> middle - child.height / 2
bottom != null -> bottom - child.height
else -> child.y
}
height = height ?: when {
middle != null -> (middle - top) * 2
bottom != null -> bottom - top
else -> child.height
}
// TODO: Fully handle width/height
var left = process(constraints.left )
var width = process(constraints.width )
val center = process(constraints.centerX)
val right = process(constraints.right )
left = left ?: when {
center != null && width != null -> center - width / 2
center != null && right != null -> right - (right - center) * 2
width != null && right != null -> right - width
center != null -> center - child.width / 2
right != null -> right - child.width
else -> child.x
}
width = width ?: when {
center != null -> (center - left) * 2
right != null -> right - left
else -> child.width
}
child.bounds = Rectangle(left, top, max(0.0, width), max(0.0, height))
processing -= child
processed += child
}
// FIXME: Gracefully handle circular dependencies
override fun layout(container: PositionableContainer) {
processed.clear ()
processing.clear()
constraints.filter { it.key in container.children && it.key !in processed }.forEach { (child, constraints) ->
layoutChild(child, constraints)
}
}
private fun constraints(view: View, within: Rectangle): Constraints {
val parent = RectangleConstraints(within)
return constraints.getOrPut(view) { ConstraintsImpl(view, parent) }
}
private fun constraints(child: View, vararg others: View): List {
child.parent?.let {
val parent = ParentConstraintsImpl(it)
val children = arrayOf(child) + others
val constraints = children.filter { it.parent == parent.target }.map {
it.parentChange += parentChanged_
constraints.getOrPut(it) { ConstraintsImpl(it, parent) }
}
if (constraints.size != children.size) {
throw Exception("Must all share same parent")
}
return constraints
} ?: child.display?.let { display ->
val parent = displayConstraints ?: DisplayConstraints(display).also { displayConstraints = it }
val children = arrayOf(child) + others
val constraints = children.filter { it.parent == null && it.displayed }.map {
it.parentChange += parentChanged_
constraints.getOrPut(it) { ConstraintsImpl(it, parent) }
}
if (constraints.size != children.size) {
throw Exception("Must all be displayed")
}
return constraints
} ?: throw Exception("Must all share same parent")
}
private val parentChanged_ = ::parentChanged
@Suppress("UNUSED_PARAMETER")
private fun parentChanged(child: View, old: View?, new: View?) {
constraints.remove(child)
child.parentChange -= parentChanged_
}
}
public open class Constraint(internal val target: View, dependencies: Set = emptySet(), internal val default: Boolean = true, internal var block: (View) -> Double) {
internal val dependencies by lazy { mutableSetOf(target) + dependencies }
internal operator fun invoke() = block(target)
}
public class VerticalConstraint(target: View, dependencies: Set = emptySet(), default: Boolean = true, block: (View) -> Double): Constraint(target, dependencies, default, block) {
public operator fun plus(value: Number): VerticalConstraint = plus { value }
public operator fun plus(value: () -> Number): VerticalConstraint = VerticalConstraint(target, dependencies) {
block(it) + value().toDouble()
}
public operator fun plus(value: MagnitudeConstraint): VerticalConstraint = VerticalConstraint(target, dependencies + value.dependencies) {
block(it) + value()
}
public operator fun plus(value: VerticalConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies) {
block(it) + value()
}
public operator fun minus(value: Number): VerticalConstraint = minus { value }
public operator fun minus(value: () -> Number): VerticalConstraint = VerticalConstraint(target, dependencies) {
block(it) - value().toDouble()
}
public operator fun minus(value: VerticalConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies) {
block(it) - value()
}
public operator fun minus(value: MagnitudeConstraint): VerticalConstraint = VerticalConstraint(target, dependencies + value.dependencies) {
block(it) - value()
}
public operator fun times(value: Number): VerticalConstraint = times { value }
public operator fun times(value: () -> Number): VerticalConstraint = VerticalConstraint(target, dependencies) {
block(it) * value().toDouble()
}
public operator fun div(value: Number): VerticalConstraint = div { value }
public operator fun div(value: () -> Number): VerticalConstraint = VerticalConstraint(target, dependencies) {
block(it) / value().toDouble()
}
public operator fun times(value: MagnitudeConstraint): VerticalConstraint = VerticalConstraint(target, dependencies + value.dependencies) {
block(it) * value()
}
public operator fun div(value: MagnitudeConstraint): VerticalConstraint = VerticalConstraint(target, dependencies + value.dependencies) {
block(it) / value()
}
// override fun toString(): String = "V ($default) <- $dependencies"
}
public class HorizontalConstraint(target: View, dependencies: Set = emptySet(), default: Boolean = true, block: (View) -> Double): Constraint(target, dependencies, default, block) {
public operator fun plus(value: Number): HorizontalConstraint = plus { value }
public operator fun plus(value: () -> Number): HorizontalConstraint = HorizontalConstraint(target, dependencies) {
block(it) + value().toDouble()
}
public operator fun plus(value: MagnitudeConstraint): HorizontalConstraint = HorizontalConstraint(target, dependencies + value.dependencies) {
block(it) + value()
}
public operator fun plus(value: HorizontalConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies) {
block(it) + value()
}
public operator fun minus(value: Number): HorizontalConstraint = minus { value }
public operator fun minus(value: () -> Number): HorizontalConstraint = HorizontalConstraint(target, dependencies) {
block(it) - value().toDouble()
}
public operator fun minus(value: MagnitudeConstraint): HorizontalConstraint = HorizontalConstraint(target, dependencies + value.dependencies) {
block(it) - value()
}
public operator fun minus(value: HorizontalConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies) {
block(it) - value()
}
public operator fun times(value: Number): HorizontalConstraint = times { value }
public operator fun times(value: () -> Number): HorizontalConstraint = HorizontalConstraint(target, dependencies) {
block(it) * value().toDouble()
}
public operator fun div(value: Number): HorizontalConstraint = div { value }
public operator fun div(value: () -> Number): HorizontalConstraint = HorizontalConstraint(target, dependencies) {
block(it) / value().toDouble()
}
public operator fun times(value: MagnitudeConstraint): HorizontalConstraint = HorizontalConstraint(target, dependencies + value.dependencies) {
block(it) * value()
}
public operator fun div(value: MagnitudeConstraint): HorizontalConstraint = HorizontalConstraint(target, dependencies + value.dependencies) {
block(it) / value()
}
// override fun toString(): String = "H ($default) <- $dependencies"
}
private object IgnoreTarget: View()
public fun constant(value: Double): MagnitudeConstraint = MagnitudeConstraint(IgnoreTarget, block = { value })
public interface Nullable {
public infix fun or(other: T): T = other
}
private open class NullableMagnitudeConstraint(private val target: View, private val dependencies: Set = emptySet(), private val default: Boolean = true, private val optionalBlock: (View) -> Double?): Nullable {
override infix fun or(other: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies, default) {
optionalBlock(it) ?: other()
}
}
public open class MagnitudeConstraint(target: View, dependencies: Set = emptySet(), default: Boolean = true, block: (View) -> Double): Constraint(target, dependencies, default, block) {
public operator fun plus(value: Number): MagnitudeConstraint = plus { value }
public operator fun plus(value: () -> Number): MagnitudeConstraint = MagnitudeConstraint(target, dependencies) {
block(it) + value().toDouble()
}
public operator fun plus(value: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies){
block(it) + value()
}
public operator fun minus(value: Number): MagnitudeConstraint = minus { value }
public operator fun minus(value: () -> Number): MagnitudeConstraint = MagnitudeConstraint(target, dependencies) {
block(it) - value().toDouble()
}
public operator fun minus(value: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies){
block(it) - value()
}
public operator fun times(value: Number): MagnitudeConstraint = times { value }
public operator fun times(value: () -> Number): MagnitudeConstraint = MagnitudeConstraint(target, dependencies) {
block(it) * value().toDouble()
}
public operator fun times(value: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies){
block(it) * value()
}
public operator fun div(value: Number): MagnitudeConstraint = div { value }
public operator fun div(value: () -> Number): MagnitudeConstraint = MagnitudeConstraint(target, dependencies) {
block(it) / value().toDouble()
}
public operator fun div(value: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(target, dependencies + value.dependencies){
block(it) / value()
}
// override fun toString(): String = "H ($default) <- $dependencies"
}
public interface ParentConstraints {
public val top : VerticalConstraint
public val centerY : VerticalConstraint
public val bottom : VerticalConstraint
public val height : MagnitudeConstraint
public val minHeight : MagnitudeConstraint get() = constant(0.0)
public val idealHeight: Nullable get() = object: Nullable {}
public val left : HorizontalConstraint
public val centerX : HorizontalConstraint
public val right : HorizontalConstraint
public val width : MagnitudeConstraint
public val minWidth : MagnitudeConstraint get() = constant(0.0)
public val idealWidth: Nullable get() = object: Nullable {}
public val center: Pair get() = centerX to centerY
}
public interface Constraints: ParentConstraints {
override var top : VerticalConstraint
override var centerY: VerticalConstraint
override var bottom : VerticalConstraint
override var height : MagnitudeConstraint
override var left : HorizontalConstraint
override var centerX: HorizontalConstraint
override var right : HorizontalConstraint
override var width : MagnitudeConstraint
override var center: Pair get() = centerX to centerY
set(value) {
centerX = value.first
centerY = value.second
}
public val parent: ParentConstraints
}
private open class ParentConstraintsImpl(val target: View): ParentConstraints {
override val top = VerticalConstraint (target ) { 0.0 }
override val centerY = VerticalConstraint (target ) { it.height / 2 }
override val bottom = VerticalConstraint (target ) { it.height }
override val height = MagnitudeConstraint(target ) { it.height }
override val minHeight = MagnitudeConstraint(IgnoreTarget) { target.minimumSize.height }
override val left = HorizontalConstraint(target ) { 0.0 }
override val centerX = HorizontalConstraint(target ) { it.width / 2 }
override val right = HorizontalConstraint(target ) { it.width }
override val width = MagnitudeConstraint (target ) { it.width }
override val minWidth = MagnitudeConstraint (IgnoreTarget) { target.minimumSize.width }
// override fun toString() = "P $target -> top: $top, left: $left, centerX: $centerX, centerY: $centerY, right: $right, bottom: $bottom"
}
private open class DisplayConstraints(private val display: Display): ParentConstraints {
val target = object: View() {}
init {
display.sizeChanged += { _,_,new ->
target.size = new
}
}
override val top = VerticalConstraint (target) { 0.0 }
override val left = HorizontalConstraint(target) { 0.0 }
override val centerY = VerticalConstraint (target) { display.size.height / 2 }
override val centerX = HorizontalConstraint(target) { display.size.width / 2 }
override val right = HorizontalConstraint(target) { display.size.width }
override val bottom = VerticalConstraint (target) { display.size.height }
override val width = MagnitudeConstraint (target) { display.size.width }
override val height = MagnitudeConstraint (target) { display.size.height }
// override fun toString() = "D $target -> top: $top, left: $left, centerX: $centerX, centerY: $centerY, right: $right, bottom: $bottom"
}
private open class RectangleConstraints(private val rectangle: Rectangle): ParentConstraints {
val target = object: View() {}
override val top = VerticalConstraint (target) { rectangle.y }
override val centerY = VerticalConstraint (target) { rectangle.center.y }
override val bottom = VerticalConstraint (target) { rectangle.bottom }
override val height = MagnitudeConstraint (target) { rectangle.height }
override val left = HorizontalConstraint(target) { rectangle.x }
override val centerX = HorizontalConstraint(target) { rectangle.center.x }
override val right = HorizontalConstraint(target) { rectangle.right }
override val width = MagnitudeConstraint (target) { rectangle.width }
}
private class ConstraintsImpl(target: View, override val parent: ParentConstraints): ParentConstraintsImpl(target), Constraints {
override var top = VerticalConstraint(target) { it.y }
set(new) { field = VerticalConstraint(new.target, new.dependencies, false, new.block) }
override var left = HorizontalConstraint(target) { it.x }
set(new) { field = HorizontalConstraint(new.target, new.dependencies, false, new.block) }
override var centerY = VerticalConstraint(target) { top() + it.height / 2 }
set(new) { field = VerticalConstraint(new.target, new.dependencies, false, new.block) }
override var centerX = HorizontalConstraint(target) { left() + it.width / 2 }
set(new) { field = HorizontalConstraint(new.target, new.dependencies, false, new.block) }
override var right = HorizontalConstraint(target) { left() + it.width }
set(new) { field = HorizontalConstraint(new.target, new.dependencies, false, new.block) }
override var bottom = VerticalConstraint(target) { top() + it.height }
set(new) { field = VerticalConstraint(new.target, new.dependencies, false, new.block) }
override var width = MagnitudeConstraint(target) { it.width }
set(new) { field = MagnitudeConstraint(new.target, new.dependencies, false, new.block) }
override var height = MagnitudeConstraint(target) { it.height }
set(new) { field = MagnitudeConstraint(new.target, new.dependencies, false, new.block) }
override val idealWidth = NullableMagnitudeConstraint(IgnoreTarget) { target.idealSize?.width }
override val idealHeight = NullableMagnitudeConstraint(IgnoreTarget) { target.idealSize?.height }
override val minWidth = MagnitudeConstraint(IgnoreTarget) { target.minimumSize.width }
override val minHeight = MagnitudeConstraint(IgnoreTarget) { target.minimumSize.height }
// override fun toString() = "C $target -> top: $top, left: $left, centerX: $centerX, centerY: $centerY, right: $right, bottom: $bottom"
}
public fun constrain(view: View, within: Rectangle, block: (Constraints) -> Unit) {
ConstraintLayoutImpl().let { layout ->
layout.constrain(view, within) { block(it) }
}
}
public interface ConstraintBlockContext {
public val parent: ParentConstraints
}
private class ConstraintBlockContextImpl(override val parent: ParentConstraints): ConstraintBlockContext
public fun constrain(a: View, block: ConstraintBlockContext.(Constraints ) -> Unit): ConstraintLayout = ConstraintLayoutImpl().also { it.constrain(a, block) }
public fun constrain(a: View, b: View, block: ConstraintBlockContext.(Constraints, Constraints ) -> Unit): ConstraintLayout = ConstraintLayoutImpl().also { it.constrain(a, b, block) }
public fun constrain(a: View, b: View, c: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints ) -> Unit): ConstraintLayout = ConstraintLayoutImpl().also { it.constrain(a, b, c, block) }
public fun constrain(a: View, b: View, c: View, d: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints, Constraints ) -> Unit): ConstraintLayout = ConstraintLayoutImpl().also { it.constrain(a, b, c, d, block) }
public fun constrain(a: View, b: View, c: View, d: View, e: View, block: ConstraintBlockContext.(Constraints, Constraints, Constraints, Constraints, Constraints) -> Unit): ConstraintLayout = ConstraintLayoutImpl().also { it.constrain(a, b, c, d, e, block) }
public fun max(a: Double, b: HorizontalConstraint): HorizontalConstraint = HorizontalConstraint(b.target, b.dependencies, false) { max(a, b.invoke()) }
public inline fun max(a: HorizontalConstraint, b: Double): HorizontalConstraint = max(b, a)
public fun max(a: HorizontalConstraint, b: HorizontalConstraint): HorizontalConstraint = HorizontalConstraint(b.target, b.dependencies, false) { max(a.invoke(), b.invoke()) }
public fun min(a: Double, b: HorizontalConstraint): HorizontalConstraint = HorizontalConstraint(b.target, b.dependencies, false) { min(a, b.invoke()) }
public inline fun min(a: HorizontalConstraint, b: Double): HorizontalConstraint = min(b, a)
public fun min(a: HorizontalConstraint, b: HorizontalConstraint): HorizontalConstraint = HorizontalConstraint(b.target, b.dependencies, false) { min(a.invoke(), b.invoke()) }
public fun max(a: Double, b: VerticalConstraint): VerticalConstraint = VerticalConstraint(b.target, b.dependencies, false) { max(a, b.invoke()) }
public inline fun max(a: VerticalConstraint, b: Double): VerticalConstraint = max(b, a)
public fun max(a: VerticalConstraint, b: VerticalConstraint): VerticalConstraint = VerticalConstraint(b.target, b.dependencies, false) { max(a.invoke(), b.invoke()) }
public fun min(a: Double, b: VerticalConstraint): VerticalConstraint = VerticalConstraint(b.target, b.dependencies, false) { min(a, b.invoke()) }
public inline fun min(a: VerticalConstraint, b: Double): VerticalConstraint = min(b, a)
public fun min(a: VerticalConstraint, b: VerticalConstraint): VerticalConstraint = VerticalConstraint(b.target, b.dependencies, false) { min(a.invoke(), b.invoke()) }
public fun max(a: Double, b: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(b.target, b.dependencies, false) { max(a, b.invoke()) }
public inline fun max(a: MagnitudeConstraint, b: Double): MagnitudeConstraint = max(b, a)
public fun max(a: MagnitudeConstraint, b: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(b.target, b.dependencies, false) { max(a.invoke(), b.invoke()) }
public fun min(a: Double, b: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(b.target, b.dependencies, false) { min(a, b.invoke()) }
public inline fun min(a: MagnitudeConstraint, b: Double): MagnitudeConstraint = min(b, a)
public fun min(a: MagnitudeConstraint, b: MagnitudeConstraint): MagnitudeConstraint = MagnitudeConstraint(b.target, b.dependencies, false) { min(a.invoke(), b.invoke()) }
© 2015 - 2025 Weber Informatics LLC | Privacy Policy