Add basic button implementation

This commit is contained in:
Shadowfacts 2019-06-23 17:56:49 -04:00
parent bf7e7bd24c
commit 62fbc10aa3
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
6 changed files with 209 additions and 19 deletions

1
.gitignore vendored
View File

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

View File

@ -5,12 +5,11 @@ import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.button.Button
/**
* @author shadowfacts
@ -34,20 +33,12 @@ class TestCacaoScreen: CacaoScreen() {
intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color(0x0000ff)
})
val purple = blue.addSubview(object: View() {
init {
intrinsicContentSize = Size(25.0, 25.0)
backgroundColor = Color(0x800080)
val purple = blue.addSubview(Button(Label("Hello, button!")).apply {
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 = View().apply {
backgroundColor = Color(0xebfc00)
}
})

View File

@ -219,14 +219,15 @@ open class View() {
/**
* Called to draw this view.
* 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 delta The time since the last frame.
*/
fun draw(mouse: Point, delta: Float) {
open fun draw(mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top, 0.0)
RenderHelper.translate(frame.left, frame.top)
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

@ -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))
}
}