Cacao: Add TabViewController

This commit is contained in:
Shadowfacts 2021-02-27 13:24:29 -05:00
parent d20ba7460e
commit 277bcb71ee
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
7 changed files with 330 additions and 55 deletions

View File

@ -11,6 +11,7 @@ import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import java.lang.RuntimeException
import java.util.*
import kotlin.collections.HashSet
/**
* The base Cacao View class. Provides layout anchors, properties, and helper methods.
@ -27,11 +28,16 @@ open class View() {
*/
var window: Window? = null
private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) {
v.solver = it
}
}
/**
* The constraint solver used by the [net.shadowfacts.cacao.Window] this view belongs to.
* Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
*/
lateinit var solver: Solver
var solver: Solver by solverDelegate
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
@ -167,7 +173,9 @@ open class View() {
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = this
view.solver = solver
if (solverDelegate.isInitialized) {
view.solver = solver
}
view.window = window
view.wasAdded()
@ -182,16 +190,34 @@ open class View() {
* @throws RuntimeException If the given [view] is not a subview of this view.
*/
fun removeSubview(view: View) {
if (view.superview != this) {
if (view.superview !== this) {
throw RuntimeException("Cannot remove subview whose superview is not this view")
}
solver.constraints.filter { constraint ->
constraint.getVariables().any { it is LayoutVariable && it.owner == view }
}.forEach(solver::removeConstraint)
_subviews.remove(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = null
// we need to remove constraints for this subview that cross the boundary between the subview and ourself
val constraintsToRemove = solver.constraints.filter { constraint ->
val variables = constraint.getVariables().mapNotNull { it as? LayoutVariable }
for (a in 0 until variables.size - 1) {
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)
if (ancestor == null) {
return@filter true
}
}
}
false
}
constraintsToRemove.forEach(solver::removeConstraint)
// todo: does this need to be reset
// view.solver = null
view.window = null
@ -254,7 +280,7 @@ open class View() {
}
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!usesConstraintBasedLayout || !this::solver.isInitialized) return
if (!usesConstraintBasedLayout || !solverDelegate.isInitialized) return
if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
@ -273,14 +299,14 @@ open class View() {
* If overridden, the super-class method must be called.
*/
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)
}
subviews.forEach(View::didLayout)
}
/**

View File

@ -86,16 +86,9 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
RenderHelper.fill(matrixStack, bounds, backgroundColor)
var currentBackground: View? = background
if (mouse in bounds) {
currentBackground = hoveredBackground ?: currentBackground
}
if (disabled) {
currentBackground = disabledBackground ?: currentBackground
}
// don't need to convert mouse to background coordinate system
// the edges are all pinned, so the coordinate space is the same
currentBackground?.draw(matrixStack, mouse, delta)
getCurrentBackground(mouse)?.draw(matrixStack, mouse, delta)
val mouseInContent = convert(mouse, to = content)
content.draw(matrixStack, mouseInContent, delta)
@ -117,4 +110,14 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
return true
}
protected open fun getCurrentBackground(mouse: Point): View? {
return if (disabled) {
disabledBackground ?: background
} else if (mouse in bounds) {
hoveredBackground ?: background
} else {
background
}
}
}

View File

@ -0,0 +1,160 @@
package net.shadowfacts.cacao.viewcontroller
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.NinePatchView
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.view.button.AbstractButton
import net.shadowfacts.kiwidsl.dsl
/**
* @author shadowfacts
*/
class TabViewController<T: TabViewController.Tab>(
val tabs: Array<T>,
initalTab: T = tabs.first()
): ViewController() {
interface Tab {
val tabView: View
val tooltip: Text?
val controller: ViewController
}
var currentTab: T = initalTab
private set
private lateinit var tabButtons: List<TabButton<T>>
private lateinit var outerStack: StackView
private lateinit var tabStack: StackView
private lateinit var tabVCContainer: View
override fun viewDidLoad() {
super.viewDidLoad()
// padding is -4 so tab button texture overlaps with panel BG as expected
outerStack = StackView(Axis.VERTICAL, StackView.Distribution.FILL, -4.0)
view.addSubview(outerStack)
tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL)
tabStack.zIndex = 1.0
outerStack.addArrangedSubview(tabStack)
tabVCContainer = View()
outerStack.addArrangedSubview(tabVCContainer)
tabButtons = tabs.mapIndexed { index, tab ->
val btn = TabButton(tab)
btn.handler = this::selectTab
if (tab == currentTab) {
btn.setSelected(true)
}
btn
}
// todo: batch calls to addArrangedSubview
tabButtons.forEach(tabStack::addArrangedSubview)
// spacer
tabStack.addArrangedSubview(View())
val background = NinePatchView(NinePatchTexture.PANEL_BG)
background.zIndex = -1.0
tabVCContainer.addSubview(background)
embedChild(currentTab.controller, tabVCContainer)
view.solver.dsl {
outerStack.leftAnchor equalTo view.leftAnchor
outerStack.rightAnchor equalTo view.rightAnchor
outerStack.topAnchor equalTo view.topAnchor
outerStack.bottomAnchor equalTo view.bottomAnchor
background.leftAnchor equalTo tabVCContainer.leftAnchor
background.rightAnchor equalTo tabVCContainer.rightAnchor
background.topAnchor equalTo tabVCContainer.topAnchor
background.bottomAnchor equalTo tabVCContainer.bottomAnchor
}
}
private fun selectTab(button: TabButton<T>) {
val oldTab = currentTab
currentTab = button.tab
// todo: unselect old button
tabButtons.forEach { it.setSelected(false) }
oldTab.controller.removeFromParent()
button.setSelected(true)
embedChild(currentTab.controller, tabVCContainer)
// todo: setNeedsLayout
window!!.layout()
}
private class TabButton<T: Tab>(
val tab: T,
): AbstractButton<TabButton<T>>(
tab.tabView,
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("textures/gui/container/creative_inventory/tabs.png")
}
private var selected = false
private var backgroundView = TextureView(Texture(BACKGROUND, 0, 0))
init {
intrinsicContentSize = Size(28.0, 32.0)
background = null
hoveredBackground = null
disabledBackground = null
}
override fun wasAdded() {
super.wasAdded()
backgroundView.usesConstraintBasedLayout = false
backgroundView.frame = Rect(0.0, 0.0, 28.0, 32.0)
addSubview(backgroundView)
}
override fun didLayout() {
super.didLayout()
updateBackgroundTexture()
}
fun setSelected(selected: Boolean) {
this.selected = selected
updateBackgroundTexture()
}
override fun getCurrentBackground(mouse: Point) = backgroundView
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (selected) return false
else return super.mouseClicked(point, mouseButton)
}
private fun updateBackgroundTexture() {
val v = if (selected) 32 else 0
val u = when {
superview == null -> 0
frame.left == 0.0 -> 0
frame.right == superview!!.bounds.right -> 140
else -> 28
}
backgroundView.texture = Texture(BACKGROUND, u, v)
backgroundView.frame = Rect(0.0, 0.0, 28.0, if (selected) 32.0 else 28.0)
}
}
}

View File

@ -23,6 +23,12 @@ abstract class ViewController {
* b) this VC is added as a child of another view controller.
*/
var window: Window? = null
set(value) {
field = value
for (vc in children) {
vc.window = value
}
}
/**
* Helper function for creating layout constraints in the domain of this VC's window.
@ -70,6 +76,18 @@ abstract class ViewController {
view = View()
}
val isViewLoaded: Boolean
get() = ::view.isInitialized
/**
* Calls [loadView] to load this controller's view only if it has not already been loaded.
*/
fun loadViewIfNeeded() {
if (!isViewLoaded) {
loadView()
}
}
/**
* This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been
* called.
@ -149,7 +167,8 @@ abstract class ViewController {
viewController.parent = this
viewController.window = window
_children.add(viewController)
viewController.loadView()
val wasViewLoaded = viewController.isViewLoaded
viewController.loadViewIfNeeded()
container.addSubview(viewController.view)
@ -162,7 +181,9 @@ abstract class ViewController {
}
}
viewController.viewDidLoad()
if (!wasViewLoaded) {
viewController.viewDidLoad()
}
}
/**
@ -186,7 +207,6 @@ abstract class ViewController {
fun removeFromParent() {
parent?.removeChild(this)
view.removeFromSuperview()
// todo: remove view from superview
}
}

View File

@ -86,7 +86,7 @@ open class Window(
fun wasAdded() {
viewController.window = this
viewController.loadView()
viewController.loadViewIfNeeded()
viewController.view.window = this
viewController.view.solver = solver
@ -214,4 +214,4 @@ open class Window(
return false
}
}
}

View File

@ -2,6 +2,7 @@ package net.shadowfacts.phycon.screen
import net.minecraft.client.MinecraftClient
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.window.Window
@ -12,6 +13,7 @@ import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.*
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.cacao.viewcontroller.TabViewController
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
@ -21,48 +23,104 @@ import net.shadowfacts.kiwidsl.dsl
class TestCacaoScreen: CacaoScreen() {
init {
val viewController = object: ViewController() {
override fun loadView() {
view = View()
}
// val viewController = object: ViewController() {
// override fun loadView() {
// view = View()
// }
//
// override fun viewDidLoad() {
// super.viewDidLoad()
//
// val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0)).apply {
// backgroundColor = Color.WHITE
// }
// val birch = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16))).apply {
// intrinsicContentSize = Size(50.0, 50.0)
// }
// val ninePatch = stack.addArrangedSubview(NinePatchView(NinePatchTexture.PANEL_BG)).apply {
// intrinsicContentSize = Size(75.0, 100.0)
// }
// val red = stack.addArrangedSubview(View()).apply {
// intrinsicContentSize = Size(50.0, 50.0)
// backgroundColor = Color.RED
// }
//
// val label = Label(LiteralText("Test"), wrappingMode = Label.WrappingMode.NO_WRAP).apply {
//// textColor = Color.BLACK
// }
//// stack.addArrangedSubview(label)
// val button = red.addSubview(Button(label))
//
// view.solver.dsl {
// stack.topAnchor equalTo 0
// stack.centerXAnchor equalTo window!!.centerXAnchor
// stack.widthAnchor equalTo 100
//
//
// button.centerXAnchor equalTo red.centerXAnchor
// button.centerYAnchor equalTo red.centerYAnchor
//// label.heightAnchor equalTo 9
// button.heightAnchor equalTo 20
// }
//
// }
// }
// addWindow(Window(viewController))
val viewController = object: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0)).apply {
backgroundColor = Color.WHITE
}
val birch = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16))).apply {
intrinsicContentSize = Size(50.0, 50.0)
}
val ninePatch = stack.addArrangedSubview(NinePatchView(NinePatchTexture.PANEL_BG)).apply {
intrinsicContentSize = Size(75.0, 100.0)
}
val red = stack.addArrangedSubview(View()).apply {
intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color.RED
}
val label = Label(LiteralText("Test"), wrappingMode = Label.WrappingMode.NO_WRAP).apply {
// textColor = Color.BLACK
}
// stack.addArrangedSubview(label)
val button = red.addSubview(Button(label))
val tabs = arrayOf(
Tab(Label("A"), AViewController()),
Tab(Label("B"), BViewController()),
)
val tabVC = TabViewController(tabs)
embedChild(tabVC, pinEdges = false)
view.solver.dsl {
stack.topAnchor equalTo 0
stack.centerXAnchor equalTo window!!.centerXAnchor
stack.widthAnchor equalTo 100
button.centerXAnchor equalTo red.centerXAnchor
button.centerYAnchor equalTo red.centerYAnchor
// label.heightAnchor equalTo 9
button.heightAnchor equalTo 20
tabVC.view.centerXAnchor equalTo view.centerXAnchor
tabVC.view.centerYAnchor equalTo view.centerYAnchor
tabVC.view.widthAnchor equalTo 200
tabVC.view.heightAnchor equalTo 150
}
}
}
addWindow(Window(viewController))
}
data class Tab(
override val tabView: View,
override val controller: ViewController,
override val tooltip: Text? = null
): TabViewController.Tab
class AViewController: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val button = Button(Label("A content")) {
println("A pressed")
}
view.addSubview(button)
view.solver.dsl {
button.centerXAnchor equalTo view.centerXAnchor
button.centerYAnchor equalTo view.centerYAnchor
}
}
}
class BViewController: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val button = Button(Label("B content")) {
println("B pressed")
}
view.addSubview(button)
view.solver.dsl {
button.centerXAnchor equalTo view.centerXAnchor
button.centerYAnchor equalTo view.centerYAnchor
}
}
}
}

View File

@ -19,6 +19,14 @@ class LCATest {
assertEquals(parent, LowestCommonAncestor.find(child, parent, Node::parent))
}
@Test
fun testNestedParent() {
val parent = Node("parent", null)
val middle = Node("middle", parent)
val child = Node("child", middle)
assertEquals(parent, LowestCommonAncestor.find(parent, child, Node::parent))
}
@Test
fun testSiblings() {
val root = Node("root", null)
@ -70,4 +78,4 @@ class LCATest {
assertNull(LowestCommonAncestor.find(b, c, Node::parent))
}
}
}