2021-02-27 18:24:29 +00:00
|
|
|
package net.shadowfacts.cacao.viewcontroller
|
|
|
|
|
2021-02-27 23:28:19 +00:00
|
|
|
import net.minecraft.client.util.math.MatrixStack
|
2021-02-27 18:24:29 +00:00
|
|
|
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
|
2021-02-28 17:19:09 +00:00
|
|
|
import java.lang.RuntimeException
|
2021-02-27 18:24:29 +00:00
|
|
|
|
|
|
|
/**
|
2021-02-28 17:19:09 +00:00
|
|
|
* A tab view controller is divided into two sections: a tab bar at the top, and a content view at the bottom.
|
|
|
|
*
|
|
|
|
* The tab bar contains a tab button for each of the tabs in the VC and the content view contains the view of the
|
|
|
|
* active tab's view controller.
|
|
|
|
*
|
|
|
|
* The active tab's view controller is also added as a child of the tab view controller.
|
|
|
|
*
|
2021-02-27 18:24:29 +00:00
|
|
|
* @author shadowfacts
|
2021-02-28 17:19:09 +00:00
|
|
|
* @param T The type of the tab objects this view controller uses.
|
|
|
|
* @param tabs The list of tabs in this controller.
|
|
|
|
* @param initialTab The tab that is initially selected when the controller is first created.
|
|
|
|
* @param onTabChange A function invoked immediately after the active tab has changed (and the content view has been
|
|
|
|
* updated).
|
2021-02-27 18:24:29 +00:00
|
|
|
*/
|
|
|
|
class TabViewController<T: TabViewController.Tab>(
|
2021-02-28 02:48:40 +00:00
|
|
|
val tabs: List<T>,
|
2021-02-28 17:19:09 +00:00
|
|
|
initialTab: T = tabs.first(),
|
|
|
|
val onTabChange: ((T) -> Unit)? = null
|
2021-02-27 18:24:29 +00:00
|
|
|
): ViewController() {
|
|
|
|
|
2021-02-28 17:19:09 +00:00
|
|
|
/**
|
|
|
|
* The Tab interface defines the requirements for tab objects that can be used with this view controller.
|
|
|
|
*
|
|
|
|
* This is an interface to allow for tab objects to carry additional data. A simple implementation is provided.
|
|
|
|
* @see SimpleTab
|
|
|
|
*/
|
2021-02-27 18:24:29 +00:00
|
|
|
interface Tab {
|
2021-02-28 17:19:09 +00:00
|
|
|
/**
|
|
|
|
* The view displayed on the button for this tab.
|
|
|
|
*/
|
2021-02-27 18:24:29 +00:00
|
|
|
val tabView: View
|
2021-02-28 17:19:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
|
|
|
|
*/
|
2021-02-27 18:24:29 +00:00
|
|
|
val tooltip: Text?
|
2021-02-28 17:19:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The view controller used as content when this tab is active. When switching tabs, the returned content VC
|
|
|
|
* may be reused or created from scratch each time.
|
|
|
|
*/
|
2021-02-27 18:24:29 +00:00
|
|
|
val controller: ViewController
|
2021-03-03 22:23:57 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Used by the tab view controller to determine whether the button for this tab should be displayed.
|
|
|
|
* If the conditions that control this change, call [TabViewController.visibleTabsChanged].
|
|
|
|
*/
|
|
|
|
val isVisible: Boolean
|
|
|
|
get() = true
|
2021-02-27 18:24:29 +00:00
|
|
|
}
|
|
|
|
|
2021-02-28 17:19:09 +00:00
|
|
|
/**
|
|
|
|
* A simple [Tab] implementation that provides the minimum necessary information.
|
|
|
|
* @param tabView The view to display on the tab's button.
|
|
|
|
* @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none).
|
|
|
|
* @param controller The content view controller for this tab.
|
2021-03-03 22:23:57 +00:00
|
|
|
* @param visible A function that determines if the tab should currently be visible.
|
2021-02-28 17:19:09 +00:00
|
|
|
*/
|
|
|
|
class SimpleTab(
|
|
|
|
override val tabView: View,
|
|
|
|
override val tooltip: Text? = null,
|
|
|
|
override val controller: ViewController,
|
2021-03-03 22:23:57 +00:00
|
|
|
private val visible: (() -> Boolean)? = null
|
|
|
|
): Tab {
|
|
|
|
override val isVisible: Boolean
|
|
|
|
get() = visible?.invoke() ?: true
|
|
|
|
}
|
2021-02-28 17:19:09 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The currently selected tab.
|
|
|
|
*/
|
|
|
|
var currentTab: T = initialTab
|
2021-02-27 18:24:29 +00:00
|
|
|
private set
|
|
|
|
|
|
|
|
private lateinit var tabButtons: List<TabButton<T>>
|
|
|
|
|
|
|
|
private lateinit var outerStack: StackView
|
|
|
|
private lateinit var tabStack: StackView
|
2021-03-05 00:44:31 +00:00
|
|
|
private lateinit var currentTabController: ViewController
|
2021-02-28 02:48:40 +00:00
|
|
|
// todo: this shouldn't be public, use layout guides
|
|
|
|
lateinit var tabVCContainer: View
|
|
|
|
private set
|
2021-02-27 18:24:29 +00:00
|
|
|
|
|
|
|
override fun viewDidLoad() {
|
|
|
|
super.viewDidLoad()
|
|
|
|
|
2021-02-28 02:48:40 +00:00
|
|
|
// todo: might be simpler to just not use a stack view
|
2021-02-27 18:24:29 +00:00
|
|
|
// 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)
|
2021-03-03 22:23:57 +00:00
|
|
|
updateTabButtons()
|
2021-02-27 18:24:29 +00:00
|
|
|
|
|
|
|
val background = NinePatchView(NinePatchTexture.PANEL_BG)
|
2021-02-28 02:48:40 +00:00
|
|
|
outerStack.addArrangedSubview(background)
|
|
|
|
|
|
|
|
tabVCContainer = View()
|
|
|
|
tabVCContainer.zIndex = 1.0
|
|
|
|
view.addSubview(tabVCContainer)
|
2021-02-27 18:24:29 +00:00
|
|
|
|
2021-03-05 00:44:31 +00:00
|
|
|
currentTabController = currentTab.controller
|
|
|
|
currentTabController.willMoveTo(this)
|
|
|
|
embedChild(currentTabController, tabVCContainer)
|
|
|
|
currentTabController.didMoveTo(this)
|
|
|
|
// will/did appear events for the initial VC are provided by this class' implementations of those
|
2021-02-27 18:24:29 +00:00
|
|
|
|
|
|
|
view.solver.dsl {
|
|
|
|
outerStack.leftAnchor equalTo view.leftAnchor
|
|
|
|
outerStack.rightAnchor equalTo view.rightAnchor
|
|
|
|
outerStack.topAnchor equalTo view.topAnchor
|
|
|
|
outerStack.bottomAnchor equalTo view.bottomAnchor
|
|
|
|
|
2021-02-28 02:48:40 +00:00
|
|
|
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
|
|
|
|
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
|
|
|
|
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
|
|
|
|
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
|
2021-02-27 18:24:29 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-05 00:44:31 +00:00
|
|
|
override fun viewWillAppear() {
|
|
|
|
super.viewWillAppear()
|
|
|
|
currentTabController.viewWillAppear()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun viewDidAppear() {
|
|
|
|
super.viewDidAppear()
|
|
|
|
currentTabController.viewDidAppear()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun viewWillDisappear() {
|
|
|
|
super.viewWillDisappear()
|
|
|
|
currentTabController.viewWillDisappear()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun viewDidDisappear() {
|
|
|
|
super.viewDidDisappear()
|
|
|
|
currentTabController.viewDidDisappear()
|
|
|
|
}
|
|
|
|
|
2021-03-03 22:23:57 +00:00
|
|
|
private fun updateTabButtons() {
|
|
|
|
while (tabStack.arrangedSubviews.isNotEmpty()) tabStack.removeArrangedSubview(tabStack.arrangedSubviews.first())
|
|
|
|
|
|
|
|
tabButtons = tabs.mapNotNull { tab ->
|
|
|
|
if (!tab.isVisible) {
|
|
|
|
return@mapNotNull null
|
|
|
|
}
|
|
|
|
|
|
|
|
val btn = TabButton(tab)
|
|
|
|
btn.handler = { selectTab(it.tab) }
|
|
|
|
if (tab == currentTab) {
|
|
|
|
btn.setSelected(true)
|
|
|
|
}
|
|
|
|
btn
|
|
|
|
}
|
|
|
|
// todo: batch calls to addArrangedSubview
|
|
|
|
tabButtons.forEach(tabStack::addArrangedSubview)
|
|
|
|
|
|
|
|
// spacer
|
|
|
|
tabStack.addArrangedSubview(View())
|
|
|
|
|
|
|
|
window!!.layout()
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Call this method when the conditions that make the configured tabs visible change.
|
|
|
|
*/
|
|
|
|
fun visibleTabsChanged() {
|
|
|
|
updateTabButtons()
|
|
|
|
}
|
|
|
|
|
2021-02-28 17:19:09 +00:00
|
|
|
/**
|
|
|
|
* Sets the provided tab as the currently active tab for this controller. Updates the state of tab bar buttons and
|
|
|
|
* swaps the content view controller.
|
|
|
|
*
|
|
|
|
* After the tab and the content are changed, [onTabChange] is invoked.
|
|
|
|
*
|
|
|
|
* @throws RuntimeException If the provided tab was not passed in as part of the [tabs] list.
|
|
|
|
*/
|
|
|
|
fun selectTab(tab: T) {
|
|
|
|
if (!tabs.contains(tab)) {
|
|
|
|
throw RuntimeException("Cannot activate tab not in TabViewController.tabs")
|
|
|
|
}
|
|
|
|
|
2021-02-27 18:24:29 +00:00
|
|
|
val oldTab = currentTab
|
2021-02-28 17:19:09 +00:00
|
|
|
currentTab = tab
|
2021-02-27 18:24:29 +00:00
|
|
|
|
2021-02-28 17:19:09 +00:00
|
|
|
tabButtons.forEach {
|
|
|
|
it.setSelected(it.tab === tab)
|
|
|
|
}
|
2021-03-05 00:44:31 +00:00
|
|
|
currentTabController.viewWillDisappear()
|
|
|
|
currentTabController.view.removeFromSuperview()
|
|
|
|
currentTabController.viewDidDisappear()
|
|
|
|
currentTabController.willMoveTo(null)
|
|
|
|
currentTabController.removeFromParent()
|
|
|
|
currentTabController.didMoveTo(null)
|
|
|
|
|
|
|
|
currentTabController = currentTab.controller
|
|
|
|
|
|
|
|
currentTabController.willMoveTo(this)
|
|
|
|
embedChild(currentTabController, tabVCContainer)
|
|
|
|
currentTabController.didMoveTo(this)
|
|
|
|
currentTabController.viewWillAppear()
|
|
|
|
currentTabController.viewDidAppear()
|
2021-02-28 17:19:09 +00:00
|
|
|
|
|
|
|
onTabChange?.invoke(currentTab)
|
|
|
|
|
2021-02-27 18:24:29 +00:00
|
|
|
// 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)
|
2021-02-28 02:48:40 +00:00
|
|
|
backgroundView.zIndex = -1.0
|
2021-02-27 18:24:29 +00:00
|
|
|
addSubview(backgroundView)
|
2021-02-28 02:48:40 +00:00
|
|
|
solver.dsl {
|
|
|
|
content.bottomAnchor lessThanOrEqualTo (bottomAnchor - 4)
|
|
|
|
}
|
2021-02-27 18:24:29 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun didLayout() {
|
|
|
|
super.didLayout()
|
|
|
|
updateBackgroundTexture()
|
|
|
|
}
|
|
|
|
|
|
|
|
fun setSelected(selected: Boolean) {
|
|
|
|
this.selected = selected
|
|
|
|
updateBackgroundTexture()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getCurrentBackground(mouse: Point) = backgroundView
|
|
|
|
|
2021-02-27 23:28:19 +00:00
|
|
|
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
|
|
|
|
super.draw(matrixStack, mouse, delta)
|
|
|
|
|
|
|
|
if (mouse in bounds && tab.tooltip != null) {
|
|
|
|
window!!.drawTooltip(listOf(tab.tooltip!!))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-27 18:24:29 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|