Add Cacao

This commit is contained in:
Shadowfacts 2021-02-18 23:12:43 -05:00
parent aec32ae270
commit 8f577598ff
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
56 changed files with 3963 additions and 1 deletions

3
.gitmodules vendored Normal file
View File

@ -0,0 +1,3 @@
[submodule "kiwi-java"]
path = kiwi-java
url = git@git.shadowfacts.net:shadowfacts/kiwi-java.git

View File

@ -2,6 +2,7 @@ plugins {
id "fabric-loom" version "0.5-SNAPSHOT"
id "maven-publish"
id "org.jetbrains.kotlin.jvm" version "1.4.30"
id "com.github.johnrengelman.shadow" version "4.0.4"
}
sourceCompatibility = JavaVersion.VERSION_1_8
@ -18,6 +19,7 @@ repositories {
maven {
url = "https://mod-buildcraft.com/maven"
}
jcenter()
}
dependencies {
@ -35,6 +37,10 @@ dependencies {
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-items:${project.libblockattributes_version}"
shadow project(":kiwi-java")
testImplementation "org.junit.jupiter:junit-jupiter:${project.junit_version}"
}
processResources {

View File

@ -13,3 +13,5 @@ fabric_version=0.30.0+1.16
fabric_kotlin_version=1.4.30+build.2
libblockattributes_version=0.8.5
junit_version = 5.4.0

1
kiwi-java Submodule

@ -0,0 +1 @@
Subproject commit 1cbaea53d207f1e16c6e5ee2e6bf6e3c1440ac44

View File

@ -7,4 +7,6 @@ pluginManagement {
}
gradlePluginPortal()
}
}
}
include("kiwi-java")

View File

@ -0,0 +1,100 @@
package net.shadowfacts.cacao
import com.mojang.blaze3d.platform.GlStateManager
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.sound.SoundEvents
import net.minecraft.text.LiteralText
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import java.util.*
/**
* This class serves as the bridge between Cacao and a Minecraft [Screen]. It renders Cacao [Window]s in Minecraft and
* sends input events from Minecraft back to Cacao objects.
*
* @author shadowfacts
*/
open class CacaoScreen: Screen(LiteralText("CacaoScreen")) {
// _windows is the internal, mutable object, since we only want it to by mutated by the add/removeWindow methods.
private val _windows = LinkedList<Window>()
/**
* The list of windows that belong to this screen.
* This list should never be modified directly, only by using the [addWindow]/[removeWindow] methods.
*/
val windows: List<Window> = _windows
/**
* Adds the given window to this screen's window list.
* By default, the new window is added at the tail of the window list, making it the active window.
* Only the active window will receive input events.
*
* @param window The Window to add to this screen.
* @param index The index to insert the window into the window list at.
* @return The window that was added, as a convenience.
*/
fun <T: Window> addWindow(window: T, index: Int = _windows.size): T {
_windows.add(index, window)
window.screen = this
window.wasAdded()
window.resize(width, height)
return window
}
/**
* Removes the given window from this screen's window list.
*/
fun removeWindow(window: Window) {
_windows.remove(window)
}
override fun init() {
super.init()
windows.forEach {
it.resize(width, height)
}
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
if (client != null) {
// workaround this.minecraft sometimes being null causing a crash
renderBackground(matrixStack)
}
val mouse = Point(mouseX, mouseY)
windows.forEach {
it.draw(matrixStack, mouse, delta)
}
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseClicked(Point(mouseX, mouseY), MouseButton.fromMC(button))
return if (result == true) {
RenderHelper.playSound(SoundEvents.UI_BUTTON_CLICK)
true
} else {
false
}
}
override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, deltaX: Double, deltaY: Double): Boolean {
val window = windows.lastOrNull()
val startPoint = Point(mouseX, mouseY)
val delta = Point(deltaX, deltaY)
val result = window?.mouseDragged(startPoint, delta, MouseButton.fromMC(button))
return result == true
}
override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean {
val window = windows.lastOrNull()
val result = window?.mouseReleased(Point(mouseX, mouseY), MouseButton.fromMC(button))
return result == true
}
}

View File

@ -0,0 +1,18 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.view.View
import no.birkett.kiwi.Variable
/**
* A Kiwi variable that belongs to a Cacao view.
* This class generally isn't used directly, but via the anchor *Anchor properties on [View].
*
* @author shadowfacts
*/
class LayoutVariable(val owner: View, val property: String): Variable("LayoutVariable") {
override fun getName() = "$owner.$property"
override fun toString() = "LayoutVariable(name=$name, value=$value)"
}

View File

@ -0,0 +1,33 @@
# Cacao
Cacao is a UI framework for Fabric/Minecraft mods based on Apple's [Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API)
UI toolkit.
## Architecture
### Screen
A [CacaoScreen][] is the object that acts as the interface between Minecraft GUI code and the Cacao framework.
The CacaoScreen draws Cacao views on screen and passes Minecraft input events to the appropriate Views. The CacaoScreen
owns a group of [Window](#window) objects which are displayed on screen, one on top of the other.
[CacaoScreen]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt
### Window
A [Window][] object has a root [View Controller](#view-controller) that it displays on screen.
The Window occupies the entire screen space and translates events from the screen to the root View Controller's View.
It owns a Solver object that manages layout constraints. The window also handles screen resizing and re-lays out the
view hierarchy.
[Window]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/Window.kt
### View Controller
A [ViewController][] object owns a view, receives lifecycle events for it, and is generally used to control the view.
Each View Controller has a single root [View](#view) which in turn may have subviews.
[ViewController]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt
### View
A [View][] object represents a single view on screen. It handles drawing, positioning, and directly handles input.
[View]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/view/View.kt

View File

@ -0,0 +1,214 @@
package net.shadowfacts.cacao
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import no.birkett.kiwi.Variable
/**
* A Window is the object at the top of a Cacao view hierarchy. It occupies the entirety of the Minecraft screen size
* and provides the base coordinate system for its view hierarchy.
*
* The Window owns the Kiwi [Solver] object used for layout by all of its views.
*
* @author shadowfacts
*
* @param viewController The root view controller for this window.
*/
class Window(
/**
* The root view controller for this window.
*/
val viewController: ViewController
) {
/**
* The screen that this window belongs to.
* Not initialized until this window is added to a screen, using it before that point will throw a runtime exception.
*/
lateinit var screen: CacaoScreen
/**
* The constraint solver used by this window and all its views and subviews.
*/
var solver = Solver()
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
val leftAnchor = Variable("left")
/**
* Layout anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = Variable("right")
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
val topAnchor = Variable("top")
/**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = Variable("bottom")
/**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = Variable("width")
/**
* Layout anchor for the height of this view in the window's coordinate system.
*/
val heightAnchor = Variable("height")
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = Variable("centerX")
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
val centerYAnchor = Variable("centerY")
// internal constraints that specify the window size based on the MC screen size
// stored so that they can be removed when the screen is resized
private var widthConstraint: Constraint? = null
private var heightConstraint: Constraint? = null
private var currentDragReceiver: View? = null
init {
createInternalConstraints()
}
fun wasAdded() {
viewController.window = this
viewController.loadView()
viewController.view.window = this
viewController.view.solver = solver
viewController.view.wasAdded()
viewController.createConstraints {
viewController.view.leftAnchor equalTo leftAnchor
viewController.view.rightAnchor equalTo rightAnchor
viewController.view.topAnchor equalTo topAnchor
viewController.view.bottomAnchor equalTo bottomAnchor
}
viewController.viewDidLoad()
layout()
}
/**
* Creates the internal constraints used by the window.
* If overridden, the super-class method must be called.
*/
protected fun createInternalConstraints() {
solver.dsl {
leftAnchor equalTo 0
topAnchor equalTo 0
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
/**
* Called by the window's [screen] when the Minecraft screen is resized.
* Used to update the window's width and height constraints and re-layout views.
*/
internal fun resize(width: Int, height: Int) {
if (widthConstraint != null) solver.removeConstraint(widthConstraint)
if (heightConstraint != null) solver.removeConstraint(heightConstraint)
solver.dsl {
widthConstraint = (widthAnchor equalTo width)
heightConstraint = (heightAnchor equalTo height)
}
layout()
}
/**
* Convenience method that removes this window from its [screen].
*/
fun removeFromScreen() {
viewController.viewWillDisappear()
screen.removeWindow(this)
viewController.viewDidDisappear()
}
/**
* Instructs the solver to solve all of the provided constraints.
* Should be called after the view hierarchy is setup.
*/
fun layout() {
viewController.viewWillLayoutSubviews()
solver.updateVariables()
viewController.viewDidLayoutSubviews()
}
/**
* Draws this window and all of its views.
* This method is called by [CacaoScreen] and generally shouldn't be called directly.
*
* @param mouse The point in the coordinate system of the window.
* @param delta The time elapsed since the last frame.
*/
fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
val mouseInView = Point(mouse.x - viewController.view.frame.left, mouse.y - viewController.view.frame.top)
viewController.view.draw(matrixStack, 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.
* @return Whether the mouse click was handled by a view.
*/
fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (point in viewController.view.frame) {
val mouseInView = Point(point.x - viewController.view.frame.left, point.y - viewController.view.frame.top)
return viewController.view.mouseClicked(mouseInView, mouseButton)
} else {
// remove the window from the screen when the mouse clicks outside the window and this is not the primary window
if (screen.windows.size > 1) {
removeFromScreen()
}
}
return false
}
fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
return currentlyDraggedView.mouseDragged(startPoint, delta, mouseButton)
} else if (startPoint in viewController.view.frame) {
val startInView = Point(startPoint.x - viewController.view.frame.left, startPoint.y - viewController.view.frame.top)
var prevView: View? = null
var view = viewController.view.subviewsAtPoint(startInView).maxBy(View::zIndex)
while (view != null && !view.respondsToDragging) {
prevView = view
val pointInView = viewController.view.convert(startInView, to = view)
view = view.subviewsAtPoint(pointInView).maxBy(View::zIndex)
}
this.currentDragReceiver = view ?: prevView
return this.currentDragReceiver?.mouseDragged(startPoint, delta, mouseButton) ?: false
}
return false
}
fun mouseReleased(point: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
this.currentDragReceiver = null
return true
}
return false
}
}

View File

@ -0,0 +1,19 @@
package net.shadowfacts.cacao.geometry
/**
* An axis in a 2D coordinate plane.
*
* @author shadowfacts
*/
enum class Axis {
HORIZONTAL, VERTICAL;
/**
* Gets the axis that is perpendicular to this one.
*/
val perpendicular: Axis
get() = when (this) {
HORIZONTAL -> VERTICAL
VERTICAL -> HORIZONTAL
}
}

View File

@ -0,0 +1,21 @@
package net.shadowfacts.cacao.geometry
/**
* A relative position on a line along an axis.
*
* @author shadowfacts
*/
enum class AxisPosition {
/**
* Top for vertical, left for horizontal.
*/
LEADING,
/**
* Center X/Y.
*/
CENTER,
/**
* Bottom for vertical, right for horizontal.
*/
TRAILING;
}

View File

@ -0,0 +1,49 @@
package net.shadowfacts.cacao.geometry
import java.lang.RuntimeException
import kotlin.math.pow
/**
* Helper class that represents a cubic bezier curve.
*
* @author shadowfacts
*/
data class BezierCurve(private val points: Array<Point>) {
init {
if (points.size != 4) {
throw RuntimeException("Cubic bezier curve must have exactly four points")
}
}
fun point(time: Double): Point {
val x = coordinate(time, Axis.HORIZONTAL)
val y = coordinate(time, Axis.VERTICAL)
return Point(x, y)
}
private fun coordinate(t: Double, axis: Axis): Double {
// B(t)=(1-t)^3*p0+3(1-t)^2*t*p1+3(1-t)*t^2*p2+t^3*p3
val p0 = points[0][axis]
val p1 = points[1][axis]
val p2 = points[2][axis]
val p3 = points[3][axis]
return ((1 - t).pow(3) * p0) + (3 * (1 - t).pow(2) * t * p1) + (3 * (1 - t) * t.pow(2) * p2) + (t.pow(3) * p3)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as BezierCurve
if (!points.contentEquals(other.points)) return false
return true
}
override fun hashCode(): Int {
return points.contentHashCode()
}
}

View File

@ -0,0 +1,31 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for defining 2D points.
*
* @author shadowfacts
*/
data class Point(val x: Double, val y: Double) {
constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble())
companion object {
val ORIGIN = Point(0.0, 0.0)
}
operator fun plus(other: Point): Point {
return Point(x + other.x, y + other.y)
}
operator fun minus(other: Point): Point {
return Point(x - other.x, y - other.y)
}
operator fun get(axis: Axis): Double {
return when (axis) {
Axis.HORIZONTAL -> x
Axis.VERTICAL -> y
}
}
}

View File

@ -0,0 +1,41 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for defining rectangles. Provides helper values for calculating perpendicular components of a rectangle based on X/Y/W/H.
*
* @author shadowfacts
*/
data class Rect(val left: Double, val top: Double, val width: Double, val height: Double) {
constructor(origin: Point, size: Size): this(origin.x, origin.y, size.width, size.height)
val right: Double by lazy {
left + width
}
val bottom: Double by lazy {
top + height
}
val midX: Double by lazy {
left + width / 2
}
val midY: Double by lazy {
top + height / 2
}
val origin: Point by lazy {
Point(left, top)
}
val center: Point by lazy {
Point(midX, midY)
}
val size: Size by lazy {
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,8 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for specifying the size of objects.
*
* @author shadowfacts
*/
data class Size(val width: Double, val height: Double)

View File

@ -0,0 +1,34 @@
package net.shadowfacts.cacao.util
/**
* Helper class for Cacao colors.
*
* @author shadowfacts
* @param red The red component, from 0-255.
* @param green The green component, from 0-255.
* @param blue The blue component, from 0-255.
* @param alpha The alpha (i.e. transparency) component, from 0-255. (0 is transparent, 255 is opaque)
*/
data class Color(val red: Int, val green: Int, val blue: Int, val alpha: Int = 255) {
/**
* Constructs a color from the packed RGB color.
*/
constructor(rgb: Int, alpha: Int = 255): this(rgb shr 16, (rgb shr 8) and 255, rgb and 255, alpha)
/**
* The ARGB packed representation of this color.
*/
val argb: Int
get() = ((alpha and 255) shl 24) or ((red and 255) shl 16) or ((green and 255) shl 8) or (blue and 255)
companion object {
val CLEAR = Color(0, alpha = 0)
val WHITE = Color(0xffffff)
val BLACK = Color(0)
val RED = Color(0xff0000)
val GREEN = Color(0x00ff00)
val BLUE = Color(0x0000ff)
}
}

View File

@ -0,0 +1,20 @@
package net.shadowfacts.cacao.util
/**
* @author shadowfacts
*/
object EnumHelper {
fun <E: Enum<E>> next(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) + 1
return if (index < constants.size) constants[index] else constants.first()
}
fun <E: Enum<E>> previous(value: E): E {
val constants = value.declaringClass.enumConstants
val index = constants.indexOf(value) - 1
return if (index >= 0) constants[index] else constants.last()
}
}

View File

@ -0,0 +1,56 @@
package net.shadowfacts.cacao.util
import java.util.*
import kotlin.NoSuchElementException
/**
* A linear time algorithm for finding the lowest common ancestor of two nodes in a graph.
* Based on https://stackoverflow.com/a/6342546/4731558
*
* Works be finding the path from each node back to the root node.
* The LCA will then be the node after which the paths diverge.
*
* @author shadowfacts
*/
object LowestCommonAncestor {
fun <Node> find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? {
@Suppress("NAME_SHADOWING") var node1: Node? = node1
@Suppress("NAME_SHADOWING") var node2: Node? = node2
val parent1 = LinkedList<Node>()
while (node1 != null) {
parent1.push(node1)
node1 = node1.parent()
}
val parent2 = LinkedList<Node>()
while (node2 != null) {
parent2.push(node2)
node2 = node2.parent()
}
// paths don't converge on the same root element
if (parent1.first != parent2.first) {
return null
}
var oldNode: Node? = null
while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) {
oldNode = node1
node1 = parent1.popOrNull()
node2 = parent2.popOrNull()
}
return if (node1 == node2) node1!!
else oldNode!!
}
}
private fun <T> LinkedList<T>.popOrNull(): T? {
return try {
pop()
} catch (e: NoSuchElementException) {
null
}
}

View File

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

View File

@ -0,0 +1,130 @@
package net.shadowfacts.cacao.util
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawableHelper
import net.minecraft.client.render.*
import net.minecraft.client.sound.PositionedSoundInstance
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.sound.SoundEvent
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.texture.Texture
import org.lwjgl.opengl.GL11
/**
* 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 {
val disabled = (System.getProperty("cacao.drawing.disabled") ?: "false").toBoolean()
// TODO: find a better place for this
fun playSound(event: SoundEvent) {
if (disabled) return
MinecraftClient.getInstance().soundManager.play(PositionedSoundInstance.master(event, 1f))
}
/**
* Draws a solid [rect] filled with the given [color].
*/
fun fill(matrixStack: MatrixStack, rect: Rect, color: Color) {
if (disabled) return
DrawableHelper.fill(matrixStack, rect.left.toInt(), rect.top.toInt(), rect.right.toInt(), rect.bottom.toInt(), color.argb)
}
/**
* Binds and draws the given [texture] filling the [rect].
*/
fun draw(rect: Rect, texture: Texture) {
if (disabled) return
color(1f, 1f, 1f, 1f)
MinecraftClient.getInstance().textureManager.bindTexture(texture.location)
draw(rect.left, rect.top, texture.u, texture.v, rect.width, rect.height, texture.width, texture.height)
}
fun drawLine(start: Point, end: Point, z: Double, width: Float, color: Color) {
if (disabled) return
GlStateManager.lineWidth(width)
val tessellator = Tessellator.getInstance()
val buffer = tessellator.buffer
buffer.begin(GL11.GL_LINES, VertexFormats.POSITION_COLOR)
buffer.vertex(start.x, start.y, z).color(color).next()
buffer.vertex(end.x, end.y, z).color(color).next()
tessellator.draw()
}
/**
* Draws the bound texture with the given screen and texture position and size.
*/
fun draw(x: Double, y: Double, u: Int, v: Int, width: Double, height: Double, textureWidth: Int, textureHeight: Int) {
if (disabled) return
val uStart = u.toFloat() / textureWidth
val uEnd = (u + width).toFloat() / textureWidth
val vStart = v.toFloat() / textureHeight
val vEnd = (v + height).toFloat() / textureHeight
drawTexturedQuad(x, x + width, y, y + height, 0.0, uStart, uEnd, vStart, vEnd)
}
// Copied from net.minecraft.client.gui.DrawableHelper
private fun drawTexturedQuad(xStart: Double, xEnd: Double, yStart: Double, yEnd: Double, z: Double, uStart: Float, uEnd: Float, vStart: Float, vEnd: Float) {
val tessellator = Tessellator.getInstance()
val buffer = tessellator.buffer
buffer.begin(GL11.GL_QUADS, VertexFormats.POSITION_TEXTURE)
buffer.vertex(xStart, yEnd, z).texture(uStart, vEnd).next()
buffer.vertex(xEnd, yEnd, z).texture(uEnd, vEnd).next()
buffer.vertex(xEnd, yStart, z).texture(uEnd, vStart).next()
buffer.vertex(xStart, yStart, z).texture(uStart, vStart).next()
tessellator.draw()
}
/**
* @see org.lwjgl.opengl.GL11.glPushMatrix
*/
fun pushMatrix() {
if (disabled) return
RenderSystem.pushMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glPopMatrix
*/
fun popMatrix() {
if (disabled) return
RenderSystem.popMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glTranslated
*/
fun translate(x: Double, y: Double, z: Double = 0.0) {
if (disabled) return
RenderSystem.translated(x, y, z)
}
/**
* @see org.lwjgl.opengl.GL11.glScaled
*/
fun scale(x: Double, y: Double, z: Double = 1.0) {
if (disabled) return
RenderSystem.scaled(x, y, z)
}
/**
* @see org.lwjgl.opengl.GL11.glColor4f
*/
fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return
RenderSystem.color4f(r, g, b, alpha)
}
private fun VertexConsumer.color(color: Color): VertexConsumer {
return color(color.red, color.green, color.blue, color.alpha)
}
}

View File

@ -0,0 +1,14 @@
package net.shadowfacts.cacao.util
import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Term
import no.birkett.kiwi.Variable
/**
* Gets all the variables used by this constraint.
*
* @author shadowfacts
*/
fun Constraint.getVariables(): List<Variable> {
return expression.terms.map(Term::getVariable)
}

View File

@ -0,0 +1,24 @@
package net.shadowfacts.cacao.util.properties
import kotlin.reflect.KProperty
/**
* @author shadowfacts
*/
class ObservableLateInitProperty<T: Any>(val observer: (T) -> Unit) {
lateinit var storage: T
val isInitialized: Boolean
get() = this::storage.isInitialized
operator fun getValue(thisRef: Any, property: KProperty<*>): T {
return storage
}
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
storage = value
observer(value)
}
}

View File

@ -0,0 +1,20 @@
package net.shadowfacts.cacao.util.properties
import kotlin.reflect.KProperty
/**
* @author shadowfacts
*/
class ObservableLazyProperty<Value>(val create: () -> Value, val onCreate: () -> Unit) {
var storage: Value? = null
operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
if (storage == null) {
storage = create()
onCreate()
}
return storage!!
}
}

View File

@ -0,0 +1,24 @@
package net.shadowfacts.cacao.util.properties
import kotlin.reflect.KProperty
/**
* @author shadowfacts
*/
class ResettableLazyProperty<Value>(val initializer: () -> Value) {
var value: Value? = null
val isInitialized: Boolean
get() = value != null
operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
if (value == null) {
value = initializer()
}
return value!!
}
fun reset() {
value = null
}
}

View File

@ -0,0 +1,61 @@
package net.shadowfacts.cacao.util.texture
import net.minecraft.util.Identifier
/**
* Helper class that represents a texture that can be divided into nine pieces (4 corners, 4 edges, and the center)
* and can be drawn at any size by combining and repeating those pieces.
*
* It also provides convenience [Texture] objects that represent the different patches.
*
* @author shadowfacts
* @param texture The base [Texture] object.
* @param cornerWidth The width of each corner (and therefore the width of the vertical edges).
* @param cornerHeight The height of each corner (and therefore the height of the horizontal edges.)
* @param centerWidth The width of the center patch.
* @param centerHeight The height of the center patch.
*/
data class NinePatchTexture(val texture: Texture, val cornerWidth: Int, val cornerHeight: Int, val centerWidth: Int, val centerHeight: Int) {
companion object {
val PANEL_BG = NinePatchTexture(Texture(Identifier("textures/gui/demo_background.png"), 0, 0), 5, 5, 238, 156)
val BUTTON_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 66), 3, 3, 194, 14)
val BUTTON_HOVERED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 86), 3, 3, 194, 14)
val BUTTON_DISABLED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 46), 3, 3, 194, 14)
}
// Corners
val topLeft by lazy {
texture
}
val topRight by lazy {
Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height)
}
val bottomLeft by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height)
}
val bottomRight by lazy {
Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height)
}
// Edges
val topMiddle by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height)
}
val bottomMiddle by lazy {
Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height)
}
val leftMiddle by lazy {
Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height)
}
val rightMiddle by lazy {
Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height)
}
// Center
val center by lazy {
Texture(texture.location, texture.u + cornerWidth, texture.v + cornerHeight, texture.width, texture.height)
}
}

View File

@ -0,0 +1,15 @@
package net.shadowfacts.cacao.util.texture
import net.minecraft.util.Identifier
/**
* A helper class that represents a texture.
*
* @author shadowfacts
* @param location The identifier representing the resource-pack location of the texture image.
* @param u The X coordinate in pixels of where the texture starts within the image.
* @param v The Y coordinate in pixels of where the texture starts within the image.
* @param width The width in pixels of the entire image.
* @param height The height in pixels of the entire image.
*/
data class Texture(val location: Identifier, val u: Int, val v: Int, val width: Int = 256, val height: Int = 256)

View File

@ -0,0 +1,36 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.BezierCurve
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.RenderHelper
/**
* @author shadowfacts
*/
class BezierCurveView(val curve: BezierCurve): View() {
private val points by lazy {
val step = 0.05
var t = 0.0
val points = mutableListOf<Point>()
while (t <= 1) {
points.add(curve.point(t))
t += step
}
points
}
var lineWidth = 3f
var lineColor = Color.BLACK
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.scale(bounds.width, bounds.height)
for ((index, point) in points.withIndex()) {
val next = points.getOrNull(index + 1) ?: break
RenderHelper.drawLine(point, next, zIndex, lineWidth, lineColor)
}
}
}

View File

@ -0,0 +1,114 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.kiwidsl.dsl
/**
* @author shadowfacts
*/
class DialogView(
val title: String,
val message: String,
val buttonTypes: Array<ButtonType>,
val iconTexture: Texture?,
val buttonCallback: (ButtonType, Window) -> Unit
): View() {
interface ButtonType {
val localizedName: String
}
enum class DefaultButtonType: ButtonType {
CANCEL, CONFIRM, OK, CLOSE;
override val localizedName: String
get() = name.toLowerCase().capitalize() // todo: actually localize me
}
private lateinit var background: NinePatchView
private lateinit var hStack: StackView
private var iconView: TextureView? = null
private lateinit var vStack: StackView
private lateinit var messageLabel: Label
private var buttonContainer: View? = null
private var buttonStack: StackView? = null
override fun wasAdded() {
background = addSubview(NinePatchView(NinePatchTexture.PANEL_BG).apply { zIndex = -1.0 })
hStack = addSubview(StackView(Axis.HORIZONTAL, StackView.Distribution.LEADING, spacing = 8.0))
if (iconTexture != null) {
iconView = hStack.addArrangedSubview(TextureView(iconTexture))
}
vStack = hStack.addArrangedSubview(StackView(Axis.VERTICAL, spacing = 4.0))
vStack.addArrangedSubview(Label(title, shadow = false).apply {
textColor = Color(0x404040)
})
messageLabel = vStack.addArrangedSubview(Label(message, shadow = false).apply {
textColor = Color(0x404040)
})
if (buttonTypes.isNotEmpty()) {
buttonContainer = vStack.addArrangedSubview(View())
buttonStack = buttonContainer!!.addSubview(StackView(Axis.HORIZONTAL))
for (type in buttonTypes) {
buttonStack!!.addArrangedSubview(Button(Label(type.localizedName)).apply {
handler = {
this@DialogView.buttonCallback(type, this@DialogView.window!!)
}
})
}
}
super.wasAdded()
}
override fun createInternalConstraints() {
super.createInternalConstraints()
solver.dsl {
centerXAnchor equalTo window!!.centerXAnchor
centerYAnchor equalTo window!!.centerYAnchor
widthAnchor greaterThanOrEqualTo 175
background.leftAnchor equalTo leftAnchor - 8
background.rightAnchor equalTo rightAnchor + 8
background.topAnchor equalTo topAnchor - 8
background.bottomAnchor equalTo bottomAnchor + 8
hStack.leftAnchor equalTo leftAnchor
hStack.rightAnchor equalTo rightAnchor
hStack.topAnchor equalTo topAnchor
hStack.bottomAnchor equalTo bottomAnchor
if (iconView != null) {
hStack.bottomAnchor greaterThanOrEqualTo iconView!!.bottomAnchor
}
hStack.bottomAnchor greaterThanOrEqualTo vStack.bottomAnchor
if (iconView != null) {
iconView!!.widthAnchor equalTo 30
iconView!!.heightAnchor equalTo 30
}
messageLabel.heightAnchor greaterThanOrEqualTo 50
if (buttonContainer != null) {
buttonStack!!.heightAnchor equalTo buttonContainer!!.heightAnchor
buttonStack!!.centerYAnchor equalTo buttonContainer!!.centerYAnchor
buttonStack!!.rightAnchor equalTo buttonContainer!!.rightAnchor
}
}
}
}

View File

@ -0,0 +1,123 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.MinecraftClient
import net.minecraft.client.font.TextRenderer
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.RenderHelper
/**
* A simple View that displays text. Allows for controlling the color and shadow of the text. Label cannot be used
* for multi-line text, instead use [TextView].
*
* @author shadowfacts
* @param text The text of this label.
* @param shadow Whether the text should be rendered with a shadow or not.
* @param maxLines The maximum number of lines of text to render.
* `0` means that there is no line limit and all lines will be rendered.
* When using a non-zero [maxLines] value, the [intrinsicContentSize] of the Label still assumes all
* content on one line. So, constraints must be provided to calculate the actual width to use for line
* wrapping.
*/
class Label(
text: String,
val shadow: Boolean = true,
val maxLines: Int = 0,
val wrappingMode: WrappingMode = WrappingMode.WRAP,
val textAlignment: TextAlignment = TextAlignment.LEFT
): View() {
companion object {
private val textRenderer: TextRenderer
get() = MinecraftClient.getInstance().textRenderer
}
enum class WrappingMode {
WRAP, NO_WRAP
}
enum class TextAlignment {
LEFT, CENTER, RIGHT
}
/**
* The text of this label. Mutating this field will update the intrinsic content size and trigger a layout.
*/
var text: String = text
set(value) {
field = value
updateIntrinsicContentSize()
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
private lateinit var lines: List<String>
var textColor = Color.WHITE
set(value) {
field = value
textColorARGB = value.argb
}
private var textColorARGB: Int = textColor.argb
override fun wasAdded() {
super.wasAdded()
updateIntrinsicContentSize()
}
private fun updateIntrinsicContentSize() {
if (RenderHelper.disabled) return
val width = textRenderer.getWidth(text)
val height = textRenderer.fontHeight
intrinsicContentSize = Size(width.toDouble(), height.toDouble())
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
if (!this::lines.isInitialized) {
computeLines()
}
for (i in 0 until lines.size) {
val x = when (textAlignment) {
TextAlignment.LEFT -> 0.0
TextAlignment.CENTER -> (bounds.width + textRenderer.getWidth(lines[i])) / 2
TextAlignment.RIGHT -> bounds.width - textRenderer.getWidth(lines[i])
}
val y = i * textRenderer.fontHeight
if (shadow) {
textRenderer.drawWithShadow(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
} else {
textRenderer.draw(matrixStack, lines[i], x.toFloat(), y.toFloat(), textColorARGB)
}
}
}
override fun didLayout() {
super.didLayout()
computeLines()
}
private fun computeLines() {
var lines = text.split("\n")
if (wrappingMode == WrappingMode.WRAP) {
lines = lines.flatMap {
wrapStringToWidthAsList(it, bounds.width)
}
}
if (0 < maxLines && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
}
this.lines = lines
}
private fun wrapStringToWidthAsList(string: String, width: Double): List<String> {
if (RenderHelper.disabled) return listOf(string)
// return textRenderer.wrapStringToWidthAsList(string, width.toInt())
TODO()
}
}

View File

@ -0,0 +1,136 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.properties.ResettableLazyProperty
/**
* A helper class for drawing a [NinePatchTexture] in a view.
* `NinePatchView` will draw the given nine patch texture filling its bounds.
*
* This class and the region properties are left open for internal framework use, overriding them is not recommended.
*
* @author shadowfacts
* @param ninePatch The nine patch texture that this view will draw.
*/
open class NinePatchView(val ninePatch: NinePatchTexture): View() {
// Corners
private val topLeftDelegate = ResettableLazyProperty {
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(0.0, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, bounds.height - ninePatch.cornerHeight, ninePatch.cornerWidth.toDouble(), ninePatch.cornerHeight.toDouble())
}
protected open val bottomRight by bottomRightDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, ninePatch.cornerHeight.toDouble())
}
protected open val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, topMiddle.height)
}
protected open val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, ninePatch.cornerHeight.toDouble(), ninePatch.cornerWidth.toDouble(), bounds.height - 2 * ninePatch.cornerHeight)
}
protected open val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, leftMiddle.top, leftMiddle.width, leftMiddle.height)
}
protected open val rightMiddle by rightMiddleDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topLeft.bottom, topMiddle.width, leftMiddle.height)
}
protected open val center by centerDelegate
private val delegates = listOf(topLeftDelegate, topRightDelegate, bottomLeftDelegate, bottomRightDelegate, topMiddleDelegate, bottomMiddleDelegate, leftMiddleDelegate, rightMiddleDelegate, centerDelegate)
override fun didLayout() {
super.didLayout()
delegates.forEach(ResettableLazyProperty<Rect>::reset)
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
drawCorners()
drawEdges()
drawCenter()
}
private fun drawCorners() {
RenderHelper.draw(topLeft, ninePatch.topLeft)
RenderHelper.draw(topRight, ninePatch.topRight)
RenderHelper.draw(bottomLeft, ninePatch.bottomLeft)
RenderHelper.draw(bottomRight, ninePatch.bottomRight)
}
private fun drawEdges() {
// Horizontal
for (i in 0 until (topMiddle.width.toInt() / ninePatch.centerWidth)) {
RenderHelper.draw(topMiddle.left + i * ninePatch.centerWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, ninePatch.centerWidth.toDouble(), topMiddle.height, ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(bottomMiddle.left + i * ninePatch.centerWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, ninePatch.centerWidth.toDouble(), bottomMiddle.height, ninePatch.texture.width, ninePatch.texture.height)
}
val remWidth = topMiddle.width.toInt() % ninePatch.centerWidth
if (remWidth > 0) {
RenderHelper.draw(topMiddle.right - remWidth, topMiddle.top, ninePatch.topMiddle.u, ninePatch.topMiddle.v, remWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(bottomMiddle.right - remWidth, bottomMiddle.top, ninePatch.bottomMiddle.u, ninePatch.bottomMiddle.v, remWidth.toDouble(), ninePatch.cornerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
}
// Vertical
for (i in 0 until (leftMiddle.height.toInt() / ninePatch.centerHeight)) {
RenderHelper.draw(leftMiddle.left, leftMiddle.top + i * ninePatch.centerHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(rightMiddle.left, rightMiddle.top + i * ninePatch.centerHeight, ninePatch.rightMiddle.u, ninePatch.rightMiddle.v, ninePatch.cornerWidth.toDouble(), ninePatch.centerHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
}
val remHeight = leftMiddle.height.toInt() % ninePatch.centerHeight
if (remHeight > 0) {
RenderHelper.draw(leftMiddle.left, leftMiddle.bottom - remHeight, ninePatch.leftMiddle.u, ninePatch.leftMiddle.v, ninePatch.cornerWidth.toDouble(), remHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
RenderHelper.draw(rightMiddle.left, rightMiddle.bottom - remHeight, ninePatch.rightMiddle.u, ninePatch.rightMiddle.v, ninePatch.cornerWidth.toDouble(), remHeight.toDouble(), ninePatch.texture.width, ninePatch.texture.height)
}
}
private fun drawCenter() {
for (i in 0 until (center.height.toInt() / ninePatch.centerHeight)) {
drawCenterRow(center.top + i * ninePatch.centerHeight.toDouble(), ninePatch.centerHeight.toDouble())
}
val remHeight = center.height.toInt() % ninePatch.centerHeight
if (remHeight > 0) {
drawCenterRow(center.bottom - remHeight, remHeight.toDouble())
}
}
private fun drawCenterRow(y: Double, height: Double) {
for (i in 0 until (center.width.toInt() / ninePatch.centerWidth)) {
RenderHelper.draw(center.left + i * ninePatch.centerWidth, y, ninePatch.center.u, ninePatch.center.v, ninePatch.centerWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height)
}
val remWidth = center.width.toInt() % ninePatch.centerWidth
if (remWidth > 0) {
RenderHelper.draw(center.right - remWidth, y, ninePatch.center.u, ninePatch.center.v, remWidth.toDouble(), height, ninePatch.texture.width, ninePatch.texture.height)
}
}
}

View File

@ -0,0 +1,212 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.AxisPosition
import net.shadowfacts.cacao.geometry.AxisPosition.*
import no.birkett.kiwi.Constraint
import java.util.*
/**
* A view that lays out its children in a stack along either the horizontal for vertical axes.
* This view does not have any content of its own.
*
* Only arranged subviews will be laid out in the stack mode, normal subviews must perform their own layout.
*
* @author shadowfacts
* @param axis The primary axis that this stack lays out its children along.
* @param distribution The mode by which this stack lays out its children along the axis perpendicular to the
* primary [axis].
*/
open class StackView(
val axis: Axis,
val distribution: Distribution = Distribution.FILL,
val spacing: Double = 0.0
): View() {
// the internal mutable, list of arranged subviews
private val _arrangedSubviews = LinkedList<View>()
/**
* The list of arranged subviews belonging to this stack view.
* This list should never be mutated directly, only be calling the [addArrangedSubview]/[removeArrangedSubview]
* methods.
*/
val arrangedSubviews: List<View> = _arrangedSubviews
private var leadingConnection: Constraint? = null
private var trailingConnection: Constraint? = null
private var arrangedSubviewConnections = mutableListOf<Constraint>()
/**
* Adds an arranged subview to this view.
* Arranged subviews are laid out according to the stack. If you wish to add a subview that is laid out separately,
* use the normal [addSubview] method.
*
* @param view The view to add.
* @param index The index in this stack to add the view at.
* By default, adds the view to the end of the stack.
* @return The view that was added, as a convenience.
*/
fun <T: View> addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T {
addSubview(view)
_arrangedSubviews.add(index, view)
addConstraintsForArrangedView(view, index)
return view
}
private fun addConstraintsForArrangedView(view: View, index: Int) {
if (index == 0) {
if (leadingConnection != null) {
solver.removeConstraint(leadingConnection)
}
solver.dsl {
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, view)
}
}
if (index == arrangedSubviews.size - 1) {
if (trailingConnection != null) {
solver.removeConstraint(trailingConnection)
}
solver.dsl {
trailingConnection = anchor(TRAILING, view) equalTo anchor(TRAILING)
}
}
if (arrangedSubviews.size > 1) {
solver.dsl {
val previous = arrangedSubviews.getOrNull(index - 1)
val next = arrangedSubviews.getOrNull(index + 1)
if (next != null) {
arrangedSubviewConnections.add(index, anchor(TRAILING, view) equalTo (anchor(LEADING, next) + spacing))
}
if (previous != null) {
arrangedSubviewConnections.add(index - 1, anchor(TRAILING, previous) equalTo (anchor(LEADING, view) - spacing))
}
}
}
solver.dsl {
when (distribution) {
Distribution.LEADING ->
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
Distribution.TRAILING ->
perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
Distribution.FILL -> {
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
}
Distribution.CENTER ->
perpAnchor(CENTER) equalTo perpAnchor(CENTER, view)
}
}
}
private fun anchor(position: AxisPosition, view: View = this): LayoutVariable {
return view.getAnchor(axis, position)
}
private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable {
return view.getAnchor(axis.perpendicular, position)
}
/**
* Defines the modes of how content is distributed in a stack view along the perpendicular axis (i.e. the
* non-primary axis).
*
* ASCII-art examples are shown below in a stack view with the primary axis [Axis.VERTICAL].
*/
enum class Distribution {
/**
* The leading edges of arranged subviews are pinned to the leading edge of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
LEADING,
/**
* The centers of the arranged subviews are pinned to the center of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
CENTER,
/**
* The trailing edges of arranged subviews are pinned to the leading edge of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
TRAILING,
/**
* The arranged subviews fill the perpendicular axis of the stack view.
* ```
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
*
* ```
*/
FILL
}
}

View File

@ -0,0 +1,20 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.util.texture.Texture
/**
* A helper class for drawing a [Texture] in a view.
* `TextureView` will draw the given texture filling the bounds of the view.
*
* @author shadowfacts
*/
class TextureView(var texture: Texture): View() {
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.draw(bounds, texture)
}
}

View File

@ -0,0 +1,391 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.*
import net.shadowfacts.cacao.util.properties.ObservableLateInitProperty
import no.birkett.kiwi.Constraint
import no.birkett.kiwi.Solver
import java.lang.RuntimeException
import java.util.*
/**
* The base Cacao View class. Provides layout anchors, properties, and helper methods.
* Doesn't draw anything itself (unless [backgroundColor] is specified), but may be used for encapsulation/grouping.
*
* @author shadowfacts
*/
open class View() {
/**
* The window whose view hierarchy this view belongs to.
* Not initialized until the root view in this hierarchy has been added to a hierarchy,
* using it before that will throw a runtime exception.
*/
var window: Window? = null
/**
* The constraint solver used by the [net.shadowfacts.cacao.Window] this view belongs to.
* Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
*/
lateinit var solver: Solver
/**
* Layout anchor for the left edge of this view in the window's coordinate system.
*/
val leftAnchor = LayoutVariable(this, "left")
/**
* Layout anchor for the right edge of this view in the window's coordinate system.
*/
val rightAnchor = LayoutVariable(this, "right")
/**
* Layout anchor for the top edge of this view in the window's coordinate system.
*/
val topAnchor = LayoutVariable(this, "top")
/**
* Layout anchor for the bottom edge of this view in the window's coordinate system.
*/
val bottomAnchor = LayoutVariable(this, "bottom")
/**
* Layout anchor for the width of this view in the window's coordinate system.
*/
val widthAnchor = LayoutVariable(this, "width")
/**
* Layout anchor for the height of this view in the window's coordinate system.
*/
val heightAnchor = LayoutVariable(this, "height")
/**
* Layout anchor for the center X position of this view in the window's coordinate system.
*/
val centerXAnchor = LayoutVariable(this, "centerX")
/**
* Layout anchor for the center Y position of this view in the window's coordinate system.
*/
val centerYAnchor = LayoutVariable(this, "centerY")
/**
* Whether this view uses constraint-based layout.
* If `false`, the view's `frame` must be set manually and the layout anchors may not be used.
* Note: some views (such as [StackView] require arranged subviews to use constraint based layout.
*
* Default is `true`.
*/
var usesConstraintBasedLayout = true
/**
* The rectangle for this view in the coordinate system of its superview view (or the window, if there is no superview).
* If using constraint based layout, this property is not initialized until [didLayout] called.
* Otherwise, this must be set manually.
* Setting this property updates the [bounds].
*/
var frame: Rect by ObservableLateInitProperty { this.bounds = Rect(Point.ORIGIN, it.size) }
/**
* The rectangle for this view in its own coordinate system.
* If using constraint based layout, this property is not initialized until [didLayout] called.
* Otherwise, this will be initialized when [frame] is set.
*/
lateinit var bounds: Rect
/**
* The position on the Z-axis of this view.
* Views are rendered from lowest Z index to highest. Clicks are handled from highest to lowest.
*/
var zIndex: Double = 0.0
/**
* The intrinsic size of this view's content. May be null if the view doesn't have any content or there is no
* intrinsic size.
*
* Setting this creates/updates [no.birkett.kiwi.Strength.WEAK] constraints on this view's width/height using
* the size.
*/
var intrinsicContentSize: Size? = null
set(value) {
updateIntrinsicContentSizeConstraints(intrinsicContentSize, value)
field = value
}
private var intrinsicContentSizeWidthConstraint: Constraint? = null
private var intrinsicContentSizeHeightConstraint: Constraint? = null
/**
* The background color of this view.
*/
var backgroundColor = Color.CLEAR
var respondsToDragging = false
/**
* This view's parent view. If `null`, this view is a top-level view in the [Window].
*/
var superview: View? = null
// _subviews is the internal, mutable object since we only want it to be mutated by the add/removeSubview methods
private val _subviews = LinkedList<View>()
private var subviewsSortedByZIndex: List<View> = listOf()
/**
* The list of all the subviews of this view.
* This list should never by mutated directly, only by the [addSubview]/[removeSubview] methods.
*/
val subviews: List<View> = _subviews
constructor(frame: Rect): this() {
this.usesConstraintBasedLayout = false
this.frame = frame
}
/**
* Helper method for retrieve the anchor for a specific position on the given axis.
*/
fun getAnchor(axis: Axis, position: AxisPosition): LayoutVariable {
return when (axis) {
Axis.HORIZONTAL ->
when (position) {
AxisPosition.LEADING -> leftAnchor
AxisPosition.CENTER -> centerXAnchor
AxisPosition.TRAILING -> rightAnchor
}
Axis.VERTICAL ->
when (position) {
AxisPosition.LEADING -> topAnchor
AxisPosition.CENTER -> centerYAnchor
AxisPosition.TRAILING -> bottomAnchor
}
}
}
/**
* Adds the given subview as a child of this view.
*
* @param view The view to add.
* @return The view that was added, as a convenience.
*/
fun <T: View> addSubview(view: T): T {
_subviews.add(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = this
view.solver = solver
view.window = window
view.wasAdded()
return view
}
/**
* Removes the given view from this view's children and removes all constraints associated with it.
*
* @param view The view to removed as a child of this view.
* @throws RuntimeException If the given [view] is not a subview of this view.
*/
fun removeSubview(view: View) {
if (view.superview != this) {
throw RuntimeException("Cannot remove subview whose superview is not this view")
}
solver.constraints.filter { constraint ->
constraint.getVariables().any { it is LayoutVariable && it.owner == view }
}.forEach(solver::removeConstraint)
_subviews.remove(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = null
// todo: does this need to be reset
// view.solver = null
view.window = null
// todo: is this necessary?
// view.wasRemoved()
}
/**
* Removes this view from its superview, if it has one.
*/
fun removeFromSuperview() {
superview?.removeSubview(this)
}
/**
* Finds all subviews that contain the given point.
*
* @param point The point to find subviews for, in the coordinate system of this view.
* @return All views that contain the given point.
*/
fun subviewsAtPoint(point: Point): List<View> {
return subviews.filter { point in it.frame }
}
/**
* Attempts to find a subview which contains the given point.
* If multiple subviews contain the given point, which one this method returns is undefined.
* [subviewsAtPoint] may be used, and the resulting List sorted by [View.zIndex].
*
* @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.
*/
open fun wasAdded() {
createInternalConstraints()
updateIntrinsicContentSizeConstraints(null, intrinsicContentSize)
}
/**
* Called during [wasAdded] to add any constraints to the [solver] that are internal to this view.
* If overridden, the super-class method must be called.
*/
protected open fun createInternalConstraints() {
if (!usesConstraintBasedLayout) return
solver.dsl {
rightAnchor equalTo (leftAnchor + widthAnchor)
bottomAnchor equalTo (topAnchor + heightAnchor)
centerXAnchor equalTo (leftAnchor + widthAnchor / 2)
centerYAnchor equalTo (topAnchor + heightAnchor / 2)
}
}
private fun updateIntrinsicContentSizeConstraints(old: Size?, new: Size?) {
if (!usesConstraintBasedLayout || !this::solver.isInitialized) return
if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
solver.removeConstraint(intrinsicContentSizeHeightConstraint!!)
}
if (new != null) {
solver.dsl {
this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = WEAK))
this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = WEAK))
}
}
}
/**
* Called after this view has been laid-out.
* If overridden, the super-class method must be called.
*/
open fun didLayout() {
subviews.forEach(View::didLayout)
if (usesConstraintBasedLayout) {
val superviewLeft = superview?.leftAnchor?.value ?: 0.0
val superviewTop = superview?.topAnchor?.value ?: 0.0
frame = Rect(leftAnchor.value - superviewLeft, topAnchor.value - superviewTop, widthAnchor.value, heightAnchor.value)
bounds = Rect(0.0, 0.0, widthAnchor.value, heightAnchor.value)
}
}
/**
* Called to draw this view.
* This method should not be called directly, it is called by the parent view/window.
* 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 delta The time since the last frame.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top)
RenderHelper.fill(matrixStack, bounds, backgroundColor)
drawContent(matrixStack, mouse, delta)
subviewsSortedByZIndex.forEach {
val mouseInView = convert(mouse, to = it)
it.draw(matrixStack, mouseInView, delta)
}
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(matrixStack: MatrixStack, 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.
* @return Whether the mouse click was handled by this view or any subviews.
*/
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(point).maxBy(View::zIndex)
if (view != null) {
val pointInView = convert(point, to = view)
return view.mouseClicked(pointInView, mouseButton)
}
return false
}
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(startPoint).maxBy(View::zIndex)
if (view != null) {
val startInView = convert(startPoint, to = view)
return view.mouseDragged(startInView, delta, mouseButton)
}
return false
}
/**
* Converts the given point in this view's coordinate system to the coordinate system of another view or the window.
*
* @param point The point to convert, in the coordinate system of this view.
* @param to The view to convert to. If `null`, it will be converted to the window's coordinate system.
* @return The point in the coordinate system of the [to] view.
*/
fun convert(point: Point, to: View?): Point {
if (to != null) {
val ancestor = LowestCommonAncestor.find(this, to, View::superview)
@Suppress("NAME_SHADOWING") var point = point
// Convert up to the LCA
var view: View? = this
while (view != null && view != ancestor) {
point = Point(point.x + view.frame.left, point.y + view.frame.top)
view = view.superview
}
// Convert back down to the other view
view = to
while (view != null && view != ancestor) {
point = Point(point.x - view.frame.left, point.y - view.frame.top)
view = view.superview
}
return point
} else {
return Point(leftAnchor.value + point.x, topAnchor.value + point.y)
}
}
/**
* Converts the given rectangle in this view's coordinate system to the coordinate system of another view or the window.
*
* @param rect The rectangle to convert, in the coordinate system of this view.
* @param to The view to convert to. If `null`, it will be converted to the window's coordinate system.
* @return The rectangle in the coordinate system of the [to] view.
*/
fun convert(rect: Rect, to: View?): Rect {
return Rect(convert(rect.origin, to), rect.size)
}
}

View File

@ -0,0 +1,117 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.NinePatchView
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? = NinePatchView(NinePatchTexture.BUTTON_BG)
/**
* 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? = NinePatchView(NinePatchTexture.BUTTON_HOVERED_BG)
/**
* The background to draw when the button is [disabled].
* If `null`, the normal [background] will be used.
* @see background
*/
var disabledBackground: View? = NinePatchView(NinePatchTexture.BUTTON_DISABLED_BG)
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(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(frame.left, frame.top)
RenderHelper.fill(matrixStack, 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(matrixStack, mouse, delta)
val mouseInContent = convert(mouse, to = content)
content.draw(matrixStack, mouseInContent, delta)
// don't draw subviews, otherwise all background views + content will get drawn
RenderHelper.popMatrix()
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (disabled) return false
// 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?.invoke(this as Impl)
return true
}
}

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,206 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.util.properties.ResettableLazyProperty
import net.shadowfacts.cacao.view.NinePatchView
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
/**
* A button that provides a dropdown for the user to select from a list of values.
* The button itself shows a [ContentView] representing the currently selected [Value] and an image indicator that serves
* as a hint for the ability to click the button and display the dropdown.
*
* The dropdown list itself is displayed by presenting a new [Window] at the front of the window stack.
* Each possible value is represented in the list by a button containing a [ContentView] for that value, with the button
* for the current value being disabled.
*
* @author shadowfacts
* @param Value The type of value that the dropdown selects.
* @param ContentView The specific type of the [View] that represents selected item in the button and each item in the dropdown list.
* @param initialValue The initial value of the dropdown button.
* @param allValues List of all allowed values for the dropdown.
* @param createView A function that creates a [ContentView] representing the given [Value].
* Positioning of content views is handled by the dropdown.
* @param updateView A function for updating the view used as the button's 'label' that's visible even when the dropdown isn't.
*/
class DropdownButton<Value, ContentView: View>(
val initialValue: Value,
val allValues: Iterable<Value>,
val createView: (Value) -> ContentView,
val updateView: (newValue: Value, view: ContentView) -> Unit,
padding: Double = 4.0
): AbstractButton<DropdownButton<Value, ContentView>>(
StackView(Axis.HORIZONTAL),
padding
) {
companion object {
val DROPDOWN_INDICATOR = Texture(Identifier("asmr", "textures/gui/dropdown.png"), 0, 0)
}
private val stackView: StackView
get() = content as StackView
private val contentView: ContentView
get() = stackView.arrangedSubviews.first() as ContentView
private lateinit var dropdownIndicator: TextureView
/**
* The currently selected [Value] of the dropdown.
*/
var value: Value = initialValue
set(value) {
field = value
updateView(value, contentView)
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
override fun wasAdded() {
super.wasAdded()
stackView.addArrangedSubview(createView(initialValue))
dropdownIndicator = stackView.addArrangedSubview(TextureView(DROPDOWN_INDICATOR))
solver.dsl {
dropdownIndicator.widthAnchor equalTo 9
dropdownIndicator.heightAnchor equalTo 9
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
return if (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT) {
showDropdown()
true
} else {
super.mouseClicked(point, mouseButton)
}
}
private fun showDropdown() {
// val dropdownWindow = window.screen.addWindow(Window())
// val dropdownBackground = dropdownWindow.addView(NinePatchView(NinePatchTexture.BUTTON_BG).apply {
// zIndex = -1.0
// })
// val stack = dropdownWindow.addView(StackView(Axis.VERTICAL, StackView.Distribution.FILL))
// lateinit var selectedButton: View
// val buttons = mutableListOf<Button>()
// val last = allValues.count() - 1
// for ((index, value) in allValues.withIndex()) {
// val contentView = createView(value)
// val button = stack.addArrangedSubview(Button(contentView, padding).apply {
// background = null
// hoveredBackground = DropdownItemBackgroundView(index == 0, index == last, NinePatchTexture.BUTTON_HOVERED_BG)
// disabledBackground = DropdownItemBackgroundView(index == 0, index == last, NinePatchTexture.BUTTON_DISABLED_BG)
// disabled = value == this@DropdownButton.value
// handler = {
// dropdownWindow.removeFromScreen()
// valueSelected(value)
// }
// })
// if (value == this@DropdownButton.value) {
// selectedButton = button
// }
// buttons.add(button)
// dropdownWindow.solver.dsl {
// if (button.content.intrinsicContentSize != null) {
// button.widthAnchor greaterThanOrEqualTo button.content.intrinsicContentSize!!.width + 2 * button.padding
// }
// }
// }
// dropdownWindow.solver.dsl {
// // constrain to the DropdownButton anchor's value constant, because we're crossing windows and
// // therefore solvers, which isn't allowed
// stack.leftAnchor equalTo this@DropdownButton.rightAnchor.value
// selectedButton.centerYAnchor equalTo this@DropdownButton.centerYAnchor.value
//
// dropdownBackground.leftAnchor equalTo stack.leftAnchor
// dropdownBackground.rightAnchor equalTo stack.rightAnchor
// dropdownBackground.topAnchor equalTo stack.topAnchor
// dropdownBackground.bottomAnchor equalTo stack.bottomAnchor
// }
// dropdownWindow.layout()
}
private fun valueSelected(value: Value) {
this.value = value
handler?.invoke(this)
}
}
private class DropdownItemBackgroundView(
private val first: Boolean,
private val last: Boolean,
ninePatch: NinePatchTexture
): NinePatchView(ninePatch) {
// Corners
private val topLeftDelegate = ResettableLazyProperty {
super.topLeft
Rect(0.0, 0.0, ninePatch.cornerWidth.toDouble(), if (first) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val topLeft by topLeftDelegate
private val topRightDelegate = ResettableLazyProperty {
Rect(bounds.width - ninePatch.cornerWidth, 0.0, topLeft.width, topLeft.height)
}
override val topRight by topRightDelegate
private val bottomLeftDelegate = ResettableLazyProperty {
Rect(topLeft.left, bounds.height - ninePatch.cornerHeight, topLeft.width, if (last) ninePatch.cornerHeight.toDouble() else 0.0)
}
override val bottomLeft by bottomLeftDelegate
private val bottomRightDelegate = ResettableLazyProperty {
Rect(topRight.left, bottomLeft.top, topLeft.width, bottomLeft.height)
}
override val bottomRight by bottomRightDelegate
// Edges
private val topMiddleDelegate = ResettableLazyProperty {
Rect(ninePatch.cornerWidth.toDouble(), topLeft.top, bounds.width - 2 * ninePatch.cornerWidth, topLeft.height)
}
override val topMiddle by topMiddleDelegate
private val bottomMiddleDelegate = ResettableLazyProperty {
Rect(topMiddle.left, bottomLeft.top, topMiddle.width, bottomLeft.height)
}
override val bottomMiddle by bottomMiddleDelegate
private val leftMiddleDelegate = ResettableLazyProperty {
Rect(topLeft.left, topLeft.bottom, topLeft.width, bounds.height - (if (first && last) 2 else if (first || last) 1 else 0) * ninePatch.cornerHeight)
}
override val leftMiddle by leftMiddleDelegate
private val rightMiddleDelegate = ResettableLazyProperty {
Rect(topRight.left, topRight.bottom, topRight.width, leftMiddle.height)
}
override val rightMiddle by rightMiddleDelegate
// Center
private val centerDelegate = ResettableLazyProperty {
Rect(topLeft.right, topMiddle.bottom, topMiddle.width, leftMiddle.height)
}
override val center by centerDelegate
private val delegates = listOf(topLeftDelegate, topRightDelegate, bottomLeftDelegate, bottomRightDelegate, topMiddleDelegate, bottomMiddleDelegate, leftMiddleDelegate, rightMiddleDelegate, centerDelegate)
override fun didLayout() {
super.didLayout()
delegates.forEach(ResettableLazyProperty<Rect>::reset)
}
}

View File

@ -0,0 +1,45 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.EnumHelper
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.Label
/**
* A button that cycles through enum values.
* Left click: forwards
* Right click: backwards
* All other mouse buttons call the handler with the unchanged value
*
* @author shadowfacts
* @param initialValue The initial enum value for this button.
* @param localizer A function that takes an enum value and converts into a string for the button's label.
*/
class EnumButton<E: Enum<E>>(initialValue: E, val localizer: (E) -> String): AbstractButton<EnumButton<E>>(Label(localizer(initialValue))) {
private val label: Label
get() = content as Label
/**
* The current value of the enum button.
* Updating this property will use the [localizer] to update the label.
*/
var value: E = initialValue
set(value) {
field = value
label.text = localizer(value)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value)
else -> value
}
}
return super.mouseClicked(point, mouseButton)
}
}

View File

@ -0,0 +1,46 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.TextureView
/**
* A button for toggling between on/off states.
*
* @author shadowfacts
* @param initialState Whether the button starts as on or off.
*/
class ToggleButton(initialState: Boolean): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF).apply {
intrinsicContentSize = Size(19.0, 19.0)
}, padding = 0.0) {
companion object {
val ON = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 0)
val OFF = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 19)
}
private val textureView: TextureView
get() = content as TextureView
/**
* The button's current on/off state.
* Updating this property updates the button's texture.
*/
var state: Boolean = initialState
set(value) {
field = value
textureView.texture = if (value) ON else OFF
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
state = !state
}
return super.mouseClicked(point, mouseButton)
}
}

View File

@ -0,0 +1,192 @@
package net.shadowfacts.cacao.viewcontroller
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.util.properties.ObservableLazyProperty
import net.shadowfacts.cacao.view.View
import net.shadowfacts.kiwidsl.dsl
import java.lang.RuntimeException
import java.util.*
/**
* The base Cacao View Controller class. A view controller is an object that owns and manages a [View].
*
* The view controller receives lifecycle callbacks for its view.
*
* @author shadowfacts
*/
abstract class ViewController {
/**
* The window that contains this view controller.
* This property is not set until either:
* a) a [Window] is initialized with this VC as it's root view controller or
* b) this VC is added as a child of another view controller.
*/
var window: Window? = null
/**
* Helper function for creating layout constraints in the domain of this VC's window.
* This function is not usable until [window] is initialized.
*/
val createConstraints
get() = window!!.solver::dsl
/**
* The view that this View Controller has.
* This property is created by [loadView] and is not initialized before that method has been called.
*
* @see loadView
*/
lateinit var view: View
protected set
/**
* This VC's parent view controller. If `null`, this VC is the root view controller of its [window].
*/
var parent: ViewController? = null
set(value) {
willMoveTo(value)
field = value
didMoveTo(value)
}
// _children is the internal, mutable object since we only want it to be mutated by the embed/removeChild methods
private var _children = LinkedList<ViewController>()
/**
* The list of all the child VCs of this view controller.
* This list should never be mutated directly, only by the [embedChild]/[removeChild] methods.
*/
val children: List<ViewController> = _children
/**
* This method somehow loads a [View] and sets this VC's [view] property to it.
*
* This method should only be called by the framework. After the [view] property is set, the framework is
* responsible for initializing its [View.window]/[View.solver] properties and calling [View.wasAdded].
*
* The default implementation simply creates a [View] and does nothing else with it.
*/
open fun loadView() {
view = View()
}
/**
* This method is called after the view is loaded, it's properties are initialized, and [View.wasAdded] has been
* called.
*/
open fun viewDidLoad() {}
/**
* This method is called immediately before the [Window.solver] is going to solve constraints and update variables.
* If overridden, the superclass method must be called.
*/
open fun viewWillLayoutSubviews() {
children.forEach(ViewController::viewWillLayoutSubviews)
}
/**
* This method is called immediately after the [Window.solver] has solved constraints and variables have been updated.
* This method is responsible for invoking the VC's [View.didLayout] method.
* If overridden, the superclass method must be called.
*/
open fun viewDidLayoutSubviews() {
view.didLayout()
children.forEach(ViewController::viewDidLayoutSubviews)
}
/**
* Called when the VC's view has been added to the screen and is about to be displayed.
*/
open fun viewWillAppear() {
children.forEach(ViewController::viewWillAppear)
}
/**
* Called immediately after the VC's view has first been displayed on screen.
*/
open fun viewDidAppear() {
children.forEach(ViewController::viewDidAppear)
}
/**
* Called before the view will disappear from the screen, either because the VC has been removed from it's parent/screen
* or because the [net.shadowfacts.cacao.CacaoScreen] has been closed.
*/
open fun viewWillDisappear() {
children.forEach(ViewController::viewWillDisappear)
}
/**
* Called after the view has disappeared from the screen.
*/
open fun viewDidDisappear() {
children.forEach(ViewController::viewDidDisappear)
}
/**
* Called before the view controller's parent changes to the given new value.
*
* @param parent The new parent view controller.
*/
open fun willMoveTo(parent: ViewController?) {}
/**
* Called after the view controller's parent has changed to the given new value.
*
* @param parent The new parent view controller.
*/
open fun didMoveTo(parent: ViewController?) {}
/**
* Embeds a child view controller in this VC.
*
* @param viewController The new child VC.
* @param container The view that will be used as the superview for the child VC's view. Defaults to this VC's [view].
* @param pinEdges Whether the edges of the child VC will be pinned (constrained to be equal to) the container's edges.
* Defaults to `true`.
*/
fun embedChild(viewController: ViewController, container: View = this.view, pinEdges: Boolean = true) {
viewController.parent = this
viewController.window = window
_children.add(viewController)
viewController.loadView()
container.addSubview(viewController.view)
if (pinEdges) {
createConstraints {
viewController.view.leftAnchor equalTo container.leftAnchor
viewController.view.rightAnchor equalTo container.rightAnchor
viewController.view.topAnchor equalTo container.topAnchor
viewController.view.bottomAnchor equalTo container.bottomAnchor
}
}
viewController.viewDidLoad()
}
/**
* Removes the given view controller
*
* @param viewController The child VC to remove from this view controller.
* @throws RuntimeException If the given [viewController] is not a child of this VC.
*/
fun removeChild(viewController: ViewController) {
if (viewController.parent != this) {
throw RuntimeException("Cannot remove child view controller whose parent is not this view controller")
}
viewController.parent = null
_children.remove(viewController)
}
/**
* Removes this view controller from its parent, if it has one.
*/
fun removeFromParent() {
parent?.removeChild(this)
view.removeFromSuperview()
// todo: remove view from superview
}
}

View File

@ -0,0 +1,103 @@
package net.shadowfacts.kiwidsl
import no.birkett.kiwi.*
/**
* @author shadowfacts
*/
class KiwiContext(val solver: Solver) {
val REQUIRED = Strength.REQUIRED
val STRONG = Strength.STRONG
val MEDIUM = Strength.MEDIUM
val WEAK = Strength.WEAK
// Constraints
infix fun ExpressionConvertible.equalTo(other: ExpressionConvertible): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
fun ExpressionConvertible.equalTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.equalTo(constant: Number): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
fun ExpressionConvertible.equalTo(constant: Number, strength: Double): Constraint {
return Symbolics.equals(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
fun ExpressionConvertible.lessThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.lessThanOrEqualTo(constant: Number): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
fun ExpressionConvertible.lessThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.lessThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).apply(solver::addConstraint)
}
fun ExpressionConvertible.greaterThanOrEqualTo(other: ExpressionConvertible, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), other.toExpression()).setStrength(strength).apply(solver::addConstraint)
}
infix fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).apply(solver::addConstraint)
}
fun ExpressionConvertible.greaterThanOrEqualTo(constant: Number, strength: Double): Constraint {
return Symbolics.greaterThanOrEqualTo(this.toExpression(), constant.toDouble()).setStrength(strength).apply(solver::addConstraint)
}
// Addition
operator fun ExpressionConvertible.plus(other: ExpressionConvertible): Expression {
return Symbolics.add(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.plus(constant: Number): Expression {
return Symbolics.add(this.toExpression(), constant.toDouble())
}
// Subtraction
operator fun ExpressionConvertible.minus(other: ExpressionConvertible): Expression {
return Symbolics.subtract(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.minus(constant: Number): Expression {
return Symbolics.subtract(this.toExpression(), constant.toDouble())
}
// Multiplication
operator fun ExpressionConvertible.times(other: ExpressionConvertible): Expression {
return Symbolics.multiply(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.times(constant: Number): Expression {
return Symbolics.multiply(this.toExpression(), constant.toDouble())
}
// Division
operator fun ExpressionConvertible.div(other: ExpressionConvertible): Expression {
return Symbolics.divide(this.toExpression(), other.toExpression())
}
operator fun ExpressionConvertible.div(constant: Number): Expression {
return Symbolics.divide(this.toExpression(), constant.toDouble())
}
}
fun Solver.dsl(init: KiwiContext.() -> Unit): Solver {
KiwiContext(this).init()
return this
}

View File

@ -0,0 +1,56 @@
package net.shadowfacts.phycon.screen
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.texture.NinePatchTexture
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.NinePatchView
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
/**
* @author shadowfacts
*/
class TestCacaoScreen: CacaoScreen() {
init {
val viewController = object: ViewController() {
override fun loadView() {
view = View()
}
override fun viewDidLoad() {
super.viewDidLoad()
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0)).apply {
backgroundColor = Color.WHITE
}
val birch = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16))).apply {
intrinsicContentSize = Size(50.0, 50.0)
}
val ninePatch = stack.addArrangedSubview(NinePatchView(NinePatchTexture.PANEL_BG)).apply {
intrinsicContentSize = Size(75.0, 100.0)
}
val red = stack.addArrangedSubview(View()).apply {
intrinsicContentSize = Size(50.0, 50.0)
backgroundColor = Color.RED
}
view.solver.dsl {
stack.topAnchor equalTo 0
stack.centerXAnchor equalTo window!!.centerXAnchor
stack.widthAnchor equalTo 100
}
}
}
addWindow(Window(viewController))
}
}

View File

@ -0,0 +1,90 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class CoordinateConversionTests {
lateinit var screen: CacaoScreen
lateinit var viewController: ViewController
lateinit var window: Window
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testConvertToParent() {
val a = view.addSubview(View(Rect(0.0, 0.0, 100.0, 100.0)))
val b = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
assertEquals(Point(25.0, 25.0), b.convert(Point(0.0, 0.0), to = a))
assertEquals(Point(75.0, 75.0), b.convert(Point(50.0, 50.0), to = a))
assertEquals(Rect(25.0, 25.0, 50.0, 50.0), b.convert(Rect(0.0, 0.0, 50.0, 50.0), to = a))
}
@Test
fun testConvertToSibling() {
val root = view.addSubview(View(Rect(0.0, 0.0, 200.0, 200.0)))
val a = root.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
val b = root.addSubview(View(Rect(75.0, 75.0, 50.0, 50.0)))
assertEquals(Point(-50.0, -50.0), a.convert(Point(0.0, 0.0), to = b))
assertEquals(Point(100.0, 100.0), b.convert(Point(50.0, 50.0), to = a))
assertEquals(Rect(50.0, 50.0, 50.0, 50.0), b.convert(Rect(0.0, 0.0, 50.0, 50.0), to = a))
}
@Test
fun testConvertBetweenSubtrees() {
val root = view.addSubview(View(Rect(0.0, 0.0, 200.0, 100.0)))
val a = root.addSubview(View(Rect(0.0, 0.0, 100.0, 100.0)))
val b = root.addSubview(View(Rect(100.0, 0.0, 100.0, 100.0)))
val c = a.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0)))
val d = b.addSubview(View(Rect(0.0, 0.0, 50.0, 50.0)))
assertEquals(Point(-100.0, 0.0), c.convert(Point(0.0, 0.0), to = b))
assertEquals(Point(-50.0, 50.0), c.convert(Point(50.0, 50.0), to = d))
assertEquals(Rect(100.0, 0.0, 50.0, 50.0), d.convert(Rect(0.0, 0.0, 50.0, 50.0), to = c))
}
@Test
fun testConvertBetweenTopLevelViews() {
val a = view.addSubview(View(Rect(0.0, 0.0, 100.0, 100.0)))
val b = view.addSubview(View(Rect(100.0, 100.0, 100.0, 100.0)))
assertEquals(Point(0.0, 0.0), a.convert(Point(100.0, 100.0), to = b))
assertEquals(Point(200.0, 200.0), b.convert(Point(100.0, 100.0), to = a))
assertEquals(Rect(100.0, 100.0, 100.0, 100.0), b.convert(Rect(0.0, 0.0, 100.0, 100.0), to = a))
}
@Test
fun testConvertBetweenTopLevelSubtrees() {
val a = view.addSubview(View(Rect(0.0, 0.0, 100.0, 100.0)))
val b = view.addSubview(View(Rect(100.0, 100.0, 100.0, 100.0)))
val c = a.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
val d = b.addSubview(View(Rect(25.0, 25.0, 50.0, 50.0)))
assertEquals(Point(-50.0, -50.0), c.convert(Point(50.0, 50.0), to = d))
assertEquals(Point(100.0, 100.0), d.convert(Point(0.0, 0.0), to = c))
assertEquals(Rect(100.0, 100.0, 50.0, 50.0), d.convert(Rect(0.0, 0.0, 50.0, 50.0), to = c))
}
}

View File

@ -0,0 +1,95 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class WindowLayoutTests {
lateinit var screen: CacaoScreen
lateinit var viewController: ViewController
lateinit var window: Window
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testConstraintToConstant() {
val view = view.addSubview(View())
window.solver.dsl {
view.leftAnchor equalTo 100
view.rightAnchor equalTo 200
view.topAnchor equalTo 100
view.heightAnchor equalTo 200
}
window.layout()
assertEquals(100.0, view.widthAnchor.value)
assertEquals(300.0, view.bottomAnchor.value)
}
@Test
fun testConstraintToView() {
val one = view.addSubview(View())
val two = view.addSubview(View())
window.solver.dsl {
one.leftAnchor equalTo 0
one.widthAnchor equalTo 100
one.topAnchor equalTo 0
one.heightAnchor equalTo 200
two.leftAnchor equalTo one.rightAnchor
two.rightAnchor equalTo 400
two.topAnchor equalTo one.bottomAnchor
two.heightAnchor equalTo one.heightAnchor
}
window.layout()
assertEquals(100.0, two.leftAnchor.value)
assertEquals(300.0, two.widthAnchor.value)
assertEquals(200.0, two.topAnchor.value)
assertEquals(400.0, two.bottomAnchor.value)
}
@Test
fun testIntrinsicContentSize() {
val view = view.addSubview(View()).apply {
intrinsicContentSize = Size(100.0, 200.0)
}
window.solver.dsl {
view.leftAnchor equalTo 0
view.topAnchor equalTo 100
}
window.layout()
assertEquals(100.0, view.widthAnchor.value)
assertEquals(100.0, view.rightAnchor.value)
assertEquals(200.0, view.heightAnchor.value)
assertEquals(300.0, view.bottomAnchor.value)
}
}

View File

@ -0,0 +1,39 @@
package net.shadowfacts.cacao.geometry
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class RectTest {
@Test
fun testTrailingEdges() {
val rect = Rect(25.0, 50.0, 100.0, 200.0)
assertEquals(125.0, rect.right)
assertEquals(250.0, rect.bottom)
}
@Test
fun testCenter() {
val rect = Rect(25.0, 50.0, 100.0, 200.0)
assertEquals(75.0, rect.midX)
assertEquals(150.0, rect.midY)
}
@Test
fun testPoints() {
val rect = Rect(25.0, 50.0, 100.0, 200.0)
assertEquals(Point(25.0, 50.0), rect.origin)
assertEquals(Point(75.0, 150.0), rect.center)
}
@Test
fun testSize() {
val rect = Rect(25.0, 50.0, 100.0, 200.0)
assertEquals(Size(100.0, 200.0), rect.size)
}
}

View File

@ -0,0 +1,25 @@
package net.shadowfacts.cacao.util
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class ColorTest {
@Test
fun fromRGB() {
val color = Color(0x123456)
assertEquals(0x12, color.red)
assertEquals(0x34, color.green)
assertEquals(0x56, color.blue)
}
@Test
fun toARGB() {
val color = Color(red = 0x12, green = 0x34, blue = 0x56, alpha = 0x78)
assertEquals(0x78123456, color.argb)
}
}

View File

@ -0,0 +1,27 @@
package net.shadowfacts.cacao.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class EnumHelperTests {
enum class MyEnum {
ONE, TWO
}
@Test
fun testNext() {
assertEquals(MyEnum.TWO, EnumHelper.next(MyEnum.ONE))
assertEquals(MyEnum.ONE, EnumHelper.next(MyEnum.TWO))
}
@Test
fun testPrev() {
assertEquals(MyEnum.ONE, EnumHelper.previous(MyEnum.TWO))
assertEquals(MyEnum.TWO, EnumHelper.previous(MyEnum.ONE))
}
}

View File

@ -0,0 +1,73 @@
package net.shadowfacts.cacao.util
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class LCATest {
class Node(val name: String, val parent: Node?)
@Test
fun testDirectParent() {
val parent = Node("parent", null)
val child = Node("child", parent)
assertEquals(parent, LowestCommonAncestor.find(parent, child, Node::parent))
assertEquals(parent, LowestCommonAncestor.find(child, parent, Node::parent))
}
@Test
fun testSiblings() {
val root = Node("root", null)
val a = Node("a", root)
val b = Node("b", root)
assertEquals(root, LowestCommonAncestor.find(a, b, Node::parent))
}
@Test
fun testBetweenSubtrees() {
// ┌────┐
// │root│
// └────┘
//
//
// ┌─┐ ┌─┐
// │A│ │B│
// └─┘ └─┘
//
//
// ┌─┐ ┌─┐┌─┐ ┌─┐
// │C│ │D││E│ │F│
// └─┘ └─┘└─┘ └─┘
val root = Node("root", null)
val a = Node("a", root)
val c = Node("c", a)
val d = Node("d", a)
val b = Node("b", root)
val e = Node("e", b)
val f = Node("f", b)
assertEquals(a, LowestCommonAncestor.find(c, d, Node::parent))
assertEquals(root, LowestCommonAncestor.find(c, b, Node::parent))
assertEquals(root, LowestCommonAncestor.find(d, e, Node::parent))
assertEquals(root, LowestCommonAncestor.find(c, root, Node::parent))
}
@Test
fun testBetweenDisjointTrees() {
val a = Node("a", null)
val b = Node("b", a)
val c = Node("c", null)
val d = Node("d", c)
assertNull(LowestCommonAncestor.find(a, d, Node::parent))
assertNull(LowestCommonAncestor.find(b, c, Node::parent))
}
}

View File

@ -0,0 +1,33 @@
package net.shadowfacts.cacao.util.properties
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class ObservableLateInitPropertyTests {
class MyClass(callback: (String) -> Unit) {
val delegate = ObservableLateInitProperty(callback)
var prop by delegate
}
@Test
fun testObservation() {
val future = CompletableFuture<String>()
val obj = MyClass { future.complete(it) }
obj.prop = "test"
assertEquals("test", future.getNow(null))
}
@Test
fun testIsInitialized() {
val obj = MyClass {}
assertFalse(obj.delegate.isInitialized)
obj.prop = "test"
assertTrue(obj.delegate.isInitialized)
}
}

View File

@ -0,0 +1,35 @@
package net.shadowfacts.cacao.util.properties
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.Test
/**
* @author shadowfacts
*/
class ResettableLazyPropertyTests {
class MyClass {
var iteration = 1
val delegate = ResettableLazyProperty { "test ${iteration++}" }
val prop by delegate
}
@Test
fun testResets() {
val obj = MyClass()
assertEquals("test 1", obj.prop)
obj.delegate.reset()
assertEquals("test 2", obj.prop)
assertEquals("test 2", obj.prop)
}
@Test
fun testIsInitialized() {
val obj = MyClass()
assertFalse(obj.delegate.isInitialized)
assertEquals("test 1", obj.prop)
assertTrue(obj.delegate.isInitialized)
obj.delegate.reset()
assertFalse(obj.delegate.isInitialized)
}
}

View File

@ -0,0 +1,261 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.viewcontroller.ViewController
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.math.abs
/**
* @author shadowfacts
*/
class StackViewLayoutTests {
lateinit var screen: CacaoScreen
lateinit var viewController: ViewController
lateinit var window: Window
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testVerticalLayout() {
val stack = view.addSubview(StackView(Axis.VERTICAL))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
}
window.layout()
assertEquals(0.0, abs(one.topAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
assertEquals(50.0, one.bottomAnchor.value)
assertEquals(50.0, two.topAnchor.value)
assertEquals(125.0, two.bottomAnchor.value)
assertEquals(125.0, three.topAnchor.value)
assertEquals(175.0, three.bottomAnchor.value)
assertEquals(175.0, stack.heightAnchor.value)
}
@Test
fun testHorizontalLayout() {
val stack = view.addSubview(StackView(Axis.HORIZONTAL))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
window.solver.dsl {
stack.leftAnchor equalTo 0
}
window.layout()
assertEquals(0.0, abs(one.leftAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
assertEquals(50.0, one.rightAnchor.value)
assertEquals(50.0, two.leftAnchor.value)
assertEquals(125.0, two.rightAnchor.value)
assertEquals(125.0, three.leftAnchor.value)
assertEquals(175.0, three.rightAnchor.value)
assertEquals(175.0, stack.widthAnchor.value)
}
@Test
fun testVerticalLayoutWithLeading() {
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.LEADING))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(100.0, 100.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
stack.leftAnchor equalTo 0
stack.rightAnchor equalTo 100
}
window.layout()
assertEquals(0.0, abs(one.leftAnchor.value))
assertEquals(50.0, one.rightAnchor.value)
assertEquals(0.0, abs(two.leftAnchor.value))
assertEquals(75.0, two.rightAnchor.value)
assertEquals(0.0, abs(three.leftAnchor.value))
assertEquals(100.0, three.rightAnchor.value)
}
@Test
fun testVerticalLayoutWithTrailing() {
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.TRAILING))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(100.0, 100.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
stack.leftAnchor equalTo 0
stack.rightAnchor equalTo 100
}
window.layout()
assertEquals(50.0, one.leftAnchor.value)
assertEquals(100.0, one.rightAnchor.value)
assertEquals(25.0, two.leftAnchor.value)
assertEquals(100.0, two.rightAnchor.value)
assertEquals(0.0, abs(three.leftAnchor.value))
assertEquals(100.0, three.rightAnchor.value)
}
@Test
fun testVerticalLayoutWithCenter() {
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(100.0, 100.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
stack.leftAnchor equalTo 0
stack.rightAnchor equalTo 100
}
window.layout()
assertEquals(25.0, one.leftAnchor.value)
assertEquals(75.0, one.rightAnchor.value)
assertEquals(12.5, two.leftAnchor.value)
assertEquals(87.5, two.rightAnchor.value)
assertEquals(0.0, abs(three.leftAnchor.value))
assertEquals(100.0, three.rightAnchor.value)
}
@Test
fun testVerticalLayoutWithFill() {
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.FILL))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(100.0, 100.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
stack.leftAnchor equalTo 0
stack.rightAnchor equalTo 100
}
window.layout()
assertEquals(0.0, abs(one.leftAnchor.value))
assertEquals(100.0, one.rightAnchor.value)
assertEquals(0.0, abs(two.leftAnchor.value))
assertEquals(100.0, two.rightAnchor.value)
assertEquals(0.0, abs(three.leftAnchor.value))
assertEquals(100.0, three.rightAnchor.value)
}
@Test
fun testVerticalLayoutWithSpacing() {
val stack = view.addSubview(StackView(Axis.VERTICAL, spacing = 10.0))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
window.solver.dsl {
stack.topAnchor equalTo 0
}
window.layout()
assertEquals(0.0, abs(one.topAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
assertEquals(50.0, one.bottomAnchor.value)
assertEquals(60.0, two.topAnchor.value)
assertEquals(135.0, two.bottomAnchor.value)
assertEquals(145.0, three.topAnchor.value)
assertEquals(195.0, three.bottomAnchor.value)
assertEquals(195.0, stack.heightAnchor.value)
}
@Test
fun testHorizontalLayoutWithSpacing() {
val stack = view.addSubview(StackView(Axis.HORIZONTAL, spacing = 10.0))
val one = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
val two = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(75.0, 75.0)
})
val three = stack.addArrangedSubview(View().apply {
intrinsicContentSize = Size(50.0, 50.0)
})
window.solver.dsl {
stack.leftAnchor equalTo 0
}
window.layout()
assertEquals(0.0, abs(one.leftAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
assertEquals(50.0, one.rightAnchor.value)
assertEquals(60.0, two.leftAnchor.value)
assertEquals(135.0, two.rightAnchor.value)
assertEquals(145.0, three.leftAnchor.value)
assertEquals(195.0, three.rightAnchor.value)
assertEquals(195.0, stack.widthAnchor.value)
}
}

View File

@ -0,0 +1,95 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.viewcontroller.ViewController
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 screen: CacaoScreen
lateinit var window: Window
lateinit var viewController: ViewController
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testClickInsideRootView() {
val mouse = CompletableFuture<Point>()
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
mouse.complete(point)
return true
}
})
assertTrue(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>()
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
clicked.complete(true)
return true
}
})
assertFalse(window.mouseClicked(Point(25.0, 25.0), MouseButton.LEFT))
assertFalse(clicked.getNow(false))
}
@Test
fun testClickInsideNestedView() {
val mouse = CompletableFuture<Point>()
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
mouse.complete(point)
return true
}
})
assertTrue(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 = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
clicked.complete(true)
return true
}
})
assertFalse(window.mouseClicked(Point(0.0, 0.0), MouseButton.LEFT))
assertFalse(clicked.getNow(false))
}
}

View File

@ -0,0 +1,73 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.viewcontroller.ViewController
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 screen: CacaoScreen
lateinit var viewController: ViewController
lateinit var window: Window
val view: View
get() = viewController.view
companion object {
@BeforeAll
@JvmStatic
fun setupAll() {
System.setProperty("cacao.drawing.disabled", "true")
}
}
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testHoverRootView() {
val point = CompletableFuture<Point>()
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
point.complete(mouse)
}
})
window.draw(MatrixStack(), Point(75.0, 75.0), 0f)
assertEquals(Point(25.0, 25.0), point.getNow(null))
}
@Test
fun testHoverNestedView() {
val point = CompletableFuture<Point>()
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
point.complete(mouse)
}
})
window.draw(MatrixStack(), Point(100.0, 100.0), 0f)
assertEquals(Point(25.0, 25.0), point.getNow(null))
}
}

View File

@ -0,0 +1,83 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.CacaoScreen
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.View
import net.shadowfacts.cacao.viewcontroller.ViewController
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 screen: CacaoScreen
lateinit var window: Window
lateinit var viewController: ViewController
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testClickInsideButton() {
val clicked = CompletableFuture<Boolean>()
val content = View().apply {
intrinsicContentSize = Size(25.0, 25.0)
}
val button = view.addSubview(Button(content).apply {
handler = {
clicked.complete(true)
}
})
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
}
window.layout()
assertTrue(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 = view.addSubview(Button(content).apply {
handler = {
clicked.complete(true)
}
})
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
}
window.layout()
assertFalse(window.mouseClicked(Point(50.0, 50.0), MouseButton.LEFT))
assertFalse(clicked.getNow(false))
}
}

View File

@ -0,0 +1,95 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
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 EnumButtonTests {
companion object {
@BeforeAll
@JvmStatic
fun setupAll() {
System.setProperty("cacao.drawing.disabled", "true")
}
}
enum class MyEnum {
ONE, TWO, THREE
}
lateinit var screen: CacaoScreen
lateinit var window: Window
lateinit var viewController: ViewController
lateinit var button: EnumButton<MyEnum>
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
button = viewController.view.addSubview(EnumButton(MyEnum.ONE, MyEnum::name))
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
button.widthAnchor equalTo 25
button.heightAnchor equalTo 25
}
window.layout()
}
@Test
fun testHandlerCalled() {
val called = CompletableFuture<Boolean>()
button.handler = {
called.complete(true)
}
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertTrue(called.getNow(false))
assertEquals(MyEnum.TWO, button.value)
}
@Test
fun testCyclesValues() {
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.TWO, button.value)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.THREE, button.value)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.ONE, button.value)
}
@Test
fun testCyclesValuesBackwards() {
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.TWO, button.value)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.RIGHT))
assertEquals(MyEnum.ONE, button.value)
}
@Test
fun testMiddleClickDoesNotChangeValue() {
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.MIDDLE))
assertEquals(MyEnum.ONE, button.value)
}
}

View File

@ -0,0 +1,86 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import org.junit.jupiter.api.Assertions.*
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 ToggleButtonTests {
companion object {
@BeforeAll
@JvmStatic
fun setupAll() {
System.setProperty("cacao.drawing.disabled", "true")
}
}
lateinit var screen: CacaoScreen
lateinit var window: Window
lateinit var viewController: ViewController
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testHandlerCalled() {
val called = CompletableFuture<Boolean>()
val button = view.addSubview(ToggleButton(false).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
handler = {
called.complete(true)
}
})
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertTrue(called.getNow(false))
}
@Test
fun testTogglesValues() {
val button = view.addSubview(ToggleButton(false).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
})
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertTrue(button.state)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertFalse(button.state)
}
@Test
fun testMiddleClickDoesNotChangeValue() {
val button = view.addSubview(ToggleButton(false).apply {
frame = Rect(0.0, 0.0, 25.0, 25.0)
content.frame = bounds
})
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.MIDDLE))
assertFalse(button.state)
}
}

View File

@ -0,0 +1,78 @@
package net.shadowfacts.kiwidsl
import no.birkett.kiwi.Solver
import no.birkett.kiwi.Variable
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import kotlin.math.abs
/**
* @author shadowfacts
*/
class DSLTest {
val EPSILON = 1.0e-8
lateinit var solver: Solver
@BeforeEach
fun setup() {
solver = Solver()
}
@Test
fun simpleNew() {
val x = Variable("x")
solver.dsl {
(x + 2) equalTo 20
}
solver.updateVariables()
assertEquals(x.value, 18.0, EPSILON)
}
@Test
fun simple0() {
val x = Variable("x")
val y = Variable("y")
solver.dsl {
x equalTo 20
(x + 2) equalTo (y + 10)
}
solver.updateVariables()
assertEquals(x.value, 20.0, EPSILON)
assertEquals(y.value, 12.0, EPSILON)
}
@Test
fun simple1() {
val x = Variable("x")
val y = Variable("y")
solver.dsl {
x equalTo y
}
solver.updateVariables()
assertEquals(x.value, y.value, EPSILON)
}
@Test
fun casso1() {
val x = Variable("x")
val y = Variable("y")
solver.dsl {
x lessThanOrEqualTo y
y equalTo (x + 3)
x.equalTo(10, strength = WEAK)
y.equalTo(10, strength = WEAK)
}
solver.updateVariables()
if (abs(x.value - 10.0) < EPSILON) {
assertEquals(10.0, x.value, EPSILON)
assertEquals(13.0, y.value, EPSILON)
} else {
assertEquals(7.0, x.value, EPSILON)
assertEquals(10.0, y.value, EPSILON)
}
}
}