diff --git a/src/main/kotlin/net/shadowfacts/asmr/TestCacaoScreen.kt b/src/main/kotlin/net/shadowfacts/asmr/TestCacaoScreen.kt index cedde1c..35d84f7 100644 --- a/src/main/kotlin/net/shadowfacts/asmr/TestCacaoScreen.kt +++ b/src/main/kotlin/net/shadowfacts/asmr/TestCacaoScreen.kt @@ -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() diff --git a/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt b/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt index 93b066c..6669a41 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt @@ -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 + } + } \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/Window.kt b/src/main/kotlin/net/shadowfacts/cacao/Window.kt index 8167513..5db1c38 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/Window.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/Window.kt @@ -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) + } } } \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/geometry/Rect.kt b/src/main/kotlin/net/shadowfacts/cacao/geometry/Rect.kt index b8364a8..9d19d03 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/geometry/Rect.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/geometry/Rect.kt @@ -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 + } + } \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/util/MouseButton.kt b/src/main/kotlin/net/shadowfacts/cacao/util/MouseButton.kt new file mode 100644 index 0000000..b52e5a7 --- /dev/null +++ b/src/main/kotlin/net/shadowfacts/cacao/util/MouseButton.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt b/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt index 557746e..e56dd76 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/util/RenderHelper.kt @@ -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) + } + } \ No newline at end of file diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/Label.kt b/src/main/kotlin/net/shadowfacts/cacao/view/Label.kt index d8e3cf1..17130c1 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/Label.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/Label.kt @@ -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) } diff --git a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt index dfe0f0c..5dc650e 100644 --- a/src/main/kotlin/net/shadowfacts/cacao/view/View.kt +++ b/src/main/kotlin/net/shadowfacts/cacao/view/View.kt @@ -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. diff --git a/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt b/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt new file mode 100644 index 0000000..f035418 --- /dev/null +++ b/src/test/kotlin/net/shadowfacts/cacao/view/ViewClickTests.kt @@ -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() + 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() + 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() + 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() + 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)) + } + +} \ No newline at end of file diff --git a/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt b/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt new file mode 100644 index 0000000..db9c370 --- /dev/null +++ b/src/test/kotlin/net/shadowfacts/cacao/view/ViewHoverTests.kt @@ -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() + 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() + 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)) + } + +} \ No newline at end of file