Add Cacao
This commit is contained in:
parent
aec32ae270
commit
8f577598ff
|
@ -0,0 +1,3 @@
|
|||
[submodule "kiwi-java"]
|
||||
path = kiwi-java
|
||||
url = git@git.shadowfacts.net:shadowfacts/kiwi-java.git
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Subproject commit 1cbaea53d207f1e16c6e5ee2e6bf6e3c1440ac44
|
|
@ -7,4 +7,6 @@ pluginManagement {
|
|||
}
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include("kiwi-java")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)"
|
||||
|
||||
}
|
|
@ -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
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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!!
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue