Add mouse hover & click handling
This commit is contained in:
parent
a55cd950d0
commit
54ad51c719
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue