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.Color 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( val tabs: List, 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 /** * 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 } /** * 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. * @param visible A function that determines if the tab should currently be visible. */ class SimpleTab( override val tabView: View, override val tooltip: Text? = null, override val controller: ViewController, private val visible: (() -> Boolean)? = null ): Tab { override val isVisible: Boolean get() = visible?.invoke() ?: true } /** * The currently selected tab. */ var currentTab: T = initialTab private set private lateinit var tabButtons: List> private lateinit var outerStack: StackView private lateinit var tabStack: StackView private lateinit var currentTabController: ViewController // 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) updateTabButtons() val background = NinePatchView(NinePatchTexture.PANEL_BG) outerStack.addArrangedSubview(background) tabVCContainer = View() tabVCContainer.zIndex = 1.0 view.addSubview(tabVCContainer) 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 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) } } override fun viewWillAppear() { super.viewWillAppear() currentTabController.viewWillAppear() } override fun viewWillDisappear() { super.viewWillDisappear() currentTabController.viewWillDisappear() } override fun viewDidDisappear() { super.viewDidDisappear() currentTabController.viewDidDisappear() } 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() } /** * 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) } 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() onTabChange?.invoke(currentTab) // todo: setNeedsLayout window!!.layout() } private class TabButton( val tab: T, ): AbstractButton>( tab.tabView, padding = 2.0 ) { companion object { val BACKGROUND = Identifier("phycon:textures/gui/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 -> 56 else -> 28 } backgroundView.texture = Texture(BACKGROUND, u, v) backgroundView.frame = Rect(0.0, 0.0, 28.0, if (selected) 32.0 else 28.0) } } }