Compare commits
3 Commits
3902b75a27
...
4d1fb68c89
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 4d1fb68c89 | |
Shadowfacts | b2499ad247 | |
Shadowfacts | 27bad18931 |
|
@ -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}")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
if (minecraft != null) {
|
||||||
|
// workaround this.minecraft sometimes being null causing a crash
|
||||||
renderBackground()
|
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))
|
||||||
|
when (result) {
|
||||||
|
true ->
|
||||||
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
|
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
|
||||||
|
false ->
|
||||||
|
if (windows.size > 1) {
|
||||||
|
removeWindow(windows.last())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return result == true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -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)
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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
|
|
||||||
if (handler != null) {
|
|
||||||
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
|
// We can perform an unchecked cast here because we are certain that Impl will be the concrete implementation
|
||||||
// of AbstractButton.
|
// of AbstractButton.
|
||||||
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
|
// For example, an implementing class may be defined as such: `class Button: AbstractButton<Button>`
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
handler(this as Impl)
|
handler?.invoke(this as Impl)
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 |
Loading…
Reference in New Issue