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

View File

@ -3,6 +3,7 @@ package net.shadowfacts.cacao
import net.minecraft.client.gui.screen.Screen import net.minecraft.client.gui.screen.Screen
import net.minecraft.network.chat.TextComponent import net.minecraft.network.chat.TextComponent
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import java.util.* 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 package net.shadowfacts.cacao
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Solver import no.birkett.kiwi.Solver
import java.util.* import java.util.*
@ -40,6 +41,17 @@ class Window {
return view 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. * Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup. * Should be called after the view hierarchy is setup.
@ -57,7 +69,25 @@ class Window {
* @param delta The time elapsed since the last frame. * @param delta The time elapsed since the last frame.
*/ */
fun draw(mouse: Point, delta: Float) { 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) 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 package net.shadowfacts.cacao.util
import com.mojang.blaze3d.platform.GlStateManager
import net.minecraft.client.gui.DrawableHelper import net.minecraft.client.gui.DrawableHelper
import net.shadowfacts.cacao.geometry.Rect import net.shadowfacts.cacao.geometry.Rect
/** /**
* Helper methods for rendering using Minecraft's utilities from Cacao views. * 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 * @author shadowfacts
*/ */
object RenderHelper { object RenderHelper {
private val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean()
/** /**
* Draws a solid [rect] filled with the given [color]. * Draws a solid [rect] filled with the given [color].
*/ */
fun fill(rect: Rect, color: 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) 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.MinecraftClient
import net.minecraft.client.font.TextRenderer import net.minecraft.client.font.TextRenderer
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
@ -42,7 +43,7 @@ class Label(text: String): View() {
intrinsicContentSize = Size(width.toDouble(), height.toDouble()) intrinsicContentSize = Size(width.toDouble(), height.toDouble())
} }
override fun drawContent() { override fun drawContent(mouse: Point, delta: Float) {
textRenderer.draw(text, 0f, 0f, textColor.argb) 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.geometry.*
import net.shadowfacts.cacao.util.Color import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.LowestCommonAncestor import net.shadowfacts.cacao.util.LowestCommonAncestor
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper import net.shadowfacts.cacao.util.RenderHelper
import no.birkett.kiwi.Constraint import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver import no.birkett.kiwi.Solver
@ -137,6 +138,17 @@ open class View {
return 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. * Called when this view was added to a view hierarchy.
* If overridden, the super-class method must be called. * If overridden, the super-class method must be called.
@ -191,26 +203,51 @@ 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 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() { fun draw(mouse: Point, delta: Float) {
GlStateManager.pushMatrix() RenderHelper.pushMatrix()
GlStateManager.translated(frame.left, frame.top, 0.0) RenderHelper.translate(frame.left, frame.top, 0.0)
RenderHelper.fill(bounds, backgroundColor) 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. * 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 * 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. * 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. * 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))
}
}