Compare commits

...

3 Commits

Author SHA1 Message Date
Shadowfacts 4d1fb68c89
Add DropdownButton 2019-06-25 21:52:17 -04:00
Shadowfacts b2499ad247
Add Z index and view sorting 2019-06-25 18:39:58 -04:00
Shadowfacts 27bad18931
Update the NinePatchView computed regions on layout changes
Otherwise, updating the size of a button (e.g. by changing it's content)
would result in the button having an incorrectly displayed background
2019-06-25 18:08:46 -04:00
11 changed files with 399 additions and 40 deletions

View File

@ -11,9 +11,7 @@ import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.NinePatchTexture import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.Texture import net.shadowfacts.cacao.util.Texture
import net.shadowfacts.cacao.view.* import net.shadowfacts.cacao.view.*
import net.shadowfacts.cacao.view.button.Button import net.shadowfacts.cacao.view.button.DropdownButton
import net.shadowfacts.cacao.view.button.EnumButton
import net.shadowfacts.cacao.view.button.ToggleButton
/** /**
* @author shadowfacts * @author shadowfacts
@ -36,9 +34,14 @@ class TestCacaoScreen: CacaoScreen() {
intrinsicContentSize = Size(50.0, 50.0) intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color(0x0000ff) 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 = { 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 window The Window to add to this screen.
* @param index The index to insert the window into the window list at. * @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) _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) { 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) val mouse = Point(mouseX, mouseY)
windows.forEach { windows.forEach {
@ -47,11 +60,17 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull() val window = windows.lastOrNull()
if (window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button)) == true) { val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK) 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 { 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() var solver = Solver()
// _views is the internal, mutable object, since we only want it to be mutated by the add/removeView methods // _views is the internal, mutable object, since we only want it to be mutated by the add/removeView methods
@ -26,6 +32,15 @@ class Window {
*/ */
val views: List<View> = _views val views: List<View> = _views
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. * Adds the given view as a top-level view in this window.
* *
@ -34,6 +49,9 @@ class Window {
*/ */
fun <T: View> addView(view: T): T { fun <T: View> addView(view: T): T {
_views.add(view) _views.add(view)
viewsSortedByZIndex = views.sortedBy(View::zIndex)
view.window = this
view.solver = solver view.solver = solver
view.wasAdded() view.wasAdded()
@ -42,11 +60,22 @@ class Window {
} }
/** /**
* Attempts to find a top level view of this window that contains the given point. * Finds all views in this window at the given point.
* If there are multiple overlapping views, which one this method returns is undefined.
* *
* @param point The point to find views at, in the coordinate system of the window. * @param point The point to find views at, in the coordinate system of the window.
* @return The view, if any, that contains the given point. * @return All views that contain the point.
*/
fun viewsAtPoint(point: Point): List<View> {
return views.filter { point in it.frame }
}
/**
* Attempts to find a top level view of this window that contains the given point.
* If there are multiple overlapping views, which one this method returns is undefined.
* [viewsAtPoint] may be used, and the resulting List sorted by [View.zIndex].
*
* @param point The point to find views at, in the coordinate system of the window.
* @return the Veiw, if any, that contain the given point.
*/ */
fun viewAtPoint(point: Point): View? { fun viewAtPoint(point: Point): View? {
return views.firstOrNull { point in it.frame } return views.firstOrNull { point in it.frame }
@ -69,7 +98,7 @@ class Window {
* @param delta The time elapsed since the last frame. * @param delta The time elapsed since the last frame.
*/ */
fun draw(mouse: Point, delta: Float) { fun draw(mouse: Point, delta: Float) {
views.forEach { viewsSortedByZIndex.forEach {
val mouseInView = Point(mouse.x - it.frame.left, mouse.y - it.frame.top) val mouseInView = Point(mouse.x - it.frame.left, mouse.y - it.frame.top)
it.draw(mouseInView, delta) it.draw(mouseInView, delta)
} }
@ -84,7 +113,7 @@ class Window {
* @return Whether the mouse click was handled by a view. * @return Whether the mouse click was handled by a view.
*/ */
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val view = viewAtPoint(point) val view = viewsAtPoint(point).maxBy(View::zIndex)
if (view != null) { if (view != null) {
val pointInView = Point(point.x - view.frame.left, point.y - view.frame.top) val pointInView = Point(point.x - view.frame.left, point.y - view.frame.top)
return view.mouseClicked(pointInView, mouseButton) return view.mouseClicked(pointInView, mouseButton)

View File

@ -3,9 +3,12 @@ package net.shadowfacts.cacao.util
import com.mojang.blaze3d.platform.GlStateManager import com.mojang.blaze3d.platform.GlStateManager
import net.minecraft.client.MinecraftClient import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper 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.client.sound.PositionedSoundInstance
import net.minecraft.sound.SoundEvent import net.minecraft.sound.SoundEvent
import net.shadowfacts.cacao.geometry.Rect import net.shadowfacts.cacao.geometry.Rect
import org.lwjgl.opengl.GL11
/** /**
* Helper methods for rendering using Minecraft's utilities from Cacao views. * 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) { fun draw(x: Double, y: Double, u: Int, v: Int, width: Double, height: Double, textureWidth: Int, textureHeight: Int) {
if (disabled) return 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 import kotlin.reflect.KProperty
@ -9,6 +9,9 @@ class ObservableLateInitProperty<T: Any>(val observer: (T) -> Unit) {
lateinit var storage: T lateinit var storage: T
val isInitialized: Boolean
get() = this::storage.isInitialized
operator fun getValue(thisRef: Any, property: KProperty<*>): T { operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return storage return storage
} }

View File

@ -0,0 +1,21 @@
package net.shadowfacts.cacao.util.properties
import kotlin.reflect.KProperty
/**
* @author shadowfacts
*/
class ResettableLazyProperty<Value>(val initializer: () -> Value) {
var value: Value? = null
operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
if (value == null) {
value = initializer()
}
return value!!
}
fun reset() {
value = null
}
}

View File

@ -4,48 +4,76 @@ import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.NinePatchTexture import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.properties.ResettableLazyProperty
/** /**
* A helper class for drawing a [NinePatchTexture] in a view. * A helper class for drawing a [NinePatchTexture] in a view.
* `NinePatchView` will draw the given nine patch texture filling its bounds. * `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 * @author shadowfacts
* @param ninePatch The nine patch texture that this view will draw. * @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 // Corners
private val topLeft: Rect by lazy { private val topLeftDelegate = ResettableLazyProperty {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
} }
private val topRight by lazy { protected open val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
} }
private val bottomLeft by lazy { protected open val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
} }
private val bottomRight by lazy { protected open val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble()) Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
} }
protected open val bottomRight by bottomRightDelegate
// Edges // Edges
private val topMiddle by lazy { private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble()) Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble())
} }
private val bottomMiddle by lazy { protected open val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height) Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
} }
private val leftMiddle by lazy { protected open val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight) Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight)
} }
private val rightMiddle by lazy { protected open val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height) Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
} }
protected open val rightMiddle by rightMiddleDelegate
// Center // Center
private val center by lazy { private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height) Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
} }
protected open 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)
}
override fun drawContent(mouse: Point, delta: Float) { override fun drawContent(mouse: Point, delta: Float) {
drawCorners() drawCorners()
@ -63,8 +91,8 @@ class NinePatchView(val ninePatch: NinePatchTexture): View() {
private fun drawEdges() { private fun drawEdges() {
// Horizontal // Horizontal
for (i in 0 until (topMiddle.width.toInt() / ninePatch.centerWidth)) { 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(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(), 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(), bottomMiddle.height, ninePatch.texture.width, ninePatch.texture.height)
} }
val remWidth = topMiddle.width.toInt() % ninePatch.centerWidth val remWidth = topMiddle.width.toInt() % ninePatch.centerWidth
if (remWidth > 0) { if (remWidth > 0) {

View File

@ -2,8 +2,10 @@ package net.shadowfacts.cacao.view
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.* import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.* import net.shadowfacts.cacao.util.*
import net.shadowfacts.cacao.util.properties.ObservableLateInitProperty
import no.birkett.kiwi.Constraint import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver import no.birkett.kiwi.Solver
import java.util.* import java.util.*
@ -16,6 +18,13 @@ import java.util.*
*/ */
open class View() { 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. * 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. * Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
@ -78,6 +87,12 @@ open class View() {
*/ */
lateinit var bounds: Rect lateinit var bounds: Rect
/**
* The position on the Z-axis of this view.
* Views are rendered from lowest Z index to highest. Clicks are handled from highest to lowest.
*/
var zIndex: Double = 0.0
/** /**
* The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no * The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no
* intrinsic size. * intrinsic size.
@ -104,6 +119,9 @@ open class View() {
var superview: View? = null var superview: View? = null
// _subviews is the internal, mutable object since we only want it to by mutated by the add/removeSubview methods // _subviews is the internal, mutable object since we only want it to by mutated by the add/removeSubview methods
private val _subviews = LinkedList<View>() private val _subviews = LinkedList<View>()
private var subviewsSortedByZIndex: List<View> = listOf()
/** /**
* The list of all the subviews of this view. * The list of all the subviews of this view.
* This list should never by mutated directly, only by the [addSubview]/[removeSubview] methods. * This list should never by mutated directly, only by the [addSubview]/[removeSubview] methods.
@ -143,17 +161,31 @@ open class View() {
*/ */
fun <T: View> addSubview(view: T): T { fun <T: View> addSubview(view: T): T {
_subviews.add(view) _subviews.add(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = this view.superview = this
view.solver = solver view.solver = solver
view.window = window
view.wasAdded() view.wasAdded()
return view return view
} }
/**
* Finds all subviews that contain the given point.
*
* @param point The point to find subviews for, in the coordinate system of this view.
* @return All views that contain the given point.
*/
fun subviewsAtPoint(point: Point): List<View> {
return subviews.filter { point in it.frame }
}
/** /**
* Attempts to find a subview which contains the given point. * Attempts to find a subview which contains the given point.
* If multiple subviews contain the given point, which one this method returns is undefined. * If multiple subviews contain the given point, which one this method returns is undefined.
* [subviewsAtPoint] may be used, and the resulting List sorted by [View.zIndex].
* *
* @param point The point to find a subview for, in the coordinate system of this view. * @param point The point to find a subview for, in the coordinate system of this view.
* @return The view, if any, that contains the given point. * @return The view, if any, that contains the given point.
@ -233,7 +265,7 @@ open class View() {
drawContent(mouse, delta) drawContent(mouse, delta)
subviews.forEach { subviewsSortedByZIndex.forEach {
val mouseInView = convert(mouse, to = it) val mouseInView = convert(mouse, to = it)
it.draw(mouseInView, delta) it.draw(mouseInView, delta)
} }
@ -261,7 +293,7 @@ open class View() {
* @return Whether the mouse click was handled by this view or any subviews. * @return Whether the mouse click was handled by this view or any subviews.
*/ */
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val view = subviewAtPoint(point) val view = subviewsAtPoint(point).maxBy(View::zIndex)
if (view != null) { if (view != null) {
val pointInView = convert(point, to = view) val pointInView = convert(point, to = view)
return view.mouseClicked(pointInView, mouseButton) return view.mouseClicked(pointInView, mouseButton)

View File

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