Add mouse hover & click handling

This commit is contained in:
Shadowfacts 2019-06-23 11:41:32 -04:00
parent a55cd950d0
commit 54ad51c719
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
10 changed files with 327 additions and 9 deletions

View File

@ -5,8 +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.StackView
/**
@ -31,11 +34,29 @@ 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)
}
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)
}
}
})
solver.dsl {
stack.topAnchor equalTo 50
stack.leftAnchor equalTo 50
stack.rightAnchor equalTo 150
purple.centerXAnchor equalTo blue.centerXAnchor
purple.centerYAnchor equalTo blue.centerYAnchor
}
layout()

View File

@ -3,6 +3,7 @@ package net.shadowfacts.cacao
import net.minecraft.client.gui.screen.Screen
import net.minecraft.network.chat.TextComponent
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import java.util.*
/**
@ -40,4 +41,11 @@ open class CacaoScreen: Screen(TextComponent("CacaoScreen")) {
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return false
}
}

View File

@ -1,6 +1,7 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Solver
import java.util.*
@ -40,6 +41,17 @@ class Window {
return view
}
/**
* Attempts to find a top level view of this window that contains the given point.
* If there are multiple overlapping views, which one this method returns is undefined.
*
* @param point The point to find views at, in the coordinate system of the window.
* @return The view, if any, that contains the given point.
*/
fun viewAtPoint(point: Point): View? {
return views.firstOrNull { point in it.frame }
}
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
@ -57,7 +69,25 @@ class Window {
* @param delta The time elapsed since the last frame.
*/
fun draw(mouse: Point, delta: Float) {
views.forEach(View::draw)
views.forEach {
val mouseInView = Point(mouse.x - it.frame.left, mouse.y - it.frame.top)
it.draw(mouseInView, delta)
}
}
/**
* Called when a mouse button is clicked and this is the active window.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param point The point in the window of the click.
* @param mouseButton The mouse button that was used to click.
*/
fun mouseClicked(point: Point, mouseButton: MouseButton) {
val view = viewAtPoint(point)
if (view != null) {
val pointInView = Point(point.x - view.frame.left, point.y - view.frame.top)
view.mouseClicked(pointInView, mouseButton)
}
}
}

View File

@ -34,4 +34,8 @@ data class Rect(val left: Double, val top: Double, val width: Double, val height
Size(width, height)
}
operator fun contains(point: Point): Boolean {
return point.x in left..right && point.y in top..bottom
}
}

View File

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

View File

@ -1,20 +1,49 @@
package net.shadowfacts.cacao.util
import com.mojang.blaze3d.platform.GlStateManager
import net.minecraft.client.gui.DrawableHelper
import net.shadowfacts.cacao.geometry.Rect
/**
* Helper methods for rendering using Minecraft's utilities from Cacao views.
* For unit testing, all drawing and OpenGL interaction can be disabled by setting the `cacao.drawing.disabled` JVM property to `true`.
*
* @author shadowfacts
*/
object RenderHelper {
private val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean()
/**
* Draws a solid [rect] filled with the given [color].
*/
fun fill(rect: Rect, color: Color) {
if (disabled) return
DrawableHelper.fill(rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb)
}
/**
* @see org.lwjgl.opengl.GL11.glPushMatrix
*/
fun pushMatrix() {
if (disabled) return
GlStateManager.pushMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glPopMatrix
*/
fun popMatrix() {
if (disabled) return
GlStateManager.popMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glTranslated
*/
fun translate(x: Double, y: Double, z: Double = 0.0) {
if (disabled) return
GlStateManager.translated(x, y, z)
}
}

View File

@ -2,6 +2,7 @@ package net.shadowfacts.cacao.view
import net.minecraft.client.MinecraftClient
import net.minecraft.client.font.TextRenderer
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
@ -42,7 +43,7 @@ class Label(text: String): View() {
intrinsicContentSize = Size(width.toDouble(), height.toDouble())
}
override fun drawContent() {
override fun drawContent(mouse: Point, delta: Float) {
textRenderer.draw(text, 0f, 0f, textColor.argb)
}

View File

@ -6,6 +6,7 @@ import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.Color
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.Solver
@ -137,6 +138,17 @@ open class View {
return view
}
/**
* Attempts to find a subview which contains the given point.
* If multiple subviews contain the given point, which one this method returns is undefined.
*
* @param point The point to find a subview for, in the coordinate system of this view.
* @return The view, if any, that contains the given point.
*/
fun subviewAtPoint(point: Point): View? {
return subviews.firstOrNull { point in it.frame }
}
/**
* Called when this view was added to a view hierarchy.
* If overridden, the super-class method must be called.
@ -191,26 +203,51 @@ 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.
*
* @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame.
*/
fun draw() {
GlStateManager.pushMatrix()
GlStateManager.translated(frame.left, frame.top, 0.0)
fun draw(mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top, 0.0)
RenderHelper.fill(bounds, backgroundColor)
drawContent()
drawContent(mouse, delta)
subviews.forEach(View::draw)
subviews.forEach {
val mouseInView = convert(mouse, to = it)
it.draw(mouseInView, delta)
}
GlStateManager.popMatrix()
RenderHelper.popMatrix()
}
/**
* Called during [draw] to draw content that's part of this view.
* During this method, the OpenGL coordinate system has been translated so the origin is at the top left corner
* of this view. Be careful not to translate additionally, and not to draw outside the [bounds] of the view.
*
* @param mouse The position of the mouse in the coordinate system of this view.
* @param delta The time since the last frame.
*/
open fun drawContent() {}
open fun drawContent(mouse: Point, delta: Float) {}
/**
* Called when this view is clicked. May delegate to [subviews].
* If overridden, the super-class method does not have to be called. Intentionally not calling it may be used
* to prevent [subviews] from receiving click events.
*
* @param point The point in the coordinate system of this view that the mouse was clicked.
* @param mouseButton The mouse button used to click.
*/
open fun mouseClicked(point: Point, mouseButton: MouseButton) {
val view = subviewAtPoint(point)
if (view != null) {
val pointInView = convert(point, to = view)
view.mouseClicked(pointInView, mouseButton)
}
}
/**
* Converts the given point in this view's coordinate system to the coordinate system of another view or the window.

View File

@ -0,0 +1,98 @@
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.util.MouseButton
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class ViewClickTests {
lateinit var window: Window
@BeforeEach
fun setup() {
window = Window()
}
@Test
fun testClickInsideRootView() {
val mouse = CompletableFuture<Point>()
window.addView(object: View() {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
mouse.complete(point)
}
})
window.mouseClicked(Point(75.0, 75.0), MouseButton.LEFT)
assertEquals(Point(25.0, 25.0), mouse.getNow(null))
}
@Test
fun testClickOutsideRootView() {
val clicked = CompletableFuture<Boolean>()
window.addView(object: View() {
init {
frame = Rect(50.0, 50.0, 100.0, 100.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
clicked.complete(true)
}
})
window.mouseClicked(Point(25.0, 25.0), MouseButton.LEFT)
assertFalse(clicked.getNow(false))
}
@Test
fun testClickInsideNestedView() {
val mouse = CompletableFuture<Point>()
val root = window.addView(View().apply {
frame = Rect(50.0, 50.0, 100.0, 100.0)
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
mouse.complete(point)
}
})
window.mouseClicked(Point(100.0, 100.0), MouseButton.LEFT)
assertEquals(Point(25.0, 25.0), mouse.getNow(null))
}
@Test
fun testClickOutsideNestedView() {
val clicked = CompletableFuture<Boolean>()
val root = window.addView(View().apply {
frame = Rect(50.0, 50.0, 100.0, 100.0)
})
root.addSubview(object: View() {
init {
frame = Rect(25.0, 25.0, 50.0, 50.0)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton) {
clicked.complete(true)
}
})
window.mouseClicked(Point(0.0, 0.0), MouseButton.LEFT)
assertFalse(clicked.getNow(false))
}
}

View File

@ -0,0 +1,72 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import org.junit.jupiter.api.Assertions.assertEquals
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 ViewHoverTests {
lateinit var window: Window
companion object {
@BeforeAll
@JvmStatic
fun setupAll() {
System.setProperty("cacao.drawing.disabled", "true")
}
}
@BeforeEach
fun setup() {
window = Window()
}
@Test
fun testHoverRootView() {
val point = CompletableFuture<Point>()
window.addView(object: View() {
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) {
point.complete(mouse)
}
})
window.draw(Point(75.0, 75.0), 0f)
assertEquals(Point(25.0, 25.0), point.getNow(null))
}
@Test
fun testHoverNestedView() {
val point = CompletableFuture<Point>()
val root = window.addView(View().apply {
frame = Rect(50.0, 50.0, 100.0, 100.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) {
point.complete(mouse)
}
})
window.draw(Point(100.0, 100.0), 0f)
assertEquals(Point(25.0, 25.0), point.getNow(null))
}
}