From c6aefdaad458afa597a85c4b2d75d1cafc4328d9 Mon Sep 17 00:00:00 2001 From: Shadowfacts Date: Sat, 22 Jun 2019 14:59:37 -0400 Subject: [PATCH] Add StackView --- .../kotlin/net/shadowfacts/asmr/TestScreen.kt | 92 ++++++--- .../net/shadowfacts/shadowui/geometry/Axis.kt | 14 ++ .../shadowui/geometry/AxisPosition.kt | 8 + .../shadowfacts/shadowui/view/StackView.kt | 86 ++++++++ .../net/shadowfacts/shadowui/view/View.kt | 22 ++ .../shadowui/view/StackViewTests.kt | 194 ++++++++++++++++++ 6 files changed, 383 insertions(+), 33 deletions(-) create mode 100644 src/main/kotlin/net/shadowfacts/shadowui/geometry/Axis.kt create mode 100644 src/main/kotlin/net/shadowfacts/shadowui/geometry/AxisPosition.kt create mode 100644 src/main/kotlin/net/shadowfacts/shadowui/view/StackView.kt create mode 100644 src/test/kotlin/net/shadowfacts/shadowui/view/StackViewTests.kt diff --git a/src/main/kotlin/net/shadowfacts/asmr/TestScreen.kt b/src/main/kotlin/net/shadowfacts/asmr/TestScreen.kt index 1ffb9d6..b0a0383 100644 --- a/src/main/kotlin/net/shadowfacts/asmr/TestScreen.kt +++ b/src/main/kotlin/net/shadowfacts/asmr/TestScreen.kt @@ -4,9 +4,10 @@ import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.shadowui.Screen import net.shadowfacts.shadowui.view.View import net.shadowfacts.shadowui.Window +import net.shadowfacts.shadowui.geometry.Axis import net.shadowfacts.shadowui.geometry.Size import net.shadowfacts.shadowui.util.Color -import net.shadowfacts.shadowui.view.Label +import net.shadowfacts.shadowui.view.StackView /** * @author shadowfacts @@ -15,49 +16,74 @@ class TestScreen: Screen() { init { windows.add(Window().apply { - val red = addView(View().apply { + val stack = addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER).apply { + backgroundColor = Color.WHITE + }) + val red = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) backgroundColor = Color(0xff0000) }) - val green = addView(View().apply { + val green = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 100.0) backgroundColor = Color(0x00ff00) }) - val blue = addView(View().apply { + val blue = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) backgroundColor = Color(0x0000ff) }) - val purple = green.addSubview(View().apply { - backgroundColor = Color(0x800080) - }) - purple.intrinsicContentSize = Size(width = 150.0, height = 150.0) - val label = purple.addSubview(Label("Hello, world!").apply { - textColor = Color.WHITE - }) solver.dsl { - red.leftAnchor equalTo 0 - red.widthAnchor equalTo 200 - red.topAnchor equalTo 0 - red.heightAnchor equalTo 100 - - green.leftAnchor equalTo (red.leftAnchor + red.widthAnchor + 20) - green.widthAnchor equalTo red.widthAnchor - green.topAnchor equalTo 0 - green.heightAnchor equalTo (red.heightAnchor + 100) - - blue.leftAnchor equalTo green.leftAnchor - blue.widthAnchor equalTo green.widthAnchor - blue.topAnchor equalTo (green.topAnchor + green.heightAnchor) - blue.heightAnchor equalTo 50 - -// purple.widthAnchor equalTo 100 -// purple.heightAnchor equalTo 100 - purple.centerXAnchor equalTo green.centerXAnchor - purple.centerYAnchor equalTo green.centerYAnchor - - label.centerXAnchor equalTo purple.centerXAnchor - label.centerYAnchor equalTo purple.centerYAnchor + stack.topAnchor equalTo 50 + stack.leftAnchor equalTo 50 + stack.rightAnchor equalTo 150 } layout() + + +// val red = addView(View().apply { +// backgroundColor = Color(0xff0000) +// }) +// val green = addView(View().apply { +// backgroundColor = Color(0x00ff00) +// }) +// val blue = addView(View().apply { +// backgroundColor = Color(0x0000ff) +// }) +// val purple = green.addSubview(View().apply { +// backgroundColor = Color(0x800080) +// }) +// purple.intrinsicContentSize = Size(width = 150.0, height = 150.0) +// val label = purple.addSubview(Label("Hello, world!").apply { +// textColor = Color.WHITE +// }) +// +// solver.dsl { +// red.leftAnchor equalTo 0 +// red.widthAnchor equalTo 200 +// red.topAnchor equalTo 0 +// red.heightAnchor equalTo 100 +// +// green.leftAnchor equalTo (red.leftAnchor + red.widthAnchor + 20) +// green.widthAnchor equalTo red.widthAnchor +// green.topAnchor equalTo 0 +// green.heightAnchor equalTo (red.heightAnchor + 100) +// +// blue.leftAnchor equalTo green.leftAnchor +// blue.widthAnchor equalTo green.widthAnchor +// blue.topAnchor equalTo (green.topAnchor + green.heightAnchor) +// blue.heightAnchor equalTo 50 +// +//// purple.widthAnchor equalTo 100 +//// purple.heightAnchor equalTo 100 +// purple.centerXAnchor equalTo green.centerXAnchor +// purple.centerYAnchor equalTo green.centerYAnchor +// +// label.centerXAnchor equalTo purple.centerXAnchor +// label.centerYAnchor equalTo purple.centerYAnchor +// } +// +// layout() }) } diff --git a/src/main/kotlin/net/shadowfacts/shadowui/geometry/Axis.kt b/src/main/kotlin/net/shadowfacts/shadowui/geometry/Axis.kt new file mode 100644 index 0000000..928e9f9 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/shadowui/geometry/Axis.kt @@ -0,0 +1,14 @@ +package net.shadowfacts.shadowui.geometry + +/** + * @author shadowfacts + */ +enum class Axis { + HORIZONTAL, VERTICAL; + + val other: Axis + get() = when (this) { + HORIZONTAL -> VERTICAL + VERTICAL -> HORIZONTAL + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/shadowui/geometry/AxisPosition.kt b/src/main/kotlin/net/shadowfacts/shadowui/geometry/AxisPosition.kt new file mode 100644 index 0000000..aaabf86 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/shadowui/geometry/AxisPosition.kt @@ -0,0 +1,8 @@ +package net.shadowfacts.shadowui.geometry + +/** + * @author shadowfacts + */ +enum class AxisPosition { + LEADING, CENTER, TRAILING; +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/shadowui/view/StackView.kt b/src/main/kotlin/net/shadowfacts/shadowui/view/StackView.kt new file mode 100644 index 0000000..5bc107c --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/shadowui/view/StackView.kt @@ -0,0 +1,86 @@ +package net.shadowfacts.shadowui.view + +import net.shadowfacts.kiwidsl.dsl +import net.shadowfacts.shadowui.LayoutVariable +import net.shadowfacts.shadowui.geometry.Axis +import net.shadowfacts.shadowui.geometry.AxisPosition +import net.shadowfacts.shadowui.geometry.AxisPosition.* +import no.birkett.kiwi.Constraint + +/** + * @author shadowfacts + */ +class StackView(val axis: Axis, val distribution: Distribution = Distribution.FILL): View() { + + val arrangedSubviews = mutableListOf() + + private var leadingConnection: Constraint? = null + private var trailingConnection: Constraint? = null + private var arrangedSubviewConnections = mutableListOf() + + fun addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T { + addSubview(view) + + arrangedSubviews.add(index, view) + + addConstraintsForArrangedView(view, index) + + return view + } + + fun addConstraintsForArrangedView(view: View, index: Int) { + if (index == 0) { + if (leadingConnection != null) { + solver.removeConstraint(leadingConnection) + } + solver.dsl { + leadingConnection = anchor(LEADING) equalTo anchor(LEADING, view) + } + } + if (index == arrangedSubviews.size - 1) { + if (trailingConnection != null) { + solver.removeConstraint(trailingConnection) + } + solver.dsl { + trailingConnection = anchor(TRAILING, view) equalTo anchor(TRAILING) + } + } + if (arrangedSubviews.size > 1) { + solver.dsl { + val previous = arrangedSubviews.getOrNull(index - 1) + val next = arrangedSubviews.getOrNull(index + 1) + if (next != null) { + arrangedSubviewConnections.add(index, anchor(TRAILING, view) equalTo anchor(LEADING, next)) + } + if (previous != null) { + arrangedSubviewConnections.add(index - 1, anchor(TRAILING, previous) equalTo anchor(LEADING, view)) + } + } + } + solver.dsl { + when (distribution) { + Distribution.LEADING -> + perpAnchor(LEADING) equalTo perpAnchor(LEADING, view) + Distribution.TRAILING -> + perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view) + Distribution.FILL -> { + perpAnchor(LEADING) equalTo perpAnchor(LEADING, view) + perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view) + } + Distribution.CENTER -> + perpAnchor(CENTER) equalTo perpAnchor(CENTER, view) + } + } + } + + private fun anchor(position: AxisPosition, view: View = this): LayoutVariable { + return view.getAnchor(axis, position) + } + private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable { + return view.getAnchor(axis.other, position) + } + + enum class Distribution { + FILL, LEADING, CENTER, TRAILING + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/shadowui/view/View.kt b/src/main/kotlin/net/shadowfacts/shadowui/view/View.kt index 6412142..341bf39 100644 --- a/src/main/kotlin/net/shadowfacts/shadowui/view/View.kt +++ b/src/main/kotlin/net/shadowfacts/shadowui/view/View.kt @@ -3,6 +3,8 @@ package net.shadowfacts.shadowui.view import com.mojang.blaze3d.platform.GlStateManager import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.shadowui.LayoutVariable +import net.shadowfacts.shadowui.geometry.Axis +import net.shadowfacts.shadowui.geometry.AxisPosition import net.shadowfacts.shadowui.geometry.Rect import net.shadowfacts.shadowui.geometry.Size import net.shadowfacts.shadowui.util.Color @@ -45,6 +47,23 @@ open class View { var parent: View? = null val subviews = mutableListOf() + fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable { + return when (axis) { + Axis.HORIZONTAL -> + when (position) { + AxisPosition.LEADING -> leftAnchor + AxisPosition.CENTER -> centerXAnchor + AxisPosition.TRAILING -> rightAnchor + } + Axis.VERTICAL -> + when (position) { + AxisPosition.LEADING -> topAnchor + AxisPosition.CENTER -> centerYAnchor + AxisPosition.TRAILING -> bottomAnchor + } + } + } + fun addSubview(view: View): View { subviews.add(view) view.parent = this @@ -57,6 +76,7 @@ open class View { open fun wasAdded() { createInternalConstraints() + updateIntrinsicContentSizeConstraints(null, intrinsicContentSize) } open fun createInternalConstraints() { @@ -69,6 +89,8 @@ open class View { } private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) { + if (!this::solver.isInitialized) return + if (old != null) { solver.removeConstraint(intrinsicContentSizeWidthConstraint!!) solver.removeConstraint(intrinsicContentSizeHeightConstraint!!) diff --git a/src/test/kotlin/net/shadowfacts/shadowui/view/StackViewTests.kt b/src/test/kotlin/net/shadowfacts/shadowui/view/StackViewTests.kt new file mode 100644 index 0000000..0efe2b6 --- /dev/null +++ b/src/test/kotlin/net/shadowfacts/shadowui/view/StackViewTests.kt @@ -0,0 +1,194 @@ +package net.shadowfacts.shadowui.view + +import net.shadowfacts.kiwidsl.dsl +import net.shadowfacts.shadowui.Window +import net.shadowfacts.shadowui.geometry.Axis +import net.shadowfacts.shadowui.geometry.Size +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.math.abs + +/** + * @author shadowfacts + */ +class StackViewTests { + + lateinit var window: Window + + @BeforeEach + fun setup() { + window = Window() + } + + @Test + fun testVerticalLayout() { + val stack = window.addView(StackView(Axis.VERTICAL)) + val one = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + val two = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 75.0) + }) + val three = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + window.solver.dsl { + stack.topAnchor equalTo 0 + } + window.layout() + + assertEquals(0.0, abs(one.topAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok + assertEquals(50.0, one.bottomAnchor.value) + assertEquals(50.0, two.topAnchor.value) + assertEquals(125.0, two.bottomAnchor.value) + assertEquals(125.0, three.topAnchor.value) + assertEquals(175.0, three.bottomAnchor.value) + + assertEquals(175.0, stack.heightAnchor.value) + } + + @Test + fun testHorizontalLayout() { + val stack = window.addView(StackView(Axis.HORIZONTAL)) + val one = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + val two = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 75.0) + }) + val three = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + window.solver.dsl { + stack.leftAnchor equalTo 0 + } + window.layout() + + assertEquals(0.0, abs(one.leftAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok + assertEquals(50.0, one.rightAnchor.value) + assertEquals(50.0, two.leftAnchor.value) + assertEquals(125.0, two.rightAnchor.value) + assertEquals(125.0, three.leftAnchor.value) + assertEquals(175.0, three.rightAnchor.value) + + assertEquals(175.0, stack.widthAnchor.value) + } + + @Test + fun testVerticalLayoutWithLeading() { + val stack = window.addView(StackView(Axis.VERTICAL, StackView.Distribution.LEADING)) + val one = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + val two = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 75.0) + }) + val three = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(100.0, 100.0) + }) + window.solver.dsl { + stack.topAnchor equalTo 0 + stack.leftAnchor equalTo 0 + stack.rightAnchor equalTo 100 + } + window.layout() + + assertEquals(0.0, abs(one.leftAnchor.value)) + assertEquals(50.0, one.rightAnchor.value) + + assertEquals(0.0, abs(two.leftAnchor.value)) + assertEquals(75.0, two.rightAnchor.value) + + assertEquals(0.0, abs(three.leftAnchor.value)) + assertEquals(100.0, three.rightAnchor.value) + } + + @Test + fun testVerticalLayoutWithTrailing() { + val stack = window.addView(StackView(Axis.VERTICAL, StackView.Distribution.TRAILING)) + val one = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + val two = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 75.0) + }) + val three = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(100.0, 100.0) + }) + window.solver.dsl { + stack.topAnchor equalTo 0 + stack.leftAnchor equalTo 0 + stack.rightAnchor equalTo 100 + } + window.layout() + + assertEquals(50.0, one.leftAnchor.value) + assertEquals(100.0, one.rightAnchor.value) + + assertEquals(25.0, two.leftAnchor.value) + assertEquals(100.0, two.rightAnchor.value) + + assertEquals(0.0, abs(three.leftAnchor.value)) + assertEquals(100.0, three.rightAnchor.value) + } + + @Test + fun testVerticalLayoutWithCenter() { + val stack = window.addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER)) + val one = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + val two = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 75.0) + }) + val three = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(100.0, 100.0) + }) + window.solver.dsl { + stack.topAnchor equalTo 0 + stack.leftAnchor equalTo 0 + stack.rightAnchor equalTo 100 + } + window.layout() + + assertEquals(25.0, one.leftAnchor.value) + assertEquals(75.0, one.rightAnchor.value) + + assertEquals(12.5, two.leftAnchor.value) + assertEquals(87.5, two.rightAnchor.value) + + assertEquals(0.0, abs(three.leftAnchor.value)) + assertEquals(100.0, three.rightAnchor.value) + } + + @Test + fun testVerticalLayoutWithFill() { + val stack = window.addView(StackView(Axis.VERTICAL, StackView.Distribution.FILL)) + val one = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(50.0, 50.0) + }) + val two = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(75.0, 75.0) + }) + val three = stack.addArrangedSubview(View().apply { + intrinsicContentSize = Size(100.0, 100.0) + }) + window.solver.dsl { + stack.topAnchor equalTo 0 + stack.leftAnchor equalTo 0 + stack.rightAnchor equalTo 100 + } + window.layout() + + assertEquals(0.0, abs(one.leftAnchor.value)) + assertEquals(100.0, one.rightAnchor.value) + + assertEquals(0.0, abs(two.leftAnchor.value)) + assertEquals(100.0, two.rightAnchor.value) + + assertEquals(0.0, abs(three.leftAnchor.value)) + assertEquals(100.0, three.rightAnchor.value) + } + +} \ No newline at end of file