Cacao: Add TabViewController
This commit is contained in:
parent
d20ba7460e
commit
277bcb71ee
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -86,7 +86,7 @@ open class Window(
|
|||
|
||||
fun wasAdded() {
|
||||
viewController.window = this
|
||||
viewController.loadView()
|
||||
viewController.loadViewIfNeeded()
|
||||
|
||||
viewController.view.window = this
|
||||
viewController.view.solver = solver
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue