Add DropdownButton

This commit is contained in:
Shadowfacts 2019-06-25 21:52:17 -04:00
parent b2499ad247
commit 4d1fb68c89
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
11 changed files with 317 additions and 46 deletions

View File

@ -11,9 +11,7 @@ import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.Texture
import net.shadowfacts.cacao.view.*
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.cacao.view.button.EnumButton
import net.shadowfacts.cacao.view.button.ToggleButton
import net.shadowfacts.cacao.view.button.DropdownButton
/**
* @author shadowfacts
@ -36,9 +34,14 @@ class TestCacaoScreen: CacaoScreen() {
intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color(0x0000ff)
})
val purple = blue.addSubview(ToggleButton(false).apply {
val purple = blue.addSubview(DropdownButton(
initialValue = RedstoneMode.HIGH,
allValues = RedstoneMode.values().asIterable(),
createView = { Label(it.name) },
updateView = { newValue, label -> label.text = newValue.name }
).apply {
handler = {
println("enum button clicked, new value: ${it.state}")
println("dropdown value: ${it.value}")
}
})

View File

@ -31,13 +31,26 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
*
* @param window The Window to add to this screen.
* @param index The index to insert the window into the window list at.
* @return The window that was added, as a convenience.
*/
fun addWindow(window: Window, index: Int = _windows.size) {
fun <T: Window> addWindow(window: T, index: Int = _windows.size): T {
_windows.add(index, window)
window.screen = this
return window
}
/**
* Removes the given window from this screen's window list.
*/
fun removeWindow(window: Window) {
_windows.remove(window)
}
override fun render(mouseX: Int, mouseY: Int, delta: Float) {
renderBackground()
if (minecraft != null) {
// workaround this.minecraft sometimes being null causing a crash
renderBackground()
}
val mouse = Point(mouseX, mouseY)
windows.forEach {
@ -47,11 +60,17 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
if (window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button)) == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
when (result) {
true ->
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
false ->
if (windows.size > 1) {
removeWindow(windows.last())
}
}
return false
return result == true
}
}

View File

@ -16,6 +16,12 @@ import java.util.*
*/
class Window {
/**
* The screen that this window belongs to.
* Not initialized until this window is added to a screen, using it before that point will throw a runtime exception.
*/
lateinit var screen: CacaoScreen
var solver = Solver()
// _views is the internal, mutable object, since we only want it to be mutated by the add/removeView methods
@ -28,6 +34,13 @@ class Window {
private var viewsSortedByZIndex: List<View> = listOf()
/**
* Convenience method that removes this window from its [screen].
*/
fun removeFromScreen() {
screen.removeWindow(this)
}
/**
* Adds the given view as a top-level view in this window.
*
@ -100,7 +113,7 @@ class Window {
* @return Whether the mouse click was handled by a view.
*/
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val view = viewsAtPoint(point).minBy(View::zIndex)
val view = viewsAtPoint(point).maxBy(View::zIndex)
if (view != null) {
val pointInView = Point(point.x - view.frame.left, point.y - view.frame.top)
return view.mouseClicked(pointInView, mouseButton)

View File

@ -3,9 +3,12 @@ package net.shadowfacts.cacao.util
import com.mojang.blaze3d.platform.GlStateManager
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexFormats
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.sound.SoundEvent
import net.shadowfacts.cacao.geometry.Rect
import org.lwjgl.opengl.GL11
/**
* Helper methods for rendering using Minecraft's utilities from Cacao views.
@ -46,7 +49,23 @@ object RenderHelper {
*/
fun draw(x: Double, y: Double, u: Int, v: Int, width: Double, height: Double, textureWidth: Int, textureHeight: Int) {
if (disabled) return
DrawableHelper.blit(x.toInt(), y.toInt(), u.toFloat(), v.toFloat(), width.toInt(), height.toInt(), textureWidth, textureHeight)
val uStart = u.toDouble() / textureWidth
val uEnd = (u + width) / textureWidth
val vStart = v.toDouble() / textureHeight
val vEnd = (v + height) / textureHeight
innerBlit(x, x + width, y, y + height, 0.0, uStart, uEnd, vStart, vEnd)
}
// Copied from net.minecraft.client.gui.DrawableHelper
private fun innerBlit(xStart: Double, xEnd: Double, yStart: Double, yEnd: Double, z: Double, uStart: Double, uEnd: Double, vStart: Double, vEnd: Double) {
val tessellator = Tessellator.getInstance()
val buffer = tessellator.bufferBuilder
buffer.begin(GL11.GL_QUADS, VertexFormats.POSITION_UV)
buffer.vertex(xStart, yEnd, z).texture(uStart, vEnd).next()
buffer.vertex(xEnd, yEnd, z).texture(uEnd, vEnd).next()
buffer.vertex(xEnd, yStart, z).texture(uEnd, vStart).next()
buffer.vertex(xStart, yStart, z).texture(uStart, vStart).next()
tessellator.draw()
}
/**

View File

@ -1,4 +1,4 @@
package net.shadowfacts.cacao.util
package net.shadowfacts.cacao.util.properties
import kotlin.reflect.KProperty

View File

@ -1,4 +1,4 @@
package net.shadowfacts.cacao.util
package net.shadowfacts.cacao.util.properties
import kotlin.reflect.KProperty

View File

@ -4,68 +4,70 @@ import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.ResettableLazyProperty
import net.shadowfacts.cacao.util.properties.ResettableLazyProperty
/**
* A helper class for drawing a [NinePatchTexture] in a view.
* `NinePatchView` will draw the given nine patch texture filling its bounds.
*
* This class and the region properties are left open for internal framework use, overriding them is not recommended.
*
* @author shadowfacts
* @param ninePatch The nine patch texture that this view will draw.
*/
class NinePatchView(val ninePatch: NinePatchTexture): View() {
open class NinePatchView(val ninePatch: NinePatchTexture): View() {
// Corners
protected val `$topLeft` = ResettableLazyProperty {
private val topLeftDelegate = ResettableLazyProperty {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected val topLeft by `$topLeft`
protected open val topLeft by topLeftDelegate
protected val `$topRight` = ResettableLazyProperty {
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected val topRight by `$topRight`
protected open val topRight by topRightDelegate
protected val `$bottomLeft` = ResettableLazyProperty {
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected val bottomLeft by `$bottomLeft`
protected open val bottomLeft by bottomLeftDelegate
protected val `$bottomRight` = ResettableLazyProperty {
private val bottomRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected val bottomRight by `$bottomRight`
protected open val bottomRight by bottomRightDelegate
// Edges
protected val `$topMiddle` = ResettableLazyProperty {
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble())
}
protected val topMiddle by `$topMiddle`
protected open val topMiddle by topMiddleDelegate
protected val `$bottomMiddle` = ResettableLazyProperty {
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
}
protected val bottomMiddle by `$bottomMiddle`
protected open val bottomMiddle by bottomMiddleDelegate
protected val `$leftMiddle` = ResettableLazyProperty {
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight)
}
protected val leftMiddle by `$leftMiddle`
protected open val leftMiddle by leftMiddleDelegate
protected val `$rightMiddle` = ResettableLazyProperty {
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
}
protected val rightMiddle by `$rightMiddle`
protected open val rightMiddle by rightMiddleDelegate
// Center
protected val `$center` = ResettableLazyProperty {
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
}
protected val center by `$center`
protected open val center by centerDelegate
protected val delegates = listOf(`$topLeft`, `$topRight`, `$bottomLeft`, `$bottomRight`, `$topMiddle`, `$bottomMiddle`, `$leftMiddle`, `$rightMiddle`, `$center`)
private val delegates = listOf(topLeftDelegate, topRightDelegate, bottomLeftDelegate, bottomRightDelegate, topMiddleDelegate, bottomMiddleDelegate, leftMiddleDelegate, rightMiddleDelegate, centerDelegate)
override fun didLayout() {
super.didLayout()
@ -89,8 +91,8 @@ class NinePatchView(val ninePatch: NinePatchTexture): View() {
private fun drawEdges() {
// Horizontal
for (i in 0 until (topMiddle.width.toInt() / ninePatch.centerWidth)) {
RenderHelper.draw(topMiddle.left + i * ninePatch.centerWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, ninePatch.centerWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(bottomMiddle.left + i * ninePatch.centerWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, ninePatch.centerWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(topMiddle.left + i * ninePatch.centerWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, ninePatch.centerWidth.toDouble(), topMiddle.height, ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(bottomMiddle.left + i * ninePatch.centerWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, ninePatch.centerWidth.toDouble(), bottomMiddle.height, ninePatch.texture.width, ninePatch.texture.height)
}
val remWidth = topMiddle.width.toInt() % ninePatch.centerWidth
if (remWidth > 0) {

View File

@ -2,8 +2,10 @@ package net.shadowfacts.cacao.view
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.*
import net.shadowfacts.cacao.util.properties.ObservableLateInitProperty
import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import java.util.*
@ -16,6 +18,13 @@ import java.util.*
*/
open class View() {
/**
* The window whose view hierarchy this view belongs to.
* Not initialized until the root view in this hierarchy has been added to a hierarchy,
* using it before that will throw a runtime exception.
*/
lateinit var window: Window
/**
* 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.
@ -156,6 +165,7 @@ open class View() {
view.superview = this
view.solver = solver
view.window = window
view.wasAdded()

View File

@ -1,6 +1,5 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.sound.SoundEvents
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
@ -113,14 +112,11 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (disabled) return false
val handler = handler
if (handler != null) {
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
// of AbstractButton.
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
@Suppress("UNCHECKED_CAST")
handler(this as Impl)
}
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
// of AbstractButton.
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
return true
}

View File

@ -0,0 +1,209 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.Texture
import net.shadowfacts.cacao.util.properties.ResettableLazyProperty
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.kiwidsl.dsl
/**
* A button that provides a dropdown for the user to select from a list of values.
* The button itself shows a [ContentView] representing the currently selected [Value] and an image indicator that serves
* as a hint for the ability to click the button and display the dropdown.
*
* The dropdown list itself is displayed by presenting a new [Window] at the front of the window stack.
* Each possible value is represented in the list by a button containing a [ContentView] for that value, with the button
* for the current value being disabled.
*
* @author shadowfacts
* @param Value The type of value that the dropdown selects.
* @param ContentView The specific type of the [View] that represents selected item in the button and each item in the dropdown list.
* @param initialValue The initial value of the dropdown button.
* @param allValues List of all allowed values for the dropdown.
* @param createView A function that creates a [ContentView] representing the given [Value].
* Positioning of content views is handled by the dropdown.
* @param updateView A function for updating the view used as the button's 'label' that's visible even when the dropdown isn't.
*/
class DropdownButton<Value, ContentView: View>(
val initialValue: Value,
val allValues: Iterable<Value>,
val createView: (Value) -> ContentView,
val updateView: (newValue: Value, view: ContentView) -> Unit,
padding: Double = 4.0
): AbstractButton<DropdownButton<Value, ContentView>>(
StackView(Axis.HORIZONTAL),
padding
) {
companion object {
val DROPDOWN_INDICATOR = Texture(Identifier("asmr", "textures/gui/dropdown.png"), 0, 0)
}
private val stackView: StackView
get() = content as StackView
private val contentView: ContentView
get() = stackView.arrangedSubviews.first() as ContentView
private lateinit var dropdownIndicator: TextureView
/**
* The currently selected [Value] of the dropdown.
*/
var value: Value = initialValue
set(value) {
field = value
updateView(value, contentView)
window.layout()
}
override fun wasAdded() {
super.wasAdded()
stackView.addArrangedSubview(createView(initialValue))
dropdownIndicator = stackView.addArrangedSubview(TextureView(DROPDOWN_INDICATOR))
solver.dsl {
dropdownIndicator.widthAnchor equalTo 9
dropdownIndicator.heightAnchor equalTo 9
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
return if (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT) {
showDropdown()
true
} else {
super.mouseClicked(point, mouseButton)
}
}
private fun showDropdown() {
val dropdownWindow = window.screen.addWindow(Window())
val dropdownBackground = dropdownWindow.addView(NinePatchView(DEFAULT_BG).apply {
zIndex = -1.0
})
val stack = dropdownWindow.addView(StackView(Axis.VERTICAL, StackView.Distribution.LEADING))
lateinit var selectedButton: View
val buttons = mutableListOf<Button>()
val last = allValues.count() - 1
for ((index, value) in allValues.withIndex()) {
val contentView = createView(value)
val button = stack.addArrangedSubview(Button(contentView, padding).apply {
background = null
hoveredBackground = DropdownItemBackgroundView(index == 0, index == last, HOVERED_BG)
disabledBackground = DropdownItemBackgroundView(index == 0, index == last, DISABLED_BG)
disabled = value == this@DropdownButton.value
handler = {
dropdownWindow.removeFromScreen()
valueSelected(value)
}
})
if (value == this@DropdownButton.value) {
selectedButton = button
}
buttons.add(button)
dropdownWindow.solver.dsl {
stack.widthAnchor greaterThanOrEqualTo button.widthAnchor
}
}
dropdownWindow.solver.dsl {
// constrain to the DropdownButton anchor's value constant, because we're crossing windows and
// therefore solvers, which isn't allowed
stack.leftAnchor equalTo this@DropdownButton.rightAnchor.value
selectedButton.centerYAnchor equalTo this@DropdownButton.centerYAnchor.value
dropdownBackground.leftAnchor equalTo stack.leftAnchor
dropdownBackground.rightAnchor equalTo stack.rightAnchor
dropdownBackground.topAnchor equalTo stack.topAnchor
dropdownBackground.bottomAnchor equalTo stack.bottomAnchor
}
dropdownWindow.layout()
dropdownWindow.solver.dsl {
buttons.forEach {
it.widthAnchor equalTo stack.frame.width
}
}
dropdownWindow.layout()
}
private fun valueSelected(value: Value) {
this.value = value
handler?.invoke(this)
}
}
private class DropdownItemBackgroundView(
private val first: Boolean,
private val last: Boolean,
ninePatch: NinePatchTexture
): NinePatchView(ninePatch) {
// Corners
private val topLeftDelegate = ResettableLazyProperty {
super.topLeft
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), if (first) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, topLeft.width, topLeft.height)
}
override val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(topLeft.left, bounds.height - ninePatch.cornerHeight, topLeft.width, if (last) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(topRight.left, bottomLeft.top, topLeft.width, bottomLeft.height)
}
override val bottomRight by bottomRightDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, topLeft.height)
}
override val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, bottomLeft.height)
}
override val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, topLeft.bottom, topLeft.width, bounds.height - (if (first && last) 2 else if (first || last) 1 else 0) * ninePatch.cornerHeight)
}
override val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, topRight.bottom, topRight.width, leftMiddle.height)
}
override val rightMiddle by rightMiddleDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topMiddle.bottom, topMiddle.width, leftMiddle.height)
}
override val center by centerDelegate
private val delegates = listOf(topLeftDelegate, topRightDelegate, bottomLeftDelegate, bottomRightDelegate, topMiddleDelegate, bottomMiddleDelegate, leftMiddleDelegate, rightMiddleDelegate, centerDelegate)
override fun didLayout() {
super.didLayout()
delegates.forEach(ResettableLazyProperty<Rect>::reset)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B