Add basic button implementation
This commit is contained in:
parent
bf7e7bd24c
commit
62fbc10aa3
|
@ -1,4 +1,5 @@
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
logs/
|
||||||
|
|
||||||
# gradle
|
# gradle
|
||||||
|
|
||||||
|
|
|
@ -5,12 +5,11 @@ import net.shadowfacts.cacao.CacaoScreen
|
||||||
import net.shadowfacts.cacao.view.View
|
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.view.Label
|
||||||
import net.shadowfacts.cacao.util.RenderHelper
|
|
||||||
import net.shadowfacts.cacao.view.StackView
|
import net.shadowfacts.cacao.view.StackView
|
||||||
|
import net.shadowfacts.cacao.view.button.Button
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author shadowfacts
|
* @author shadowfacts
|
||||||
|
@ -34,20 +33,12 @@ 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(object: View() {
|
val purple = blue.addSubview(Button(Label("Hello, button!")).apply {
|
||||||
init {
|
handler = {
|
||||||
intrinsicContentSize = Size(25.0, 25.0)
|
println("$it clicked!")
|
||||||
backgroundColor = Color(0x800080)
|
|
||||||
}
|
}
|
||||||
|
background = View().apply {
|
||||||
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
|
backgroundColor = Color(0xebfc00)
|
||||||
println("hello world")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun drawContent(mouse: Point, delta: Float) {
|
|
||||||
if (mouse in bounds) {
|
|
||||||
RenderHelper.fill(bounds, Color.WHITE)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -219,14 +219,15 @@ open class View() {
|
||||||
/**
|
/**
|
||||||
* 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)
|
||||||
|
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue