PhysicalConnectivity/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/TabViewController.kt

238 lines
7.1 KiB
Kotlin

package net.shadowfacts.cacao.viewcontroller
import net.minecraft.client.util.math.MatrixStack
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
import java.lang.RuntimeException
/**
* 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.
*
* @author shadowfacts
* @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).
*/
class TabViewController<T: TabViewController.Tab>(
val tabs: List<T>,
initialTab: T = tabs.first(),
val onTabChange: ((T) -> Unit)? = null
): ViewController() {
/**
* 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
*/
interface Tab {
/**
* The view displayed on the button for this tab.
*/
val tabView: View
/**
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
*/
val tooltip: Text?
/**
* 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.
*/
val controller: ViewController
}
/**
* 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.
*/
class SimpleTab(
override val tabView: View,
override val tooltip: Text? = null,
override val controller: ViewController,
): Tab
/**
* The currently selected tab.
*/
var currentTab: T = initialTab
private set
private lateinit var tabButtons: List<TabButton<T>>
private lateinit var outerStack: StackView
private lateinit var tabStack: StackView
// todo: this shouldn't be public, use layout guides
lateinit var tabVCContainer: View
private set
override fun viewDidLoad() {
super.viewDidLoad()
// todo: might be simpler to just not use a stack view
// 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)
tabButtons = tabs.mapIndexed { index, tab ->
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())
val background = NinePatchView(NinePatchTexture.PANEL_BG)
outerStack.addArrangedSubview(background)
tabVCContainer = View()
tabVCContainer.zIndex = 1.0
view.addSubview(tabVCContainer)
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
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
}
/**
* 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")
}
val oldTab = currentTab
currentTab = tab
tabButtons.forEach {
it.setSelected(it.tab === tab)
}
oldTab.controller.removeFromParent()
embedChild(currentTab.controller, tabVCContainer)
onTabChange?.invoke(currentTab)
// 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)
backgroundView.zIndex = -1.0
addSubview(backgroundView)
solver.dsl {
content.bottomAnchor lessThanOrEqualTo (bottomAnchor - 4)
}
}
override fun didLayout() {
super.didLayout()
updateBackgroundTexture()
}
fun setSelected(selected: Boolean) {
this.selected = selected
updateBackgroundTexture()
}
override fun getCurrentBackground(mouse: Point) = backgroundView
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!!))
}
}
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)
}
}
}