Compare commits

...

5 Commits

Author SHA1 Message Date
Shadowfacts df3523347c
Add ToggleButton 2019-06-23 22:21:59 -04:00
Shadowfacts f2d7e0a656
Fix button test package 2019-06-23 21:56:58 -04:00
Shadowfacts 204731e03c
Add EnumButton 2019-06-23 21:55:42 -04:00
Shadowfacts b48b72d5bb
Add EnumHelper 2019-06-23 21:29:11 -04:00
Shadowfacts 4070baaa63
Add default button backgrounds and click sound 2019-06-23 21:22:54 -04:00
15 changed files with 368 additions and 16 deletions

View File

@ -1,6 +1,7 @@
package net.shadowfacts.asmr package net.shadowfacts.asmr
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.shadowfacts.asmr.util.RedstoneMode
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.CacaoScreen import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window import net.shadowfacts.cacao.Window
@ -11,6 +12,8 @@ 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.Button
import net.shadowfacts.cacao.view.button.EnumButton
import net.shadowfacts.cacao.view.button.ToggleButton
/** /**
* @author shadowfacts * @author shadowfacts
@ -33,13 +36,10 @@ 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(Button(Label("Hello, button!").apply { val purple = blue.addSubview(ToggleButton(false).apply {
textColor = Color.WHITE
}).apply {
handler = { handler = {
println("$it clicked!") println("enum button clicked, new value: ${it.state}")
} }
background = NinePatchView(buttonNinePatch)
}) })
solver.dsl { solver.dsl {
@ -48,6 +48,7 @@ class TestCacaoScreen: CacaoScreen() {
stack.rightAnchor equalTo 150 stack.rightAnchor equalTo 150
purple.centerXAnchor equalTo blue.centerXAnchor purple.centerXAnchor equalTo blue.centerXAnchor
purple.centerYAnchor equalTo blue.centerYAnchor purple.centerYAnchor equalTo blue.centerYAnchor
// purple.widthAnchor equalTo 50
} }
layout() layout()

View File

@ -0,0 +1,14 @@
package net.shadowfacts.asmr.util
/**
* @author shadowfacts
*/
enum class RedstoneMode {
HIGH, LOW, TOGGLE;
companion object {
fun localize(value: RedstoneMode): String {
return value.name.toLowerCase().capitalize()
}
}
}

View File

@ -0,0 +1,20 @@
package net.shadowfacts.cacao.util
/**
* @author shadowfacts
*/
object EnumHelper {
fun <E: Enum<E>> next(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) + 1
return if (index < constants.size) constants[index] else constants.first()
}
fun <E: Enum<E>> previous(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) - 1
return if (index >= 0) constants[index] else constants.last()
}
}

View File

@ -4,14 +4,15 @@ package net.shadowfacts.cacao.util
* @author shadowfacts * @author shadowfacts
*/ */
enum class MouseButton { enum class MouseButton {
LEFT, RIGHT, MIDDLE; LEFT, RIGHT, MIDDLE, UNKNOWN;
companion object { companion object {
fun fromMC(button: Int): MouseButton { fun fromMC(button: Int): MouseButton {
return when (button) { return when (button) {
0 -> LEFT
1 -> RIGHT 1 -> RIGHT
2 -> MIDDLE 2 -> MIDDLE
else -> LEFT else -> UNKNOWN
} }
} }
} }

View File

@ -3,6 +3,8 @@ 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.sound.PositionedSoundInstance
import net.minecraft.sound.SoundEvent
import net.shadowfacts.cacao.geometry.Rect import net.shadowfacts.cacao.geometry.Rect
/** /**
@ -13,7 +15,13 @@ import net.shadowfacts.cacao.geometry.Rect
*/ */
object RenderHelper { object RenderHelper {
private val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean() val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean()
// TODO: find a better place for this
fun playSound(event: SoundEvent) {
if (disabled) return
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(event, 1f))
}
/** /**
* Draws a solid [rect] filled with the given [color]. * Draws a solid [rect] filled with the given [color].
@ -65,6 +73,9 @@ object RenderHelper {
GlStateManager.translated(x, y, z) GlStateManager.translated(x, y, z)
} }
/**
* @see org.lwjgl.opengl.GL11.glColor4f
*/
fun color(r: Float, g: Float, b: Float, alpha: Float) { fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return if (disabled) return
GlStateManager.color4f(r, g, b, alpha) GlStateManager.color4f(r, g, b, alpha)

View File

@ -5,6 +5,7 @@ import net.minecraft.client.font.TextRenderer
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.RenderHelper
/** /**
* A simple View that displays text. Allows for controlling the color and shadow of the text. Label cannot be used * A simple View that displays text. Allows for controlling the color and shadow of the text. Label cannot be used
@ -29,7 +30,7 @@ class Label(text: String): View() {
updateIntrinsicContentSize() updateIntrinsicContentSize()
} }
var textColor = Color(0x404040) var textColor = Color.WHITE
override fun wasAdded() { override fun wasAdded() {
super.wasAdded() super.wasAdded()
@ -38,6 +39,8 @@ class Label(text: String): View() {
} }
private fun updateIntrinsicContentSize() { private fun updateIntrinsicContentSize() {
if (RenderHelper.disabled) return
val width = textRenderer.getStringWidth(text) val width = textRenderer.getStringWidth(text)
val height = textRenderer.fontHeight val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble()) intrinsicContentSize = Size(width.toDouble(), height.toDouble())

View File

@ -10,7 +10,7 @@ import net.shadowfacts.cacao.util.Texture
* *
* @author shadowfacts * @author shadowfacts
*/ */
class TextureView(val texture: Texture): View() { class TextureView(var texture: Texture): View() {
override fun drawContent(mouse: Point, delta: Float) { override fun drawContent(mouse: Point, delta: Float) {
RenderHelper.draw(bounds, texture) RenderHelper.draw(bounds, texture)

View File

@ -1,8 +1,13 @@
package net.shadowfacts.cacao.view.button 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.geometry.Point
import net.shadowfacts.cacao.util.MouseButton import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.Texture
import net.shadowfacts.cacao.view.NinePatchView
import net.shadowfacts.cacao.view.View import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
@ -19,6 +24,12 @@ import net.shadowfacts.kiwidsl.dsl
*/ */
abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val padding: Double = 4.0): View() { abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val padding: Double = 4.0): View() {
companion object {
val DEFAULT_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 66), 3, 3, 194, 14)
val HOVERED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 86), 3, 3, 194, 14)
val DISABLED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 46), 3, 3, 194, 14)
}
/** /**
* The function that handles when this button is clicked. * The function that handles when this button is clicked.
* The parameter is the type of the concrete button implementation that was used. * The parameter is the type of the concrete button implementation that was used.
@ -40,19 +51,24 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
* If a [backgroundColor] is specified, it will be drawn behind the background View and thus not visible * If a [backgroundColor] is specified, it will be drawn behind the background View and thus not visible
* unless the background view is not fully opaque. * unless the background view is not fully opaque.
*/ */
var background: View? = null var background: View? = NinePatchView(DEFAULT_BG)
/** /**
* The background to draw when the button is hovered over by the mouse. * The background to draw when the button is hovered over by the mouse.
* If `null`, the normal [background] will be used. * If `null`, the normal [background] will be used.
* @see background * @see background
*/ */
var hoveredBackground: View? = null var hoveredBackground: View? = NinePatchView(HOVERED_BG)
/** /**
* The background to draw when the button is [disabled]. * The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used. * If `null`, the normal [background] will be used.
* @see background * @see background
*/ */
var disabledBackground: View? = null var disabledBackground: View? = NinePatchView(DISABLED_BG)
/**
* If the button will play the Minecraft button click sound when clicked.
*/
var clickSoundEnabled = true
override fun wasAdded() { override fun wasAdded() {
solver.dsl { solver.dsl {
@ -109,6 +125,10 @@ abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val
// 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(this as Impl)
if (clickSoundEnabled && !RenderHelper.disabled) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
}
} }
} }

View File

@ -0,0 +1,45 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.EnumHelper
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.Label
/**
* A button that cycles through enum values.
* Left click: forwards
* Right click: backwards
* All other mouse buttons call the handler with the unchanged value
*
* @author shadowfacts
* @param initialValue The initial enum value for this button.
* @param localizer A function that takes an enum value and converts into a string for the button's label.
*/
class EnumButton<E: Enum<E>>(initialValue: E, val localizer: (E) -> String): AbstractButton<EnumButton<E>>(Label(localizer(initialValue))) {
private val label: Label
get() = content as Label
/**
* The current value of the enum button.
* Updating this property will use the [localizer] to update the label.
*/
var value: E = initialValue
set(value) {
field = value
label.text = localizer(value)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
if (!disabled) {
value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value)
else -> value
}
}
super.mouseClicked(point, mouseButton)
}
}

View File

@ -0,0 +1,46 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.Texture
import net.shadowfacts.cacao.view.TextureView
/**
* A button for toggling between on/off states.
*
* @author shadowfacts
* @param initialState Whether the button starts as on or off.
*/
class ToggleButton(initialState: Boolean): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF).apply {
intrinsicContentSize = Size(19.0, 19.0)
}, padding = 0.0) {
companion object {
val ON = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 0)
val OFF = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 19)
}
private val textureView: TextureView
get() = content as TextureView
/**
* The button's current on/off state.
* Updating this property updates the button's texture.
*/
var state: Boolean = initialState
set(value) {
field = value
textureView.texture = if (value) ON else OFF
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
state = !state
}
super.mouseClicked(point, mouseButton)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -0,0 +1,27 @@
package net.shadowfacts.cacao.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class EnumHelperTests {
enum class MyEnum {
ONE, TWO
}
@Test
fun testNext() {
assertEquals(MyEnum.TWO, EnumHelper.next(MyEnum.ONE))
assertEquals(MyEnum.ONE, EnumHelper.next(MyEnum.TWO))
}
@Test
fun testPrev() {
assertEquals(MyEnum.ONE, EnumHelper.previous(MyEnum.TWO))
assertEquals(MyEnum.TWO, EnumHelper.previous(MyEnum.ONE))
}
}

View File

@ -1,11 +1,10 @@
package net.shadowfacts.cacao.view package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.Window import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.button.Button import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import org.junit.jupiter.api.Assertions.assertFalse import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Assertions.assertTrue

View File

@ -0,0 +1,93 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class EnumButtonTests {
companion object {
@BeforeAll
@JvmStatic
fun setupAll() {
System.setProperty("cacao.drawing.disabled", "true")
}
}
enum class MyEnum {
ONE, TWO, THREE
}
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
}
@Test
fun testHandlerCalled() {
val called = CompletableFuture<Boolean>()
val button = window.addView(EnumButton(MyEnum.ONE, MyEnum::name).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
handler = {
called.complete(true)
}
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertTrue(called.getNow(false))
assertEquals(MyEnum.TWO, button.value)
}
@Test
fun testCyclesValues() {
val button = window.addView(EnumButton(MyEnum.ONE, MyEnum::name).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertEquals(MyEnum.TWO, button.value)
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertEquals(MyEnum.THREE, button.value)
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertEquals(MyEnum.ONE, button.value)
}
@Test
fun testCyclesValuesBackwards() {
val button = window.addView(EnumButton(MyEnum.ONE, MyEnum::name).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = Rect(0.0, 0.0, 25.0, 25.0)
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertEquals(MyEnum.TWO, button.value)
window.mouseClicked(Point(5.0, 5.0), MouseButton.RIGHT)
assertEquals(MyEnum.ONE, button.value)
}
@Test
fun testMiddleClickDoesNotChangeValue() {
val button = window.addView(EnumButton(MyEnum.ONE, MyEnum::name).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = Rect(0.0, 0.0, 25.0, 25.0)
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.MIDDLE)
assertEquals(MyEnum.ONE, button.value)
}
}

View File

@ -0,0 +1,72 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class ToggleButtonTests {
companion object {
@BeforeAll
@JvmStatic
fun setupAll() {
System.setProperty("cacao.drawing.disabled", "true")
}
}
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
}
@Test
fun testHandlerCalled() {
val called = CompletableFuture<Boolean>()
val button = window.addView(ToggleButton(false).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
handler = {
called.complete(true)
}
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertTrue(called.getNow(false))
}
@Test
fun testTogglesValues() {
val button = window.addView(ToggleButton(false).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertTrue(button.state)
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertFalse(button.state)
}
@Test
fun testMiddleClickDoesNotChangeValue() {
val button = window.addView(ToggleButton(false).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
})
window.mouseClicked(Point(5.0, 5.0), MouseButton.MIDDLE)
assertFalse(button.state)
}
}