diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt index f8d9e76..6c08c7e 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt @@ -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 { + 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) } /** diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/button/AbstractButton.kt b/src/main/kotlin/net/shadowfacts/cacao/view/button/AbstractButton.kt index 895d121..6a513d3 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/button/AbstractButton.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/button/AbstractButton.kt @@ -86,16 +86,9 @@ abstract class AbstractButton>(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>(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 + } + } + } diff --git a/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt b/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt new file mode 100644 index 0000000..40bb218 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt @@ -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( + val tabs: Array, + 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> + + 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) { + 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( + val tab: T, + ): AbstractButton>( + 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) + } + } + +} diff --git a/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt b/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt index a6bf752..6515295 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt @@ -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 } } diff --git a/src/main/kotlin/net/shadowfacts/cacao/window/Window.kt b/src/main/kotlin/net/shadowfacts/cacao/window/Window.kt index 4f1ee3b..290f9db 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/window/Window.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/window/Window.kt @@ -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 } -} \ No newline at end of file +} diff --git a/src/main/kotlin/net/shadowfacts/phycon/screen/TestCacaoScreen.kt b/src/main/kotlin/net/shadowfacts/phycon/screen/TestCacaoScreen.kt index ea38e09..32e1a01 100644 --- a/src/main/kotlin/net/shadowfacts/phycon/screen/TestCacaoScreen.kt +++ b/src/main/kotlin/net/shadowfacts/phycon/screen/TestCacaoScreen.kt @@ -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 + } + } + } + } diff --git a/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt b/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt index bb261fb..1ca36a5 100644 --- a/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt +++ b/src/test/kotlin/net/shadowfacts/cacao/util/LCATest.kt @@ -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)) } -} \ No newline at end of file +}