Compare commits

..

4 Commits

17 changed files with 502 additions and 131 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.DS_Store .DS_Store
logs/
# gradle # gradle

View File

@ -1,16 +1,16 @@
package net.shadowfacts.asmr package net.shadowfacts.asmr
import net.minecraft.util.Identifier
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.CacaoScreen import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.Window import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis import net.shadowfacts.cacao.geometry.Axis
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.MouseButton import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper import net.shadowfacts.cacao.util.Texture
import net.shadowfacts.cacao.view.StackView import net.shadowfacts.cacao.view.*
import net.shadowfacts.cacao.view.button.Button
/** /**
* @author shadowfacts * @author shadowfacts
@ -22,33 +22,24 @@ class TestCacaoScreen: CacaoScreen() {
val stack = addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER).apply { val stack = addView(StackView(Axis.VERTICAL, StackView.Distribution.CENTER).apply {
backgroundColor = Color.WHITE backgroundColor = Color.WHITE
}) })
val red = stack.addArrangedSubview(View().apply { val red = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16)).apply {
intrinsicContentSize = Size(50.0, 50.0) intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color(0xff0000)
}) })
val green = stack.addArrangedSubview(View().apply { val buttonNinePatch = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 66), 3, 3, 194, 14)
val green = stack.addArrangedSubview(NinePatchView(buttonNinePatch).apply {
intrinsicContentSize = Size(75.0, 100.0) intrinsicContentSize = Size(75.0, 100.0)
backgroundColor = Color(0x00ff00)
}) })
val blue = stack.addArrangedSubview(View().apply { val blue = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0) intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color(0x0000ff) backgroundColor = Color(0x0000ff)
}) })
val purple = blue.addSubview(object: View() { val purple = blue.addSubview(Button(Label("Hello, button!").apply {
init { textColor = Color.WHITE
intrinsicContentSize = Size(25.0, 25.0) }).apply {
backgroundColor = Color(0x800080) handler = {
} println("$it clicked!")
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
println("hello world")
}
override fun drawContent(mouse: Point, delta: Float) {
if (mouse in bounds) {
RenderHelper.fill(bounds, Color.WHITE)
}
} }
background = NinePatchView(buttonNinePatch)
}) })
solver.dsl { solver.dsl {

View File

@ -35,6 +35,8 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
} }
override fun render(mouseX: Int, mouseY: Int, delta: Float) { override fun render(mouseX: Int, mouseY: Int, delta: Float) {
renderBackground()
val mouse = Point(mouseX, mouseY) val mouse = Point(mouseX, mouseY)
windows.forEach { windows.forEach {
it.draw(mouse, delta) it.draw(mouse, delta)

View File

@ -9,4 +9,8 @@ data class Point(val x: Double, val y: Double) {
constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble()) constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble())
companion object {
val ORIGIN = Point(0.0, 0.0)
}
} }

View File

@ -0,0 +1,41 @@
package net.shadowfacts.cacao.util
/**
* @author shadowfacts
*/
data class NinePatchTexture(val texture: Texture, val cornerWidth: Int, val cornerHeight: Int, val centerWidth: Int, val centerHeight: Int) {
// Corners
val topLeft by lazy {
texture
}
val topRight by lazy {
Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height)
}
val bottomLeft by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height)
}
val bottomRight by lazy {
Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height)
}
// Edges
val topMiddle by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height)
}
val bottomMiddle by lazy {
Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height)
}
val leftMiddle by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height)
}
val rightMiddle by lazy {
Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height)
}
// Center
val center by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v + cornerHeight, texture.width, texture.height)
}
}

View File

@ -0,0 +1,21 @@
package net.shadowfacts.cacao.util
import kotlin.reflect.KProperty
/**
* @author shadowfacts
*/
class ObservableLateInitProperty<T: Any>(val observer: (T) -> Unit) {
lateinit var storage: T
operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return storage
}
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
storage = value
observer(value)
}
}

View File

@ -1,6 +1,7 @@
package net.shadowfacts.cacao.util 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.gui.DrawableHelper import net.minecraft.client.gui.DrawableHelper
import net.shadowfacts.cacao.geometry.Rect import net.shadowfacts.cacao.geometry.Rect
@ -22,6 +23,24 @@ object RenderHelper {
DrawableHelper.fill(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb) DrawableHelper.fill(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb)
} }
/**
* Binds and draws the given [texture] filling the [rect].
*/
fun draw(rect: Rect, texture: Texture) {
if (disabled) return
color(1f, 1f, 1f, 1f)
MinecraftClient.getInstance().textureManager.bindTexture(texture.location)
draw(rect.left, rect.top, texture.u, texture.v, rect.width, rect.height, texture.width, texture.height)
}
/**
* Draws the bound texture with the given screen and texture position and size.
*/
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)
}
/** /**
* @see org.lwjgl.opengl.GL11.glPushMatrix * @see org.lwjgl.opengl.GL11.glPushMatrix
*/ */
@ -46,4 +65,9 @@ object RenderHelper {
GlStateManager.translated(x, y, z) GlStateManager.translated(x, y, z)
} }
fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return
GlStateManager.color4f(r, g, b, alpha)
}
} }

View File

@ -0,0 +1,15 @@
package net.shadowfacts.cacao.util
import net.minecraft.util.Identifier
/**
* A helper class that represents a texture.
*
* @author shadowfacts
* @param location The identifier representing the resource-pack location of the texture image.
* @param u The X coordinate in pixels of where the texture starts within the image.
* @param v The Y coordinate in pixels of where the texture starts within the image.
* @param width The width in pixels of the entire image.
* @param height The height in pixels of the entire image.
*/
data class Texture(val location: Identifier, val u: Int, val v: Int, val width: Int = 256, val height: Int = 256)

View File

@ -0,0 +1,103 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper
/**
* @author shadowfacts
*/
class NinePatchView(val ninePatch: NinePatchTexture): View() {
// Corners
private val topLeft: Rect by lazy {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
private val topRight by lazy {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
private val bottomLeft by lazy {
Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
private val bottomRight by lazy {
Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
// Edges
private val topMiddle by lazy {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble())
}
private val bottomMiddle by lazy {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
}
private val leftMiddle by lazy {
Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight)
}
private val rightMiddle by lazy {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
}
// Center
private val center by lazy {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
}
override fun drawContent(mouse: Point, delta: Float) {
drawCorners()
drawEdges()
drawCenter()
}
private fun drawCorners() {
RenderHelper.draw(topLeft, ninePatch.topLeft)
RenderHelper.draw(topRight, ninePatch.topRight)
RenderHelper.draw(bottomLeft, ninePatch.bottomLeft)
RenderHelper.draw(bottomRight, ninePatch.bottomRight)
}
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)
}
val remWidth = topMiddle.width.toInt() % ninePatch.centerWidth
if (remWidth > 0) {
RenderHelper.draw(topMiddle.right - remWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, remWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(bottomMiddle.right - remWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, remWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
}
// Vertical
for (i in 0 until (leftMiddle.height.toInt() / ninePatch.centerHeight)) {
RenderHelper.draw(leftMiddle.left, leftMiddle.top + i * ninePatch.centerHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(rightMiddle.left, rightMiddle.top + i * ninePatch.centerHeight, ninePatch.rightMiddle.u, ninePatch.rightMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
}
val remHeight = leftMiddle.height.toInt() % ninePatch.centerHeight
if (remHeight > 0) {
RenderHelper.draw(leftMiddle.left, leftMiddle.bottom - remHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), remHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(rightMiddle.left, rightMiddle.bottom - remHeight, ninePatch.rightMiddle.u, ninePatch.rightMiddle.v, ninePatch.cornerWidth.toDouble(), remHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
}
}
private fun drawCenter() {
for (i in 0 until (center.height.toInt() / ninePatch.centerHeight)) {
drawCenterRow(center.top + i * ninePatch.centerHeight.toDouble(), ninePatch.centerHeight.toDouble())
}
val remHeight = center.height.toInt() % ninePatch.centerHeight
if (remHeight > 0) {
drawCenterRow(center.bottom - remHeight, remHeight.toDouble())
}
}
private fun drawCenterRow(y: Double, height: Double) {
for (i in 0 until (center.width.toInt() / ninePatch.centerWidth)) {
RenderHelper.draw(center.left + i * ninePatch.centerWidth, y, ninePatch.center.u, ninePatch.center.v, ninePatch.centerWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height)
}
val remWidth = center.width.toInt() % ninePatch.centerWidth
if (remWidth > 0) {
RenderHelper.draw(center.right - remWidth, y, ninePatch.center.u, ninePatch.center.v, remWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height)
}
}
}

View File

@ -0,0 +1,19 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.Texture
/**
* A helper class for drawing a [Texture] in a view.
* `TextureView` will draw the given texture filling the bounds of the view.
*
* @author shadowfacts
*/
class TextureView(val texture: Texture): View() {
override fun drawContent(mouse: Point, delta: Float) {
RenderHelper.draw(bounds, texture)
}
}

View File

@ -1,13 +1,9 @@
package net.shadowfacts.cacao.view package net.shadowfacts.cacao.view
import com.mojang.blaze3d.platform.GlStateManager
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.geometry.* import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.Color import net.shadowfacts.cacao.util.*
import net.shadowfacts.cacao.util.LowestCommonAncestor
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
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.*
@ -18,7 +14,7 @@ import java.util.*
* *
* @author shadowfacts * @author shadowfacts
*/ */
open class View { open class View() {
/** /**
* 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.
@ -60,13 +56,25 @@ open class View {
val centerYAnchor = LayoutVariable(this, "centerY") val centerYAnchor = LayoutVariable(this, "centerY")
/** /**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview). * Whether this view uses constraint-based layout.
* Not initialized until [didLayout] called. * If `false`, the view's `frame` must be set manually and the layout anchors may not be used.
* Note: some views (such as [StackView] require arranged subviews to use constraint based layout.
*
* Default is `true`.
*/ */
lateinit var frame: Rect var usesConstraintBasedLayout = true
/**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
* If using constraint based layout, this property is not initialized until [didLayout] called.
* Otherwise, this must be set manually.
* Setting this property updates the [bounds].
*/
var frame: Rect by ObservableLateInitProperty { this.bounds = Rect(Point.ORIGIN, it.size) }
/** /**
* The rectangle for this view in its own coordinate system. * The rectangle for this view in its own coordinate system.
* Not initialized until [didLayout] called. * If using constraint based layout, this property is not initialized until [didLayout] called.
* Otherwise, this will be initialized when [frame] is set.
*/ */
lateinit var bounds: Rect lateinit var bounds: Rect
@ -102,6 +110,11 @@ open class View {
*/ */
val subviews: List<View> = _subviews val subviews: List<View> = _subviews
constructor(frame: Rect): this() {
this.usesConstraintBasedLayout = false
this.frame = frame
}
/** /**
* Helper method for retrieve the anchor for a specific position on the given axis. * Helper method for retrieve the anchor for a specific position on the given axis.
*/ */
@ -163,6 +176,8 @@ open class View {
* If overridden, the super-class method must be called. * If overridden, the super-class method must be called.
*/ */
open fun createInternalConstraints() { open fun createInternalConstraints() {
if (!usesConstraintBasedLayout) return
solver.dsl { solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor) rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor) bottomAnchor equalTo (topAnchor + heightAnchor)
@ -172,7 +187,7 @@ open class View {
} }
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) { private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!this::solver.isInitialized) return if (!usesConstraintBasedLayout || !this::solver.isInitialized) return
if (old != null) { if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!) solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
@ -193,23 +208,26 @@ open class View {
open fun didLayout() { open fun didLayout() {
subviews.forEach(View::didLayout) subviews.forEach(View::didLayout)
val superviewLeft = superview?.leftAnchor?.value ?: 0.0 if (usesConstraintBasedLayout) {
val superviewTop = superview?.topAnchor?.value ?: 0.0 val superviewLeft = superview?.leftAnchor?.value ?: 0.0
frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value) val superviewTop = superview?.topAnchor?.value ?: 0.0
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value) frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value)
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value)
}
} }
/** /**
* Called to draw this view. * Called to draw this view.
* This method should not be called directly, it is called by the parent view/window. * This method should not be called directly, it is called by the parent view/window.
* This method may not be overridden, use [drawContent] to draw any custom content. * This method generally should not be overridden, but it is left open for internal framework use.
* Use [drawContent] to draw any custom content.
* *
* @param mouse The position of the mouse in the coordinate system of this view. * @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame. * @param delta The time since the last frame.
*/ */
fun draw(mouse: Point, delta: Float) { open fun draw(mouse: Point, delta: Float) {
RenderHelper.pushMatrix() RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top, 0.0) RenderHelper.translate(frame.left, frame.top)
RenderHelper.fill(bounds, backgroundColor) RenderHelper.fill(bounds, backgroundColor)

View File

@ -0,0 +1,115 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
/**
* A abstract button class. Cannot be constructed directly, used for creating button implementations with their own logic.
* Use [Button] for a generic no-frills button.
*
* @author shadowfacts
* @param Impl The type of the concrete implementation of the button.
* Used to allow the [handler] to receive the exact button type that was constructed.
* @param content The [View] that provides the content of this button.
* Will be added as a subview of the button and laid out using constraints.
* @param padding The padding between the [content] and the edges of the button.
*/
abstract class AbstractButton<Impl: AbstractButton<Impl>>(val content: View, val padding: Double = 4.0): View() {
/**
* The function that handles when this button is clicked.
* The parameter is the type of the concrete button implementation that was used.
*/
var handler: ((Impl) -> Unit)? = null
/**
* Whether the button is disabled.
* Disabled buttons have a different background ([disabledBackground]) and do not receive click events.
*/
var disabled = false
/**
* The normal background view to draw behind the button content. It will be added as a subview during [wasAdded],
* so all background view properties must be specified prior to the button being added to a view hierarchy.
*
* The background will fill the entire button (going beneath the content [padding]).
* There are also [hoveredBackground] and [disabledBackground] for those states.
* 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.
*/
var background: View? = null
/**
* The background to draw when the button is hovered over by the mouse.
* If `null`, the normal [background] will be used.
* @see background
*/
var hoveredBackground: View? = null
/**
* The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used.
* @see background
*/
var disabledBackground: View? = null
override fun wasAdded() {
solver.dsl {
addSubview(content)
content.leftAnchor equalTo (leftAnchor + padding)
content.rightAnchor equalTo (rightAnchor - padding)
content.topAnchor equalTo (topAnchor + padding)
content.bottomAnchor equalTo (bottomAnchor - padding)
listOfNotNull(background, hoveredBackground, disabledBackground).forEach {
addSubview(it)
it.leftAnchor equalTo leftAnchor
it.rightAnchor equalTo rightAnchor
it.topAnchor equalTo topAnchor
it.bottomAnchor equalTo bottomAnchor
}
}
super.wasAdded()
}
override fun draw(mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top)
RenderHelper.fill(bounds, backgroundColor)
var currentBackground: View? = background
if (mouse in bounds) {
currentBackground = hoveredBackground ?: currentBackground
}
if (disabled) {
currentBackground = disabledBackground ?: currentBackground
}
// don't need to convert mouse to background coordinate system
// the edges are all pinned, so the coordinate space is the same
currentBackground?.draw(mouse, delta)
val mouseInContent = convert(mouse, to = content)
content.draw(mouseInContent, delta)
// don't draw subviews, otherwise all background views + content will get drawn
RenderHelper.popMatrix()
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
if (disabled) return
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)
}
}
}

View File

@ -0,0 +1,12 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.view.View
/**
* A simple button implementation that provides no additional logic.
*
* @author shadowfacts
* @param content The [View] that provides the content of this button.
* @param padding The padding between the [content] and the edges of the button.
*/
class Button(content: View, padding: Double = 4.0): AbstractButton<Button>(content, padding)

View File

@ -21,12 +21,8 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertToParent() { fun testConvertToParent() {
val a = window.addView(View().apply { val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val b = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
})
val b = a.addSubview(View().apply {
frame = Rect(25.0, 25.0, 50.0, 50.0)
})
assertEquals(Point(25.0, 25.0), b.convert(Point(0.0, 0.0), to = a)) assertEquals(Point(25.0, 25.0), b.convert(Point(0.0, 0.0), to = a))
assertEquals(Point(75.0, 75.0), b.convert(Point(50.0, 50.0), to = a)) assertEquals(Point(75.0, 75.0), b.convert(Point(50.0, 50.0), to = a))
@ -35,15 +31,9 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertToSibling() { fun testConvertToSibling() {
val root = window.addView(View().apply { val root = window.addView(View(Rect(0.0, 0.0, 200.0, 200.0)))
frame = Rect(0.0, 0.0, 200.0, 200.0) val a = root.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
}) val b = root.addSubview(View(Rect(75.0, 75.0, 50.0, 50.0)))
val a = root.addSubview(View().apply {
frame = Rect(25.0, 25.0, 50.0, 50.0)
})
val b = root.addSubview(View().apply {
frame = Rect(75.0, 75.0, 50.0, 50.0)
})
assertEquals(Point(-50.0, -50.0), a.convert(Point(0.0, 0.0), to = b)) assertEquals(Point(-50.0, -50.0), a.convert(Point(0.0, 0.0), to = b))
assertEquals(Point(100.0, 100.0), b.convert(Point(50.0, 50.0), to = a)) assertEquals(Point(100.0, 100.0), b.convert(Point(50.0, 50.0), to = a))
@ -52,21 +42,11 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertBetweenSubtrees() { fun testConvertBetweenSubtrees() {
val root = window.addView(View().apply { val root = window.addView(View(Rect(0.0, 0.0, 200.0, 100.0)))
frame = Rect(0.0, 0.0, 200.0, 100.0) val a = root.addSubview(View(Rect(0.0, 0.0, 100.0, 100.0)))
}) val b = root.addSubview(View(Rect(100.0, 0.0, 100.0, 100.0)))
val a = root.addSubview(View().apply { val c = a.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val d = b.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0)))
})
val b = root.addSubview(View().apply {
frame = Rect(100.0, 0.0, 100.0, 100.0)
})
val c = a.addSubview(View().apply {
frame = Rect(0.0, 0.0, 50.0, 50.0)
})
val d = b.addSubview(View().apply {
frame = Rect(0.0, 0.0, 50.0, 50.0)
})
assertEquals(Point(-100.0, 0.0), c.convert(Point(0.0, 0.0), to = b)) assertEquals(Point(-100.0, 0.0), c.convert(Point(0.0, 0.0), to = b))
assertEquals(Point(-50.0, 50.0), c.convert(Point(50.0, 50.0), to = d)) assertEquals(Point(-50.0, 50.0), c.convert(Point(50.0, 50.0), to = d))
@ -75,12 +55,8 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertBetweenTopLevelViews() { fun testConvertBetweenTopLevelViews() {
val a = window.addView(View().apply { val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val b = window.addView(View(Rect(100.0, 100.0, 100.0, 100.0)))
})
val b = window.addView(View().apply {
frame = Rect(100.0, 100.0, 100.0, 100.0)
})
assertEquals(Point(0.0, 0.0), a.convert(Point(100.0, 100.0), to = b)) assertEquals(Point(0.0, 0.0), a.convert(Point(100.0, 100.0), to = b))
assertEquals(Point(200.0, 200.0), b.convert(Point(100.0, 100.0), to = a)) assertEquals(Point(200.0, 200.0), b.convert(Point(100.0, 100.0), to = a))
@ -89,18 +65,10 @@ class CoordinateConversionTests {
@Test @Test
fun testConvertBetweenTopLevelSubtrees() { fun testConvertBetweenTopLevelSubtrees() {
val a = window.addView(View().apply { val a = window.addView(View(Rect(0.0, 0.0, 100.0, 100.0)))
frame = Rect(0.0, 0.0, 100.0, 100.0) val b = window.addView(View(Rect(100.0, 100.0, 100.0, 100.0)))
}) val c = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
val b = window.addView(View().apply { val d = b.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
frame = Rect(100.0, 100.0, 100.0, 100.0)
})
val c = a.addSubview(View().apply {
frame = Rect(25.0, 25.0, 50.0, 50.0)
})
val d = b.addSubview(View().apply {
frame = Rect(25.0, 25.0, 50.0, 50.0)
})
assertEquals(Point(-50.0, -50.0), c.convert(Point(50.0, 50.0), to = d)) assertEquals(Point(-50.0, -50.0), c.convert(Point(50.0, 50.0), to = d))
assertEquals(Point(100.0, 100.0), d.convert(Point(0.0, 0.0), to = c)) assertEquals(Point(100.0, 100.0), d.convert(Point(0.0, 0.0), to = c))

View File

@ -0,0 +1,70 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.kiwidsl.dsl
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class ButtonClickTests {
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
}
@Test
fun testClickInsideButton() {
val clicked = CompletableFuture<Boolean>()
val content = View().apply {
intrinsicContentSize = Size(25.0, 25.0)
}
val button = window.addView(Button(content).apply {
handler = {
clicked.complete(true)
}
})
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
}
window.layout()
window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT)
assertTrue(clicked.getNow(false))
}
@Test
fun testClickOutsideButton() {
val clicked = CompletableFuture<Boolean>()
val content = View().apply {
intrinsicContentSize = Size(25.0, 25.0)
}
val button = window.addView(Button(content).apply {
handler = {
clicked.complete(true)
}
})
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
}
window.layout()
window.mouseClicked(Point(50.0, 50.0), MouseButton.LEFT)
assertFalse(clicked.getNow(false))
}
}

View File

@ -24,11 +24,7 @@ class ViewClickTests {
@Test @Test
fun testClickInsideRootView() { fun testClickInsideRootView() {
val mouse = CompletableFuture<Point>() val mouse = CompletableFuture<Point>()
window.addView(object: View() { window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
mouse.complete(point) mouse.complete(point)
} }
@ -41,11 +37,7 @@ class ViewClickTests {
@Test @Test
fun testClickOutsideRootView() { fun testClickOutsideRootView() {
val clicked = CompletableFuture<Boolean>() val clicked = CompletableFuture<Boolean>()
window.addView(object: View() { window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
clicked.complete(true) clicked.complete(true)
} }
@ -58,14 +50,8 @@ class ViewClickTests {
@Test @Test
fun testClickInsideNestedView() { fun testClickInsideNestedView() {
val mouse = CompletableFuture<Point>() val mouse = CompletableFuture<Point>()
val root = window.addView(View().apply { val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0)))
frame = Rect(50.0, 50.0, 100.0, 100.0) root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
mouse.complete(point) mouse.complete(point)
} }
@ -78,14 +64,8 @@ class ViewClickTests {
@Test @Test
fun testClickOutsideNestedView() { fun testClickOutsideNestedView() {
val clicked = CompletableFuture<Boolean>() val clicked = CompletableFuture<Boolean>()
val root = window.addView(View().apply { val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0)))
frame = Rect(50.0, 50.0, 100.0, 100.0) root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) { override fun mouseClicked(point: Point, mouseButton: MouseButton) {
clicked.complete(true) clicked.complete(true)
} }

View File

@ -32,12 +32,7 @@ class ViewHoverTests {
@Test @Test
fun testHoverRootView() { fun testHoverRootView() {
val point = CompletableFuture<Point>() val point = CompletableFuture<Point>()
window.addView(object: View() { window.addView(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
bounds = Rect(0.0, 0.0, 100.0, 100.0)
}
override fun drawContent(mouse: Point, delta: Float) { override fun drawContent(mouse: Point, delta: Float) {
point.complete(mouse) point.complete(mouse)
} }
@ -50,16 +45,8 @@ class ViewHoverTests {
@Test @Test
fun testHoverNestedView() { fun testHoverNestedView() {
val point = CompletableFuture<Point>() val point = CompletableFuture<Point>()
val root = window.addView(View().apply { val root = window.addView(View(Rect(50.0, 50.0, 100.0, 100.0)))
frame = Rect(50.0, 50.0, 100.0, 100.0) root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
bounds = Rect(0.0, 0.0, 100.0, 100.0)
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
bounds = Rect(0.0, 0.0, 50.0, 50.0)
}
override fun drawContent(mouse: Point, delta: Float) { override fun drawContent(mouse: Point, delta: Float) {
point.complete(mouse) point.complete(mouse)
} }