Compare commits

..

11 Commits

77 changed files with 5316 additions and 214 deletions

3
.gitmodules vendored Normal file
View File

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

View File

@ -2,6 +2,7 @@ plugins {
id "fabric-loom" version "0.6.49"
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
@ -19,6 +20,7 @@ repositories {
maven {
url = "https://mod-buildcraft.com/maven"
}
jcenter()
}
dependencies {
@ -36,6 +38,10 @@ dependencies {
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
include "alexiil.mc.lib:libblockattributes-items:${project.libblockattributes_version}"
shadow project(":kiwi-java")
testImplementation "org.junit.jupiter:junit-jupiter:${project.junit_version}"
}
processResources {

View File

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

1
kiwi-java Submodule

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

View File

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

View File

@ -39,6 +39,9 @@ public final class IPAddress {
int b = Integer.parseInt(matcher.group(2));
int c = Integer.parseInt(matcher.group(3));
int d = Integer.parseInt(matcher.group(4));
if (a > 255 || b > 255 || c > 255 || d > 255) {
return null;
}
return new IPAddress(a, b, c, d);
}

View File

@ -15,7 +15,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(HandledScreen.class)
public class MixinHandledScreen {
@Inject(method = "drawSlot(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/screen/slot/Slot;)V", at = @At(value = "INVOKE", target = "enableDepthTest()V"))
@Inject(method = "drawSlot(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/screen/slot/Slot;)V", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;enableDepthTest()V"))
private void drawSlot(MatrixStack matrixStack, Slot slot, CallbackInfo ci) {
if ((Object)this instanceof TerminalScreen) {
TerminalScreen self = (TerminalScreen)(Object)this;

View File

@ -0,0 +1,14 @@
package net.shadowfacts.phycon.mixin.client;
import net.minecraft.client.gui.widget.TextFieldWidget;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
/**
* @author shadowfacts
*/
@Mixin(TextFieldWidget.class)
public interface TextFieldWidgetAccessor {
@Accessor("maxLength")
int cacao_getMaxLength();
}

View File

@ -0,0 +1,17 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.window.Window
/**
* @author shadowfacts
*/
interface AbstractCacaoScreen {
val windows: List<Window>
fun <T: Window> addWindow(window: T, index: Int): T
fun <T: Window> addWindow(window: T): T
fun removeWindow(window: Window)
}

View File

@ -0,0 +1,93 @@
package net.shadowfacts.cacao
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.ScreenHandler
import net.minecraft.sound.SoundEvents
import net.minecraft.text.Text
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.window.ScreenHandlerWindow
import net.shadowfacts.cacao.window.Window
import java.util.*
/**
* @author shadowfacts
*/
class CacaoHandledScreen<Handler: ScreenHandler>(
handler: Handler,
playerInv: PlayerInventory,
title: Text,
): HandledScreen<Handler>(handler, playerInv, title), AbstractCacaoScreen {
private val _windows = LinkedList<Window>()
override val windows: List<Window> = _windows
override fun <T: Window> addWindow(window: T, index: Int): T {
if (window is ScreenHandlerWindow && window.screenHandler != handler) {
throw RuntimeException("Adding ScreenHandlerWindow to CacaoHandledScreen with different screen handler is not supported")
}
_windows.add(index, window)
window.screen = this
window.wasAdded()
window.resize(width, height)
return window
}
override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size)
}
override fun removeWindow(window: Window) {
_windows.remove(window)
}
override fun init() {
super.init()
windows.forEach {
it.resize(width, height)
}
}
override fun drawBackground(matrixStack: MatrixStack, delta: Float, mouseX: Int, mouseY: Int) {
// no-op
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrixStack)
val mouse = Point(mouseX, mouseY)
windows.forEachIndexed { index, it ->
it.draw(matrixStack, mouse, delta)
if (it is ScreenHandlerWindow) {
if (index == windows.size - 1) {
super.render(matrixStack, mouseX, mouseY, delta)
} else {
// if the screen handler window is not the frontmost, we fake the mouse x/y to disable the slot mouseover effect
super.render(matrixStack, -1, -1, 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 if (window is ScreenHandlerWindow) {
super.mouseClicked(mouseX, mouseY, button)
} else {
false
}
}
}

View File

@ -0,0 +1,153 @@
package net.shadowfacts.cacao
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.minecraft.text.Text
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.window.Window
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(title: Text = LiteralText("CacaoScreen")): Screen(title), AbstractCacaoScreen {
// _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.
*
* The window at the end of this list is the active window is the only window that will receive input events.
*
* This list should never be modified directly, only by using the [addWindow]/[removeWindow] methods.
*/
override val windows: List<Window> = _windows
/**
* Adds the given window to this screen's window list at the given position.
*
* @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.
*/
override fun <T: Window> addWindow(window: T, index: Int): T {
_windows.add(index, window)
window.screen = this
window.wasAdded()
window.resize(width, height)
return window
}
/**
* Adds the given window to the end of this screen's window list, making it the active window.
*/
override fun <T : Window> addWindow(window: T): T {
return addWindow(window, _windows.size)
}
/**
* Removes the given window from this screen's window list.
*/
override fun removeWindow(window: Window) {
_windows.remove(window)
// todo: VC callbacks
}
override fun init() {
super.init()
windows.forEach {
it.resize(width, height)
}
}
override fun onClose() {
super.onClose()
windows.forEach {
// todo: VC callbacks
// resign the current first responder (if any)
it.firstResponder = null
}
}
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
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun keyReleased(i: Int, j: Int, k: Int): Boolean {
return super.keyReleased(i, j, k)
}
override fun charTyped(char: Char, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.charTyped(char, modifiersSet) }) {
return true
}
return super.charTyped(char, modifiers)
}
private fun findResponder(fn: (Responder) -> Boolean): Boolean {
var responder = windows.lastOrNull()?.firstResponder
while (responder != null) {
if (fn(responder)) {
return true
}
responder = responder.nextResponder
}
return false
}
}

View File

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

View File

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

View File

@ -0,0 +1,100 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.window.Window
/**
* A responder is an object which participates in the responder chain, a mechanism for propagating indirect events (i.e.
* events for which there is no on-screen position to determine the targeted view, such as key presses) through the
* view hierarchy.
*
* @author shadowfacts
*/
interface Responder {
/**
* The window that this responder is part of.
*/
val window: Window?
/**
* Whether this responder is the first responder of its window.
*
* `false` if [window] is null.
*
* @see Window.firstResponder
*/
val isFirstResponder: Boolean
get() = window?.firstResponder === this
/**
* The next responder in the chain after this. The next responder will receive an event if this responder did not
* accept it.
*
* The next responder may be `null` if this responder is at the end of the chain.
*/
val nextResponder: Responder?
/**
* Makes this responder become the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun becomeFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot become first responder while not in Window")
}
window!!.firstResponder = this
}
/**
* Called immediately after this responder has become the window's first responder.
* @see Window.firstResponder
*/
fun didBecomeFirstResponder() {}
/**
* Removes this object as the window's first responder.
* @throws RuntimeException if [window] is null
* @see Window.firstResponder
*/
fun resignFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot resign first responder while not in Window")
}
window!!.firstResponder = null
}
/**
* Called immediately before this object is removed as the window's first responder.
* @see Window.firstResponder
*/
fun didResignFirstResponder() {}
/**
* Called when a character has been typed.
*
* @param char The character that was typed.
* @param modifiers The key modifiers that were held down when the character was typed.
* @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder.
*/
fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
return false
}
/**
* Called when a keyboard key is pressed.
*
* If the pressed key is a typed character, [charTyped] will also be called. The order in which the methods are
* invoked is undefined and should not be relied upon.
*
* @param keyCode The integer code of the key that was pressed.
* @param modifiers The key modifiers that were held down when the character was typed.
* @return Whether this responder accepted the event. If `true`, it will not be passed to the next responder.
* @see org.lwjgl.glfw.GLFW for key code constants
*/
fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return false
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,41 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for defining rectangles. Provides helper values for calculating perpendicular components of a rectangle based on X/Y/W/H.
*
* @author shadowfacts
*/
data class Rect(val left: Double, val top: Double, val width: Double, val height: Double) {
constructor(origin: Point, size: Size): this(origin.x, origin.y, size.width, size.height)
val right: Double by lazy {
left + width
}
val bottom: Double by lazy {
top + height
}
val midX: Double by lazy {
left + width / 2
}
val midY: Double by lazy {
top + height / 2
}
val origin: Point by lazy {
Point(left, top)
}
val center: Point by lazy {
Point(midX, midY)
}
val size: Size by lazy {
Size(width, height)
}
operator fun contains(point: Point): Boolean {
return point.x in left..right && point.y in top..bottom
}
}

View File

@ -0,0 +1,8 @@
package net.shadowfacts.cacao.geometry
/**
* Helper class for specifying the size of objects.
*
* @author shadowfacts
*/
data class Size(val width: Double, val height: Double)

View File

@ -0,0 +1,36 @@
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)
val TEXT = Color(0x404040)
}
}

View File

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

View File

@ -0,0 +1,35 @@
package net.shadowfacts.cacao.util
import org.lwjgl.glfw.GLFW
/**
* A set of modifier keys that are being pressed at given instant.
*
* @author shadowfacts
* @param value The integer representation of the pressed modifiers as received from GLFW.
*/
class KeyModifiers(val value: Int) {
val shift: Boolean
get() = this[GLFW.GLFW_MOD_SHIFT]
val control: Boolean
get() = this[GLFW.GLFW_MOD_CONTROL]
val alt: Boolean
get() = this[GLFW.GLFW_MOD_ALT]
val command: Boolean
get() = this[GLFW.GLFW_MOD_SUPER]
val capsLock: Boolean
get() = this[GLFW.GLFW_MOD_CAPS_LOCK]
val numLock: Boolean
get() = this[GLFW.GLFW_MOD_NUM_LOCK]
private operator fun get(mod: Int): Boolean {
return (value and mod) == mod
}
}

View File

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

View File

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

View File

@ -0,0 +1,216 @@
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.minecraft.text.OrderedText
import net.minecraft.text.Text
import net.minecraft.util.math.Matrix4f
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: DrawableHelper() {
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
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(matrixStack: MatrixStack, rect: Rect, texture: Texture) {
if (disabled) return
color(1f, 1f, 1f, 1f)
MinecraftClient.getInstance().textureManager.bindTexture(texture.location)
draw(matrixStack, 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(matrixStack: MatrixStack, 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(matrixStack.peek().model, x, x + width, y, y + height, 0.0, uStart, uEnd, vStart, vEnd)
}
// Copied from net.minecraft.client.gui.DrawableHelper
private fun drawTexturedQuad(matrix: Matrix4f, x0: Double, x1: Double, y0: Double, y1: Double, z: Double, u0: Float, u1: Float, v0: Float, v1: Float) {
val bufferBuilder = Tessellator.getInstance().buffer
bufferBuilder.begin(GL11.GL_QUADS, VertexFormats.POSITION_TEXTURE)
bufferBuilder.vertex(matrix, x0.toFloat(), y1.toFloat(), z.toFloat()).texture(u0, v1).next()
bufferBuilder.vertex(matrix, x1.toFloat(), y1.toFloat(), z.toFloat()).texture(u1, v1).next()
bufferBuilder.vertex(matrix, x1.toFloat(), y0.toFloat(), z.toFloat()).texture(u1, v0).next()
bufferBuilder.vertex(matrix, x0.toFloat(), y0.toFloat(), z.toFloat()).texture(u0, v0).next()
bufferBuilder.end()
RenderSystem.enableAlphaTest()
BufferRenderer.draw(bufferBuilder)
}
fun drawTooltip(matrixStack: MatrixStack, text: Text, mouse: Point) {
drawTooltip(matrixStack, listOf(text.asOrderedText()), mouse)
}
fun drawTooltip(matrixStack: MatrixStack, texts: List<Text>, mouse: Point) {
drawTooltip(matrixStack, texts.map(Text::asOrderedText), mouse)
}
// Based on Screen.renderOrderedTooltip
@JvmName("drawOrderedTooltip")
fun drawTooltip(matrixStack: MatrixStack, texts: List<OrderedText>, mouse: Point) {
if (disabled) return
if (texts.isEmpty()) return
val client = MinecraftClient.getInstance()
val textRenderer = client.textRenderer
val maxWidth = texts.maxOf(textRenderer::getWidth)
var x = mouse.x.toInt() + 12
var y = mouse.y.toInt() - 12
var p = 8
if (texts.size > 1) {
p += 2 + (texts.size - 1) * 8
}
if (x + maxWidth > client.window.scaledWidth) {
x -= 28 + maxWidth
}
if (y + p + 6 > client.window.scaledHeight) {
y = client.window.scaledHeight - p - 6
}
matrixStack.push()
val q = -267386864
val r = 1347420415
val s = 1344798847
val t = 1
val tessellator = Tessellator.getInstance()
val buffer = tessellator.buffer
buffer.begin(GL11.GL_QUADS, VertexFormats.POSITION_COLOR)
val matrix = matrixStack.peek().model
val z = 400
fillGradient(matrix, buffer, x - 3, y - 4, x + maxWidth + 3, y - 3, z, q, q)
fillGradient(matrix, buffer, x - 3, y + p + 3, x + maxWidth + 3, y + p + 4, z, q, q)
fillGradient(matrix, buffer, x - 3, y - 3, x + maxWidth + 3, y + p + 3, z, q, q)
fillGradient(matrix, buffer, x - 4, y - 3, x - 3, y + p + 3, z, q, q)
fillGradient(matrix, buffer, x + maxWidth + 3, y - 3, x + maxWidth + 4, y + p + 3, z, q, q)
fillGradient(matrix, buffer, x - 3, y - 3 + 1, x - 3 + 1, y + p + 3 - 1, z, r, s)
fillGradient(matrix, buffer, x + maxWidth + 2, y - 3 + 1, x + maxWidth + 3, y + p + 3 - 1, z, r, s)
fillGradient(matrix, buffer, x - 3, y - 3, x + maxWidth + 3, y - 3 + 1, z, r, r)
fillGradient(matrix, buffer, x - 3, y + p + 2, x + maxWidth + 3, y + p + 3, z, s, s)
RenderSystem.enableDepthTest()
RenderSystem.disableTexture()
RenderSystem.enableBlend()
RenderSystem.defaultBlendFunc()
RenderSystem.shadeModel(7425)
buffer.end()
BufferRenderer.draw(buffer)
RenderSystem.shadeModel(7424)
RenderSystem.disableBlend()
RenderSystem.enableTexture()
val immediate = VertexConsumerProvider.immediate(buffer)
matrixStack.translate(0.0, 0.0, 400.0)
for (i in texts.indices) {
val text = texts[i]
textRenderer.draw(text, x.toFloat(), y.toFloat(), -1, true, matrix, immediate, false, 0, 15728880)
if (i == 0) {
y += 2
}
y += 10
}
immediate.draw()
matrixStack.pop()
}
/**
* @see org.lwjgl.opengl.GL11.glPushMatrix
*/
fun pushMatrix() {
if (disabled) return
RenderSystem.pushMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glPopMatrix
*/
fun popMatrix() {
if (disabled) return
RenderSystem.popMatrix()
}
/**
* @see org.lwjgl.opengl.GL11.glTranslated
*/
fun translate(x: Double, y: Double, z: Double = 0.0) {
if (disabled) return
RenderSystem.translated(x, y, z)
}
/**
* @see org.lwjgl.opengl.GL11.glScaled
*/
fun scale(x: Double, y: Double, z: Double = 1.0) {
if (disabled) return
RenderSystem.scaled(x, y, z)
}
/**
* @see org.lwjgl.opengl.GL11.glColor4f
*/
fun color(r: Float, g: Float, b: Float, alpha: Float) {
if (disabled) return
RenderSystem.color4f(r, g, b, alpha)
}
private fun VertexConsumer.color(color: Color): VertexConsumer {
return color(color.red, color.green, color.blue, color.alpha)
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,123 @@
package net.shadowfacts.cacao.view
import net.minecraft.client.MinecraftClient
import net.minecraft.client.font.TextRenderer
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.LiteralText
import net.minecraft.text.OrderedText
import net.minecraft.text.Text
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: Text,
val shadow: Boolean = false,
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
}
constructor(
text: String,
shadow: Boolean = false,
maxLines: Int = 0,
wrappingMode: WrappingMode = WrappingMode.WRAP,
textAlignment: TextAlignment = TextAlignment.LEFT,
): this(LiteralText(text), shadow, maxLines, wrappingMode, textAlignment)
/**
* The text of this label. Mutating this field will update the intrinsic content size and trigger a layout.
*/
var text: Text = text
set(value) {
field = value
updateIntrinsicContentSize()
// todo: setNeedsLayout instead of force unwrapping window
window!!.layout()
}
private lateinit var lines: List<OrderedText>
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 = textRenderer.wrapLines(text, bounds.width.toInt())
if (maxLines > 0 && maxLines < lines.size) {
lines = lines.dropLast(lines.size - maxLines)
}
this.lines = lines
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,454 @@
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.Responder
import net.shadowfacts.cacao.window.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.*
import kotlin.collections.HashSet
/**
* 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(): Responder {
/**
* 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.
*/
override var window: Window? = null
/**
* The next responder after this one.
* For views, the next responder is the view's superview.
*/
override val nextResponder: Responder?
// todo: should the view controller be a Responder?
get() = superview
private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) {
v.solver = it
}
}
/**
* The constraint solver used by the [Window] this view belongs to.
* Not initialized until [wasAdded] called, using it before that will throw a runtime exception.
*/
var solver: Solver by solverDelegate
/**
* 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
if (solverDelegate.isInitialized) {
view.solver = solver
}
view.window = window
view.wasAdded()
return view
}
/**
* Removes the given view from this view's children and removes all constraints that connect the subview or any of
* its children (recursively) to a view outside of the subview's hierarchy. Constraints internal to the subview's
* hierarchy (e.g., one between the subview and its child) will be left in place.
*
* @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")
}
_subviews.remove(view)
subviewsSortedByZIndex = subviews.sortedBy(View::zIndex)
view.superview = null
// we need to remove constraints for this subview that cross the boundary between the subview and ourself
val constraintsToRemove = solver.constraints.filter { constraint ->
val variables = constraint.getVariables().mapNotNull { it as? LayoutVariable }
for (a in 0 until variables.size - 1) {
for (b in a + 1 until variables.size) {
// if the variable views have no common ancestor after the removed view's superview is unset,
// the constraint crossed the this<->view boundary and should be removed
val ancestor = LowestCommonAncestor.find(variables[a].owner, variables[b].owner, View::superview)
if (ancestor == null) {
return@filter true
}
}
}
false
}
constraintsToRemove.forEach(solver::removeConstraint)
// 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 || !solverDelegate.isInitialized) return
if (old != null) {
solver.removeConstraint(intrinsicContentSizeWidthConstraint!!)
solver.removeConstraint(intrinsicContentSizeHeightConstraint!!)
}
if (new != null) {
solver.dsl {
this@View.intrinsicContentSizeWidthConstraint = (widthAnchor.equalTo(new.width, strength = MEDIUM))
this@View.intrinsicContentSizeHeightConstraint = (heightAnchor.equalTo(new.height, strength = MEDIUM))
}
}
}
/**
* Called after this view has been laid-out.
* If overridden, the super-class method must be called.
*/
open fun 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)
}
subviews.forEach(View::didLayout)
}
/**
* 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.
*
* The base implementation of this method forwards the click event to the first subview (sorted by [zIndex]) that
* contains the clicked point. Additionally, any subviews of this view that do not contain the clicked point receive
* the [mouseClickedOutside] event. If multiple views contain the point, any after one that returns `true` from this
* method will not receive the event or the click-outside event.
*
* 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 (inside, outside) = subviews.partition { point in it.frame }
val view = inside.maxByOrNull(View::zIndex)
var result = false
if (view != null) {
val pointInView = convert(point, to = view)
result = view.mouseClicked(pointInView, mouseButton)
}
for (v in outside) {
val pointInV = convert(point, to = v)
v.mouseClickedOutside(pointInV, mouseButton)
}
return result
}
/**
* Called when the mouse was clicked outside this view.
*
* The base implementation of this method simply forwards the event to all of this view's subviews.
*
* @param point The clicked point _in the coordinate space of this view_.
* @param mouseButton The mouse button used to click.
*/
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
for (view in subviews) {
val pointInView = convert(point, to = view)
view.mouseClickedOutside(pointInView, mouseButton)
}
}
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(startPoint).maxByOrNull(View::zIndex)
if (view != null) {
val startInView = convert(startPoint, to = view)
return view.mouseDragged(startInView, delta, mouseButton)
}
return false
}
/**
* Converts the given point in this view's coordinate system to the coordinate system of another view or the window.
*
* @param point The point to convert, in the coordinate system of this view.
* @param to The view to convert to. If `null`, it will be converted to the window's coordinate system.
* @return The point in the coordinate system of the [to] view.
*/
fun convert(point: Point, to: View?): Point {
if (to != null) {
val ancestor = LowestCommonAncestor.find(this, to, View::superview)
@Suppress("NAME_SHADOWING") var point = point
// Convert up to the LCA
var view: View? = this
while (view != null && view != ancestor) {
point = Point(point.x + view.frame.left, point.y + view.frame.top)
view = view.superview
}
// Convert back down to the other view
view = to
while (view != null && view != ancestor) {
point = Point(point.x - view.frame.left, point.y - view.frame.top)
view = view.superview
}
return point
} else {
return Point(leftAnchor.value + point.x, topAnchor.value + point.y)
}
}
/**
* Converts the given rectangle in this view's coordinate system to the coordinate system of another view or the window.
*
* @param rect The rectangle to convert, in the coordinate system of this view.
* @param to The view to convert to. If `null`, it will be converted to the window's coordinate system.
* @return The rectangle in the coordinate system of the [to] view.
*/
fun convert(rect: Rect, to: View?): Rect {
return Rect(convert(rect.origin, to), rect.size)
}
}

View File

@ -0,0 +1,123 @@
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
/**
* An 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.centerXAnchor equalTo centerXAnchor
content.centerYAnchor equalTo centerYAnchor
content.leftAnchor.lessThanOrEqualTo((leftAnchor + padding), WEAK)
content.rightAnchor.greaterThanOrEqualTo(rightAnchor - padding, WEAK)
content.topAnchor.lessThanOrEqualTo(topAnchor + padding, WEAK)
content.bottomAnchor.greaterThanOrEqualTo(bottomAnchor - padding, WEAK)
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)
// don't need to convert mouse to background coordinate system
// the edges are all pinned, so the coordinate space is the same
getCurrentBackground(mouse)?.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
}
protected open fun getCurrentBackground(mouse: Point): View? {
return if (disabled) {
disabledBackground ?: background
} else if (mouse in bounds) {
hoveredBackground ?: background
} else {
background
}
}
}

View File

@ -0,0 +1,21 @@
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.
* @param handler The handler function to invoke when this button is pressed.
*/
class Button(
content: View,
padding: Double = 4.0,
handler: ((Button) -> Unit)? = null
): AbstractButton<Button>(content, padding) {
init {
this.handler = handler
}
}

View File

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

View File

@ -0,0 +1,51 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.text.Text
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 [Text] for the button's label.
*/
class EnumButton<E: Enum<E>>(
initialValue: E,
val localizer: (E) -> Text
): AbstractButton<EnumButton<E>>(
Label(localizer(initialValue), shadow = true)
) {
private val label: Label
get() = content as Label
/**
* The current value of the enum button.
* Updating this property will use the [localizer] to update the label.
*/
var value: E = initialValue
set(value) {
field = value
label.text = localizer(value)
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
value = when (mouseButton) {
MouseButton.LEFT -> EnumHelper.next(value)
MouseButton.RIGHT -> EnumHelper.previous(value)
else -> value
}
}
return super.mouseClicked(point, mouseButton)
}
}

View File

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

View File

@ -0,0 +1,163 @@
package net.shadowfacts.cacao.view.textfield
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.LiteralText
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.view.View
import net.shadowfacts.phycon.mixin.client.TextFieldWidgetAccessor
/**
* An abstract text field class. Cannot be constructed directly, use for creating other text fields with more specific
* behavior. Use [TextField] for a plain text field.
*
* @author shadowfacts
* @param Impl The type of the concrete implementation of the text field. Used to allow the [handler] to receive the
* the exact type of text field.
* @param initialText The initial value of the text field.
*/
abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
initialText: String
): View() {
/**
* A function that is invoked when the text in this text field changes.
*/
var handler: ((Impl) -> Unit)? = null
/**
* Whether the text field is disabled.
* Disabled text fields cannot be interacted with.
*/
var disabled = false
/**
* Whether this text field is focused (i.e. [isFirstResponder]) and receives key events.
*/
val focused: Boolean
get() = isFirstResponder
/**
* The current text of this text field.
*/
var text: String
get() = minecraftWidget.text
set(value) {
minecraftWidget.text = value
}
/**
* The maximum length of text that this text field can hold.
*
* Defaults to the Minecraft text field's maximum length (currently 32, subject to change).
*/
var maxLength: Int
get() = (minecraftWidget as TextFieldWidgetAccessor).cacao_getMaxLength()
set(value) {
minecraftWidget.setMaxLength(value)
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
}
/**
* A function used by subclasses to determine whether a proposed value is acceptable for this field.
*/
abstract fun validate(proposedText: String): Boolean
override fun didLayout() {
super.didLayout()
originInWindow = convert(bounds.origin, to = null)
// offset View dimensions by 1 on each side because TextFieldWidget draws the border outside its dimensions
minecraftWidget.x = originInWindow.x.toInt() + 1
minecraftWidget.y = originInWindow.y.toInt() + 1
minecraftWidget.width = bounds.width.toInt() - 2
minecraftWidget.height = bounds.height.toInt() - 2
// after dimensions change call setText on the widget to make sure its internal scroll position is up-to-date
minecraftWidget.text = text
}
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
RenderHelper.pushMatrix()
RenderHelper.translate(-originInWindow.x, -originInWindow.y)
val mouseXInWindow = (mouse.x + originInWindow.x).toInt()
val mouseYInWindow = (mouse.y + originInWindow.y).toInt()
minecraftWidget.render(matrixStack, mouseXInWindow, mouseYInWindow, delta)
RenderHelper.popMatrix()
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (!disabled) {
if (focused) {
val mouseXInWindow = (point.x + originInWindow.x)
val mouseYInWindow = (point.y + originInWindow.y)
minecraftWidget.mouseClicked(mouseXInWindow, mouseYInWindow, mouseButton.ordinal)
} else {
becomeFirstResponder()
}
}
// don't play sound when interacting with text field
return false
}
override fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
if (focused) {
resignFirstResponder()
}
}
override fun didBecomeFirstResponder() {
super.didBecomeFirstResponder()
minecraftWidget.setSelected(true)
}
override fun didResignFirstResponder() {
super.didResignFirstResponder()
minecraftWidget.setSelected(false)
}
override fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
val oldText = text
val result = minecraftWidget.charTyped(char, modifiers.value)
if (text != oldText) {
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result
}
override fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
val oldText = text
// scanCode isn't used by TextFieldWidget, hopefully this doesn't break :/
val result = minecraftWidget.keyPressed(keyCode, -1, modifiers.value)
if (text != oldText) {
@Suppress("UNCHECKED_CAST")
handler?.invoke(this as Impl)
}
return result
}
// todo: label for the TextFieldWidget?
private class ProxyWidget: TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
// AbstractButtonWidget.height is protected
fun setHeight(height: Int) {
this.height = height
}
}
}

View File

@ -0,0 +1,21 @@
package net.shadowfacts.cacao.view.textfield
/**
* A simple, no-frills text field. This text field accepts any value.
*
* @author shadowfacts
* @param initialText The initial value of this text field.
* @param handler A function that is invoked when the value of the text field changes.
*/
class TextField(
initialText: String,
handler: ((TextField) -> Unit)? = null
): AbstractTextField<TextField>(initialText) {
init {
this.handler = handler
}
override fun validate(proposedText: String): Boolean {
return true
}
}

View File

@ -0,0 +1,237 @@
package net.shadowfacts.cacao.viewcontroller
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.geometry.Axis
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.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.view.button.AbstractButton
import net.shadowfacts.kiwidsl.dsl
import java.lang.RuntimeException
/**
* A tab view controller is divided into two sections: a tab bar at the top, and a content view at the bottom.
*
* The tab bar contains a tab button for each of the tabs in the VC and the content view contains the view of the
* active tab's view controller.
*
* The active tab's view controller is also added as a child of the tab view controller.
*
* @author shadowfacts
* @param T The type of the tab objects this view controller uses.
* @param tabs The list of tabs in this controller.
* @param initialTab The tab that is initially selected when the controller is first created.
* @param onTabChange A function invoked immediately after the active tab has changed (and the content view has been
* updated).
*/
class TabViewController<T: TabViewController.Tab>(
val tabs: List<T>,
initialTab: T = tabs.first(),
val onTabChange: ((T) -> Unit)? = null
): ViewController() {
/**
* The Tab interface defines the requirements for tab objects that can be used with this view controller.
*
* This is an interface to allow for tab objects to carry additional data. A simple implementation is provided.
* @see SimpleTab
*/
interface Tab {
/**
* The view displayed on the button for this tab.
*/
val tabView: View
/**
* The tooltip displayed when the button for this tab is hovered. `null` if no tooltip should be shown.
*/
val tooltip: Text?
/**
* The view controller used as content when this tab is active. When switching tabs, the returned content VC
* may be reused or created from scratch each time.
*/
val controller: ViewController
}
/**
* A simple [Tab] implementation that provides the minimum necessary information.
* @param tabView The view to display on the tab's button.
* @param tooltip The tooltip to display when the tab's button is hovered (or `null`, if none).
* @param controller The content view controller for this tab.
*/
class SimpleTab(
override val tabView: View,
override val tooltip: Text? = null,
override val controller: ViewController,
): Tab
/**
* The currently selected tab.
*/
var currentTab: T = initialTab
private set
private lateinit var tabButtons: List<TabButton<T>>
private lateinit var outerStack: StackView
private lateinit var tabStack: StackView
// todo: this shouldn't be public, use layout guides
lateinit var tabVCContainer: View
private set
override fun viewDidLoad() {
super.viewDidLoad()
// todo: might be simpler to just not use a stack view
// padding is -4 so tab button texture overlaps with panel BG as expected
outerStack = StackView(Axis.VERTICAL, StackView.Distribution.FILL, -4.0)
view.addSubview(outerStack)
tabStack = StackView(Axis.HORIZONTAL, StackView.Distribution.FILL)
tabStack.zIndex = 1.0
outerStack.addArrangedSubview(tabStack)
tabButtons = tabs.mapIndexed { index, tab ->
val btn = TabButton(tab)
btn.handler = { selectTab(it.tab) }
if (tab == currentTab) {
btn.setSelected(true)
}
btn
}
// todo: batch calls to addArrangedSubview
tabButtons.forEach(tabStack::addArrangedSubview)
// spacer
tabStack.addArrangedSubview(View())
val background = NinePatchView(NinePatchTexture.PANEL_BG)
outerStack.addArrangedSubview(background)
tabVCContainer = View()
tabVCContainer.zIndex = 1.0
view.addSubview(tabVCContainer)
embedChild(currentTab.controller, tabVCContainer)
view.solver.dsl {
outerStack.leftAnchor equalTo view.leftAnchor
outerStack.rightAnchor equalTo view.rightAnchor
outerStack.topAnchor equalTo view.topAnchor
outerStack.bottomAnchor equalTo view.bottomAnchor
tabVCContainer.leftAnchor equalTo (background.leftAnchor + 6)
tabVCContainer.rightAnchor equalTo (background.rightAnchor - 6)
tabVCContainer.topAnchor equalTo (background.topAnchor + 6)
tabVCContainer.bottomAnchor equalTo (background.bottomAnchor - 6)
}
}
/**
* Sets the provided tab as the currently active tab for this controller. Updates the state of tab bar buttons and
* swaps the content view controller.
*
* After the tab and the content are changed, [onTabChange] is invoked.
*
* @throws RuntimeException If the provided tab was not passed in as part of the [tabs] list.
*/
fun selectTab(tab: T) {
if (!tabs.contains(tab)) {
throw RuntimeException("Cannot activate tab not in TabViewController.tabs")
}
val oldTab = currentTab
currentTab = tab
tabButtons.forEach {
it.setSelected(it.tab === tab)
}
oldTab.controller.removeFromParent()
embedChild(currentTab.controller, tabVCContainer)
onTabChange?.invoke(currentTab)
// todo: setNeedsLayout
window!!.layout()
}
private class TabButton<T: Tab>(
val tab: T,
): AbstractButton<TabButton<T>>(
tab.tabView,
padding = 2.0
) {
companion object {
val BACKGROUND = Identifier("textures/gui/container/creative_inventory/tabs.png")
}
private var selected = false
private var backgroundView = TextureView(Texture(BACKGROUND, 0, 0))
init {
intrinsicContentSize = Size(28.0, 32.0)
background = null
hoveredBackground = null
disabledBackground = null
}
override fun wasAdded() {
super.wasAdded()
backgroundView.usesConstraintBasedLayout = false
backgroundView.frame = Rect(0.0, 0.0, 28.0, 32.0)
backgroundView.zIndex = -1.0
addSubview(backgroundView)
solver.dsl {
content.bottomAnchor lessThanOrEqualTo (bottomAnchor - 4)
}
}
override fun didLayout() {
super.didLayout()
updateBackgroundTexture()
}
fun setSelected(selected: Boolean) {
this.selected = selected
updateBackgroundTexture()
}
override fun getCurrentBackground(mouse: Point) = backgroundView
override fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
super.draw(matrixStack, mouse, delta)
if (mouse in bounds && tab.tooltip != null) {
window!!.drawTooltip(listOf(tab.tooltip!!))
}
}
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
if (selected) return false
else return super.mouseClicked(point, mouseButton)
}
private fun updateBackgroundTexture() {
val v = if (selected) 32 else 0
val u = when {
superview == null -> 0
frame.left == 0.0 -> 0
frame.right == superview!!.bounds.right -> 140
else -> 28
}
backgroundView.texture = Texture(BACKGROUND, u, v)
backgroundView.frame = Rect(0.0, 0.0, 28.0, if (selected) 32.0 else 28.0)
}
}
}

View File

@ -0,0 +1,215 @@
package net.shadowfacts.cacao.viewcontroller
import net.shadowfacts.cacao.window.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
set(value) {
field = value
for (vc in children) {
vc.window = value
}
}
/**
* 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()
}
/**
* If the view for this controller has already been loaded.
*/
val isViewLoaded: Boolean
get() = ::view.isInitialized
/**
* Calls [loadView] to load this controller's view only if it has not already been loaded.
*/
fun loadViewIfNeeded() {
if (!isViewLoaded) {
loadView()
}
}
/**
* 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)
val wasViewLoaded = viewController.isViewLoaded
viewController.loadViewIfNeeded()
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
}
}
if (!wasViewLoaded) {
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()
}
}

View File

@ -0,0 +1,14 @@
package net.shadowfacts.cacao.window
import net.minecraft.screen.ScreenHandler
import net.shadowfacts.cacao.viewcontroller.ViewController
/**
* @author shadowfacts
*/
class ScreenHandlerWindow(
val screenHandler: ScreenHandler,
viewController: ViewController
): Window(viewController) {
}

View File

@ -0,0 +1,263 @@
package net.shadowfacts.cacao.window
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
import net.shadowfacts.cacao.AbstractCacaoScreen
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Responder
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
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
import java.lang.RuntimeException
/**
* 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.
*/
open 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: AbstractCacaoScreen
/**
* 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")
/**
* The first responder of the a window is the first object that receives indirect events (e.g., keypresses).
*
* When an indirect event is received by the window, it is given to the first responder. If the first responder does
* not accept it (i.e. returns `false` from the appropriate method), the event will be passed to that responder's
* [Responder.nextResponder], and so on.
*
* The following is the order of events when setting this property:
* 1. The old first responder (if any) has [Responder.didResignFirstResponder] invoked.
* 2. The value of the field is updated.
* 3. The new value (if any) has [Responder.didBecomeFirstResponder] invoked.
*/
var firstResponder: Responder? = null
set(value) {
field?.didResignFirstResponder()
field = value
field?.didBecomeFirstResponder()
}
// 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
private var currentDeferredTooltip: List<Text>? = null
init {
createInternalConstraints()
}
fun wasAdded() {
viewController.window = this
viewController.loadViewIfNeeded()
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.
*/
open fun draw(matrixStack: MatrixStack, mouse: Point, delta: Float) {
currentDeferredTooltip = null
val mouseInView = Point(mouse.x - viewController.view.frame.left, mouse.y - viewController.view.frame.top)
viewController.view.draw(matrixStack, mouseInView, delta)
if (currentDeferredTooltip != null) {
RenderHelper.drawTooltip(matrixStack, currentDeferredTooltip!!, mouse)
}
}
/**
* Draw a tooltip containing the given lines at the mouse pointer location.
*
* Implementation note: the tooltip is not drawn immediately, it is done after the window is done drawing all of its
* views. This is done to prevent other views from being drawn in front of the tooltip. Additionally, more than one
* tooltip cannot be drawn in a frame as they would appear at the same position.
*/
fun drawTooltip(text: List<Text>) {
if (currentDeferredTooltip != null) {
throw RuntimeException("Deferred tooltip already registered for current frame")
}
currentDeferredTooltip = text
}
/**
* 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 {
// todo: isn't this always true?
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).maxByOrNull(View::zIndex)
while (view != null && !view.respondsToDragging) {
prevView = view
val pointInView = viewController.view.convert(startInView, to = view)
view = view.subviewsAtPoint(pointInView).maxByOrNull(View::zIndex)
}
this.currentDragReceiver = view ?: prevView
return this.currentDragReceiver?.mouseDragged(startPoint, delta, mouseButton) ?: false
}
return false
}
fun mouseReleased(point: Point, mouseButton: MouseButton): Boolean {
val currentlyDraggedView = this.currentDragReceiver
if (currentlyDraggedView != null) {
this.currentDragReceiver = null
return true
}
return false
}
}

View File

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

View File

@ -8,11 +8,7 @@ import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.network.DeviceBlock
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.network.component.ActivationController
import net.shadowfacts.phycon.screen.ActivatableDeviceConsoleScreen
import net.shadowfacts.phycon.screen.DeviceConsoleScreen
import net.shadowfacts.phycon.screen.RedstoneControllerConsoleScreen
import net.shadowfacts.phycon.screen.console.DeviceConsoleScreen
/**
* @author shadowfacts
@ -40,11 +36,8 @@ class ConsoleItem: Item(Settings()) {
}
private fun openScreen(be: DeviceBlockEntity) {
val screen = when (be) {
is ActivationController.ActivatableDevice -> ActivatableDeviceConsoleScreen(be)
is RedstoneControllerBlockEntity -> RedstoneControllerConsoleScreen(be)
else -> DeviceConsoleScreen(be)
}
// val screen = TestCacaoScreen()
val screen = DeviceConsoleScreen(be)
MinecraftClient.getInstance().openScreen(screen)
}

View File

@ -1,64 +0,0 @@
package net.shadowfacts.phycon.screen
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.util.math.MatrixStack
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.component.ActivationController
import net.shadowfacts.phycon.networking.C2SConfigureActivationMode
import net.shadowfacts.phycon.util.ActivationMode
import net.shadowfacts.phycon.util.next
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class ActivatableDeviceConsoleScreen<T>(
val device: T
): Screen(device.cachedState.block.name) where T: DeviceBlockEntity, T: ActivationController.ActivatableDevice {
private val backgroundWidth = 252
private val backgroundHeight = 222
override fun init() {
super.init()
buttons.clear()
val minX = (width - backgroundWidth) / 2
val minY = (height - backgroundHeight) / 2
val mode = EnumButton(device.controller::activationMode, minX + 5, minY + 25, 55, 20) {
client!!.player!!.networkHandler.sendPacket(C2SConfigureActivationMode(device))
}
addButton(mode)
}
override fun isPauseScreen() = false
override fun keyPressed(key: Int, j: Int, k: Int): Boolean {
if (key == GLFW.GLFW_KEY_E) {
onClose()
return true
}
return super.keyPressed(key, j, k)
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrixStack)
val minX = (width - backgroundWidth) / 2
val minY = (height - backgroundHeight) / 2
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(DeviceConsoleScreen.BACKGROUND)
drawTexture(matrixStack, minX, minY, 0, 0, backgroundWidth, backgroundHeight)
super.render(matrixStack, mouseX, mouseY, delta)
textRenderer.draw(matrixStack, "IP Address: ${device.ipAddress}", minX + 5f, minY + 5f, 0x404040)
textRenderer.draw(matrixStack, "MAC Address: ${device.macAddress}", minX + 5f, minY + 15f, 0x404040)
}
}

View File

@ -1,40 +0,0 @@
package net.shadowfacts.phycon.screen
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.TranslatableText
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.network.DeviceBlockEntity
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class DeviceConsoleScreen(
val device: DeviceBlockEntity,
): Screen(TranslatableText("item.phycon.console")) {
companion object {
val BACKGROUND = Identifier(PhysicalConnectivity.MODID, "textures/gui/console.png")
}
override fun isPauseScreen() = false
override fun keyPressed(key: Int, j: Int, k: Int): Boolean {
if (key == GLFW.GLFW_KEY_E) {
onClose()
return true
}
return super.keyPressed(key, j, k)
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrixStack)
super.render(matrixStack, mouseX, mouseY, delta)
drawCenteredString(matrixStack, textRenderer, device.macAddress.toString(), width / 2, height / 2 - 5, 0xffffff)
drawCenteredString(matrixStack, textRenderer, device.ipAddress.toString(), width / 2, height / 2 + 5, 0xffffff)
}
}

View File

@ -1,96 +0,0 @@
package net.shadowfacts.phycon.screen
import com.mojang.blaze3d.systems.RenderSystem
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.widget.ButtonWidget
import net.minecraft.client.gui.widget.TextFieldWidget
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.LiteralText
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.networking.C2SConfigureRedstoneController
import net.shadowfacts.phycon.util.next
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class RedstoneControllerConsoleScreen(
val device: RedstoneControllerBlockEntity
): Screen(device.cachedState.block.name) {
private val backgroundWidth = 252
private val backgroundHeight = 222
private val ipAddressTextFields = mutableListOf<TextFieldWidget>()
override fun init() {
super.init()
buttons.clear()
ipAddressTextFields.clear()
val minX = (width - backgroundWidth) / 2
val minY = (height - backgroundHeight) / 2
val mode = EnumButton(device::redstoneMode, minX + 5, minY + 25, 75, 20) {
client!!.player!!.networkHandler.sendPacket(C2SConfigureRedstoneController(device))
}
addButton(mode)
for (i in 0 until 5) {
// todo: field name
val field = TextFieldWidget(textRenderer, minX + 5, minY + 50 + 22 * i, backgroundWidth / 2, 20, LiteralText(""))
field.setMaxLength(15)
field.setHasBorder(true)
field.isVisible = true
field.setEditableColor(0xffffff)
field.text = device.managedDevices[i]?.toString()
field.setChangedListener { newVal ->
device.managedDevices[i] = IPAddress.parse(newVal)
client!!.player!!.networkHandler.sendPacket(C2SConfigureRedstoneController(device))
}
addChild(field)
ipAddressTextFields.add(field)
}
}
override fun isPauseScreen() = false
override fun keyPressed(key: Int, j: Int, k: Int): Boolean {
if (key == GLFW.GLFW_KEY_E) {
onClose()
return true
}
return super.keyPressed(key, j, k)
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
val clickedField = ipAddressTextFields.find { it.x <= mouseX && it.x + it.width >= mouseX && it.y <= mouseY && it.y + it.height >= mouseY }
if (clickedField != null) {
ipAddressTextFields.forEach {
if (it !== clickedField) it.setSelected(false)
}
}
return super.mouseClicked(mouseX, mouseY, button)
}
override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
renderBackground(matrixStack)
val minX = (width - backgroundWidth) / 2
val minY = (height - backgroundHeight) / 2
RenderSystem.color4f(1f, 1f, 1f, 1f)
client!!.textureManager.bindTexture(DeviceConsoleScreen.BACKGROUND)
drawTexture(matrixStack, minX, minY, 0, 0, backgroundWidth, backgroundHeight)
super.render(matrixStack, mouseX, mouseY, delta)
ipAddressTextFields.forEach { it.render(matrixStack, mouseX, mouseY, delta) }
textRenderer.draw(matrixStack, "IP Address: ${device.ipAddress}", minX + 5f, minY + 5f, 0x404040)
textRenderer.draw(matrixStack, "MAC Address: ${device.macAddress}", minX + 5f, minY + 15f, 0x404040)
}
}

View File

@ -0,0 +1,135 @@
package net.shadowfacts.phycon.screen
import net.minecraft.client.MinecraftClient
import net.minecraft.text.LiteralText
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.window.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.*
import net.shadowfacts.cacao.view.button.Button
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.TabViewController
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
// }
//
// val label = Label(LiteralText("Test"), wrappingMode = Label.WrappingMode.NO_WRAP).apply {
//// textColor = Color.BLACK
// }
//// stack.addArrangedSubview(label)
// val button = red.addSubview(Button(label))
//
// val field = TextField("Test") {
// println("new value: ${it.text}")
// }
// stack.addArrangedSubview(field)
//
// view.solver.dsl {
// stack.topAnchor equalTo 0
// stack.centerXAnchor equalTo window!!.centerXAnchor
// stack.widthAnchor equalTo 100
//
//
// button.centerXAnchor equalTo red.centerXAnchor
// button.centerYAnchor equalTo red.centerYAnchor
//// label.heightAnchor equalTo 9
// button.heightAnchor equalTo 20
//
// field.widthAnchor equalTo stack.widthAnchor
// field.heightAnchor equalTo 20
// }
//
// }
// }
// addWindow(Window(viewController))
val viewController = object: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val tabs = listOf(
Tab(Label("A"), AViewController(), LiteralText("Tab A")),
Tab(Label("B"), BViewController(), LiteralText("Tab B")),
)
val tabVC = TabViewController(tabs)
embedChild(tabVC, pinEdges = false)
view.solver.dsl {
tabVC.view.centerXAnchor equalTo view.centerXAnchor
tabVC.view.centerYAnchor equalTo view.centerYAnchor
tabVC.view.widthAnchor equalTo 200
tabVC.view.heightAnchor equalTo 150
}
}
}
addWindow(Window(viewController))
}
data class Tab(
override val tabView: View,
override val controller: ViewController,
override val tooltip: Text? = null
): TabViewController.Tab
class AViewController: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val button = Button(Label("A content")) {
println("A pressed")
}
view.addSubview(button)
view.solver.dsl {
button.centerXAnchor equalTo view.centerXAnchor
button.centerYAnchor equalTo view.centerYAnchor
}
}
}
class BViewController: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val button = Button(Label("B content")) {
println("B pressed")
}
view.addSubview(button)
view.solver.dsl {
button.centerXAnchor equalTo view.centerXAnchor
button.centerYAnchor equalTo view.centerYAnchor
}
}
}
}

View File

@ -0,0 +1,50 @@
package net.shadowfacts.phycon.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.button.EnumButton
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.component.ActivationController
import net.shadowfacts.phycon.networking.C2SConfigureActivationMode
import net.shadowfacts.phycon.util.ActivationMode
/**
* @author shadowfacts
*/
class ActivatableDeviceViewController<T>(
val device: T,
): ViewController() where T: DeviceBlockEntity, T: ActivationController.ActivatableDevice {
override fun viewDidLoad() {
super.viewDidLoad()
val label = Label(TranslatableText("gui.phycon.console.remote.mode")).apply {
textColor = Color.TEXT
}
view.addSubview(label)
val mode = EnumButton(device.controller.activationMode, ActivationMode::friendlyName)
mode.handler = {
device.controller.activationMode = it.value
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureActivationMode(device))
}
view.addSubview(mode)
view.solver.dsl {
mode.widthAnchor equalTo 100
mode.heightAnchor equalTo 20
mode.topAnchor equalTo view.topAnchor
mode.rightAnchor equalTo view.rightAnchor
label.centerYAnchor equalTo mode.centerYAnchor
label.rightAnchor equalTo (mode.leftAnchor - 4)
}
}
}

View File

@ -0,0 +1,84 @@
package net.shadowfacts.phycon.screen.console
import net.minecraft.text.TranslatableText
import net.minecraft.util.Identifier
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.util.texture.Texture
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.TextureView
import net.shadowfacts.cacao.viewcontroller.TabViewController
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.network.DeviceBlockEntity
import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.network.component.ActivationController
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class DeviceConsoleScreen(
val device: DeviceBlockEntity,
): CacaoScreen(TranslatableText("item.phycon.console")) {
private val tabController: TabViewController<TabViewController.SimpleTab>
init {
val tabs = mutableListOf(
TabViewController.SimpleTab(
Label("IP").apply { textColor = Color.TEXT },
TranslatableText("gui.phycon.console.details"),
DeviceDetailsViewController(device)
)
)
if (device is ActivationController.ActivatableDevice) {
tabs.add(TabViewController.SimpleTab(
TextureView(Texture(Identifier("textures/item/ender_pearl.png"), 0, 0, 16, 16)).apply {
intrinsicContentSize = Size(16.0, 16.0)
},
TranslatableText("gui.phycon.console.remote"),
ActivatableDeviceViewController(device)
))
}
if (device is RedstoneControllerBlockEntity) {
tabs.add(TabViewController.SimpleTab(
TextureView(Texture(Identifier("textures/block/redstone_torch.png"), 0, 0, 16, 16)).apply {
intrinsicContentSize = Size(16.0, 16.0)
},
TranslatableText("block.phycon.redstone_controller"),
RedstoneControllerViewController(device)
))
}
tabController = TabViewController(tabs)
val root = object: ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
embedChild(tabController, pinEdges = false)
view.solver.dsl {
tabController.view.centerXAnchor equalTo view.centerXAnchor
tabController.view.centerYAnchor equalTo view.centerYAnchor
tabController.tabVCContainer.widthAnchor equalTo 200
tabController.tabVCContainer.heightAnchor equalTo 150
}
}
}
addWindow(Window(root))
}
override fun isPauseScreen() = false
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (keyCode == GLFW.GLFW_KEY_E) {
onClose()
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
}

View File

@ -0,0 +1,44 @@
package net.shadowfacts.phycon.screen.console
import net.minecraft.text.TranslatableText
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.network.DeviceBlockEntity
/**
* @author shadowfacts
*/
class DeviceDetailsViewController(val device: DeviceBlockEntity): ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val stack = StackView(Axis.VERTICAL, StackView.Distribution.LEADING, spacing = 4.0)
view.addSubview(stack)
val name = device.cachedState.block.name.styled { it.withBold(true) }
val deviceNameLabel = Label(name).apply {
textColor = Color.TEXT
}
stack.addArrangedSubview(deviceNameLabel)
val ipLabel = Label(TranslatableText("gui.phycon.console.details.ip", device.ipAddress.toString())).apply {
textColor = Color.TEXT
}
stack.addArrangedSubview(ipLabel)
val macLabel = Label(TranslatableText("gui.phycon.console.details.mac", device.macAddress.toString())).apply {
textColor = Color.TEXT
}
stack.addArrangedSubview(macLabel)
view.solver.dsl {
stack.leftAnchor equalTo view.leftAnchor
stack.topAnchor equalTo view.topAnchor
}
}
}

View File

@ -0,0 +1,73 @@
package net.shadowfacts.phycon.screen.console
import net.minecraft.client.MinecraftClient
import net.minecraft.text.TranslatableText
import net.shadowfacts.cacao.geometry.Axis
import net.shadowfacts.cacao.util.Color
import net.shadowfacts.cacao.view.Label
import net.shadowfacts.cacao.view.StackView
import net.shadowfacts.cacao.view.button.EnumButton
import net.shadowfacts.cacao.view.textfield.TextField
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.phycon.api.util.IPAddress
import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity
import net.shadowfacts.phycon.networking.C2SConfigureRedstoneController
import net.shadowfacts.phycon.util.RedstoneMode
/**
* @author shadowfacts
*/
class RedstoneControllerViewController(val device: RedstoneControllerBlockEntity): ViewController() {
override fun viewDidLoad() {
super.viewDidLoad()
val modeLabel = Label(TranslatableText("gui.phycon.console.redstone.mode")).apply {
textColor = Color.TEXT
}
view.addSubview(modeLabel)
val managedDevicesLabel = Label(TranslatableText("gui.phycon.console.redstone.devices")).apply {
textColor = Color.TEXT
}
view.addSubview(managedDevicesLabel)
val controls = StackView(Axis.VERTICAL, StackView.Distribution.FILL, spacing = 4.0)
view.addSubview(controls)
val mode = EnumButton(device.redstoneMode, RedstoneMode::friendlyName)
mode.handler = {
device.redstoneMode = it.value
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureRedstoneController(device))
}
controls.addArrangedSubview(mode)
val textFields = (0 until 5).map { i ->
TextField(device.managedDevices[i]?.toString() ?: "") {
device.managedDevices[i] = IPAddress.parse(it.text)
MinecraftClient.getInstance().player!!.networkHandler.sendPacket(C2SConfigureRedstoneController(device))
}
}
textFields.forEach(controls::addArrangedSubview)
view.solver.dsl {
controls.widthAnchor equalTo 100
controls.rightAnchor equalTo view.rightAnchor
controls.topAnchor equalTo view.topAnchor
mode.heightAnchor equalTo 20
textFields.forEach {
it.heightAnchor equalTo 20
}
modeLabel.centerYAnchor equalTo mode.centerYAnchor
modeLabel.rightAnchor equalTo (mode.leftAnchor - 4)
managedDevicesLabel.centerYAnchor equalTo textFields.first().centerYAnchor
managedDevicesLabel.rightAnchor equalTo (textFields.first().leftAnchor - 4)
}
}
}

View File

@ -11,6 +11,13 @@
"item.phycon.console": "Console",
"gui.phycon.terminal_buffer": "Buffer",
"gui.phycon.console.details": "Device Details",
"gui.phycon.console.details.ip": "IP Address: %s",
"gui.phycon.console.details.mac": "MAC Address: %s",
"gui.phycon.console.redstone.mode": "Redstone Mode",
"gui.phycon.console.redstone.devices": "Managed Devices",
"gui.phycon.console.remote": "Remote Management",
"gui.phycon.console.remote.mode": "Activation Mode",
"gui.phycon.redstone_mode.high": "High",
"gui.phycon.redstone_mode.low": "Low",
"gui.phycon.redstone_mode.toggle": "Toggle",

View File

@ -3,9 +3,10 @@
"package": "net.shadowfacts.phycon.mixin.client",
"compatibilityLevel": "JAVA_8",
"mixins": [
"MixinHandledScreen"
"MixinHandledScreen",
"TextFieldWidgetAccessor"
],
"injectors": {
"defaultRequire": 1
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,81 @@
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 testNestedParent() {
val parent = Node("parent", null)
val middle = Node("middle", parent)
val child = Node("child", middle)
assertEquals(parent, LowestCommonAncestor.find(parent, child, Node::parent))
}
@Test
fun testSiblings() {
val root = Node("root", null)
val a = Node("a", root)
val b = Node("b", root)
assertEquals(root, LowestCommonAncestor.find(a, b, Node::parent))
}
@Test
fun testBetweenSubtrees() {
// ┌────┐
// │root│
// └────┘
//
//
// ┌─┐ ┌─┐
// │A│ │B│
// └─┘ └─┘
//
//
// ┌─┐ ┌─┐┌─┐ ┌─┐
// │C│ │D││E│ │F│
// └─┘ └─┘└─┘ └─┘
val root = Node("root", null)
val a = Node("a", root)
val c = Node("c", a)
val d = Node("d", a)
val b = Node("b", root)
val e = Node("e", b)
val f = Node("f", b)
assertEquals(a, LowestCommonAncestor.find(c, d, Node::parent))
assertEquals(root, LowestCommonAncestor.find(c, b, Node::parent))
assertEquals(root, LowestCommonAncestor.find(d, e, Node::parent))
assertEquals(root, LowestCommonAncestor.find(c, root, Node::parent))
}
@Test
fun testBetweenDisjointTrees() {
val a = Node("a", null)
val b = Node("b", a)
val c = Node("c", null)
val d = Node("d", c)
assertNull(LowestCommonAncestor.find(a, d, Node::parent))
assertNull(LowestCommonAncestor.find(b, c, Node::parent))
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,95 @@
package net.shadowfacts.cacao.view
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.viewcontroller.ViewController
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class ViewClickTests {
lateinit var screen: CacaoScreen
lateinit var window: Window
lateinit var viewController: ViewController
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testClickInsideRootView() {
val mouse = CompletableFuture<Point>()
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
mouse.complete(point)
return true
}
})
assertTrue(window.mouseClicked(Point(75.0, 75.0), MouseButton.LEFT))
assertEquals(Point(25.0, 25.0), mouse.getNow(null))
}
@Test
fun testClickOutsideRootView() {
val clicked = CompletableFuture<Boolean>()
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
clicked.complete(true)
return true
}
})
assertFalse(window.mouseClicked(Point(25.0, 25.0), MouseButton.LEFT))
assertFalse(clicked.getNow(false))
}
@Test
fun testClickInsideNestedView() {
val mouse = CompletableFuture<Point>()
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
mouse.complete(point)
return true
}
})
assertTrue(window.mouseClicked(Point(100.0, 100.0), MouseButton.LEFT))
assertEquals(Point(25.0, 25.0), mouse.getNow(null))
}
@Test
fun testClickOutsideNestedView() {
val clicked = CompletableFuture<Boolean>()
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
clicked.complete(true)
return true
}
})
assertFalse(window.mouseClicked(Point(0.0, 0.0), MouseButton.LEFT))
assertFalse(clicked.getNow(false))
}
}

View File

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

View File

@ -0,0 +1,83 @@
package net.shadowfacts.cacao.view.button
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.window.Window
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.geometry.Rect
import net.shadowfacts.cacao.geometry.Size
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl
import org.junit.jupiter.api.Assertions.assertFalse
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import java.util.concurrent.CompletableFuture
/**
* @author shadowfacts
*/
class ButtonClickTests {
lateinit var screen: CacaoScreen
lateinit var window: Window
lateinit var viewController: ViewController
val view: View
get() = viewController.view
@BeforeEach
fun setup() {
screen = CacaoScreen()
viewController = object: ViewController() {
override fun loadView() {
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
}
}
window = screen.addWindow(Window(viewController))
}
@Test
fun testClickInsideButton() {
val clicked = CompletableFuture<Boolean>()
val content = View().apply {
intrinsicContentSize = Size(25.0, 25.0)
}
val button = view.addSubview(Button(content).apply {
handler = {
clicked.complete(true)
}
})
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
}
window.layout()
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertTrue(clicked.getNow(false))
}
@Test
fun testClickOutsideButton() {
val clicked = CompletableFuture<Boolean>()
val content = View().apply {
intrinsicContentSize = Size(25.0, 25.0)
}
val button = view.addSubview(Button(content).apply {
handler = {
clicked.complete(true)
}
})
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
}
window.layout()
assertFalse(window.mouseClicked(Point(50.0, 50.0), MouseButton.LEFT))
assertFalse(clicked.getNow(false))
}
}

View File

@ -0,0 +1,98 @@
package net.shadowfacts.cacao.view.button
import net.minecraft.text.LiteralText
import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.window.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;
val text = LiteralText(name)
}
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::text))
window.solver.dsl {
button.leftAnchor equalTo 0
button.topAnchor equalTo 0
button.widthAnchor equalTo 25
button.heightAnchor equalTo 25
}
window.layout()
}
@Test
fun testHandlerCalled() {
val called = CompletableFuture<Boolean>()
button.handler = {
called.complete(true)
}
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertTrue(called.getNow(false))
assertEquals(MyEnum.TWO, button.value)
}
@Test
fun testCyclesValues() {
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.TWO, button.value)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.THREE, button.value)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.ONE, button.value)
}
@Test
fun testCyclesValuesBackwards() {
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
assertEquals(MyEnum.TWO, button.value)
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.RIGHT))
assertEquals(MyEnum.ONE, button.value)
}
@Test
fun testMiddleClickDoesNotChangeValue() {
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.MIDDLE))
assertEquals(MyEnum.ONE, button.value)
}
}

View File

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

View File

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