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 .DS_Store
logs/
# gradle # gradle

View File

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

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