Compare commits
11 Commits
4c5b7daf9e
...
2c19b8456b
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 2c19b8456b | |
Shadowfacts | 500ad94442 | |
Shadowfacts | 3926da0c3c | |
Shadowfacts | 9e3366cbfb | |
Shadowfacts | 277bcb71ee | |
Shadowfacts | d20ba7460e | |
Shadowfacts | c1ecdacc06 | |
Shadowfacts | ecfb696bc9 | |
Shadowfacts | 2c8b7cbb63 | |
Shadowfacts | 15f9b02041 | |
Shadowfacts | 8f577598ff |
|
@ -0,0 +1,3 @@
|
||||||
|
[submodule "kiwi-java"]
|
||||||
|
path = kiwi-java
|
||||||
|
url = git@git.shadowfacts.net:shadowfacts/kiwi-java.git
|
|
@ -2,6 +2,7 @@ plugins {
|
||||||
id "fabric-loom" version "0.6.49"
|
id "fabric-loom" version "0.6.49"
|
||||||
id "maven-publish"
|
id "maven-publish"
|
||||||
id "org.jetbrains.kotlin.jvm" version "1.4.30"
|
id "org.jetbrains.kotlin.jvm" version "1.4.30"
|
||||||
|
id "com.github.johnrengelman.shadow" version "4.0.4"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
|
@ -19,6 +20,7 @@ repositories {
|
||||||
maven {
|
maven {
|
||||||
url = "https://mod-buildcraft.com/maven"
|
url = "https://mod-buildcraft.com/maven"
|
||||||
}
|
}
|
||||||
|
jcenter()
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
@ -36,6 +38,10 @@ dependencies {
|
||||||
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
|
modImplementation "alexiil.mc.lib:libblockattributes-all:${project.libblockattributes_version}"
|
||||||
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
|
include "alexiil.mc.lib:libblockattributes-core:${project.libblockattributes_version}"
|
||||||
include "alexiil.mc.lib:libblockattributes-items:${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 {
|
processResources {
|
||||||
|
|
|
@ -13,3 +13,5 @@ fabric_version=0.30.0+1.16
|
||||||
fabric_kotlin_version=1.4.30+build.2
|
fabric_kotlin_version=1.4.30+build.2
|
||||||
|
|
||||||
libblockattributes_version=0.8.5
|
libblockattributes_version=0.8.5
|
||||||
|
|
||||||
|
junit_version = 5.4.0
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 1cbaea53d207f1e16c6e5ee2e6bf6e3c1440ac44
|
|
@ -8,3 +8,5 @@ pluginManagement {
|
||||||
gradlePluginPortal()
|
gradlePluginPortal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
include("kiwi-java")
|
||||||
|
|
|
@ -39,6 +39,9 @@ public final class IPAddress {
|
||||||
int b = Integer.parseInt(matcher.group(2));
|
int b = Integer.parseInt(matcher.group(2));
|
||||||
int c = Integer.parseInt(matcher.group(3));
|
int c = Integer.parseInt(matcher.group(3));
|
||||||
int d = Integer.parseInt(matcher.group(4));
|
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);
|
return new IPAddress(a, b, c, d);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,7 +15,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
|
||||||
@Mixin(HandledScreen.class)
|
@Mixin(HandledScreen.class)
|
||||||
public class MixinHandledScreen {
|
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) {
|
private void drawSlot(MatrixStack matrixStack, Slot slot, CallbackInfo ci) {
|
||||||
if ((Object)this instanceof TerminalScreen) {
|
if ((Object)this instanceof TerminalScreen) {
|
||||||
TerminalScreen self = (TerminalScreen)(Object)this;
|
TerminalScreen self = (TerminalScreen)(Object)this;
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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)
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package net.shadowfacts.cacao
|
||||||
|
|
||||||
|
import net.shadowfacts.cacao.view.View
|
||||||
|
import no.birkett.kiwi.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Kiwi variable that belongs to a Cacao view.
|
||||||
|
* This class generally isn't used directly, but via the anchor *Anchor properties on [View].
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class LayoutVariable(val owner: View, val property: String): Variable("LayoutVariable") {
|
||||||
|
|
||||||
|
override fun getName() = "$owner.$property"
|
||||||
|
|
||||||
|
override fun toString() = "LayoutVariable(name=$name, value=$value)"
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
# Cacao
|
||||||
|
Cacao is a UI framework for Fabric/Minecraft mods based on Apple's [Cocoa](https://en.wikipedia.org/wiki/Cocoa_(API)
|
||||||
|
UI toolkit.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
### Screen
|
||||||
|
A [CacaoScreen][] is the object that acts as the interface between Minecraft GUI code and the Cacao framework.
|
||||||
|
|
||||||
|
The CacaoScreen draws Cacao views on screen and passes Minecraft input events to the appropriate Views. The CacaoScreen
|
||||||
|
owns a group of [Window](#window) objects which are displayed on screen, one on top of the other.
|
||||||
|
|
||||||
|
[CacaoScreen]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/CacaoScreen.kt
|
||||||
|
|
||||||
|
### Window
|
||||||
|
A [Window][] object has a root [View Controller](#view-controller) that it displays on screen.
|
||||||
|
|
||||||
|
The Window occupies the entire screen space and translates events from the screen to the root View Controller's View.
|
||||||
|
It owns a Solver object that manages layout constraints. The window also handles screen resizing and re-lays out the
|
||||||
|
view hierarchy.
|
||||||
|
|
||||||
|
[Window]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/Window.kt
|
||||||
|
|
||||||
|
### View Controller
|
||||||
|
A [ViewController][] object owns a view, receives lifecycle events for it, and is generally used to control the view.
|
||||||
|
|
||||||
|
Each View Controller has a single root [View](#view) which in turn may have subviews.
|
||||||
|
|
||||||
|
[ViewController]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/viewcontroller/ViewController.kt
|
||||||
|
|
||||||
|
### View
|
||||||
|
A [View][] object represents a single view on screen. It handles drawing, positioning, and directly handles input.
|
||||||
|
|
||||||
|
[View]: https://git.shadowfacts.net/minecraft/ASMR/src/branch/master/src/main/kotlin/net/shadowfacts/cacao/view/View.kt
|
|
@ -0,0 +1,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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An axis in a 2D coordinate plane.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
enum class Axis {
|
||||||
|
HORIZONTAL, VERTICAL;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the axis that is perpendicular to this one.
|
||||||
|
*/
|
||||||
|
val perpendicular: Axis
|
||||||
|
get() = when (this) {
|
||||||
|
HORIZONTAL -> VERTICAL
|
||||||
|
VERTICAL -> HORIZONTAL
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A relative position on a line along an axis.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
enum class AxisPosition {
|
||||||
|
/**
|
||||||
|
* Top for vertical, left for horizontal.
|
||||||
|
*/
|
||||||
|
LEADING,
|
||||||
|
/**
|
||||||
|
* Center X/Y.
|
||||||
|
*/
|
||||||
|
CENTER,
|
||||||
|
/**
|
||||||
|
* Bottom for vertical, right for horizontal.
|
||||||
|
*/
|
||||||
|
TRAILING;
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
import java.lang.RuntimeException
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class that represents a cubic bezier curve.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
data class BezierCurve(private val points: Array<Point>) {
|
||||||
|
|
||||||
|
init {
|
||||||
|
if (points.size != 4) {
|
||||||
|
throw RuntimeException("Cubic bezier curve must have exactly four points")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun point(time: Double): Point {
|
||||||
|
val x = coordinate(time, Axis.HORIZONTAL)
|
||||||
|
val y = coordinate(time, Axis.VERTICAL)
|
||||||
|
return Point(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun coordinate(t: Double, axis: Axis): Double {
|
||||||
|
// B(t)=(1-t)^3*p0+3(1-t)^2*t*p1+3(1-t)*t^2*p2+t^3*p3
|
||||||
|
val p0 = points[0][axis]
|
||||||
|
val p1 = points[1][axis]
|
||||||
|
val p2 = points[2][axis]
|
||||||
|
val p3 = points[3][axis]
|
||||||
|
return ((1 - t).pow(3) * p0) + (3 * (1 - t).pow(2) * t * p1) + (3 * (1 - t) * t.pow(2) * p2) + (t.pow(3) * p3)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as BezierCurve
|
||||||
|
|
||||||
|
if (!points.contentEquals(other.points)) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return points.contentHashCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,31 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for defining 2D points.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
data class Point(val x: Double, val y: Double) {
|
||||||
|
|
||||||
|
constructor(x: Int, y: Int): this(x.toDouble(), y.toDouble())
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ORIGIN = Point(0.0, 0.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun plus(other: Point): Point {
|
||||||
|
return Point(x + other.x, y + other.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun minus(other: Point): Point {
|
||||||
|
return Point(x - other.x, y - other.y)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun get(axis: Axis): Double {
|
||||||
|
return when (axis) {
|
||||||
|
Axis.HORIZONTAL -> x
|
||||||
|
Axis.VERTICAL -> y
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for defining rectangles. Provides helper values for calculating perpendicular components of a rectangle based on X/Y/W/H.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
data class Rect(val left: Double, val top: Double, val width: Double, val height: Double) {
|
||||||
|
|
||||||
|
constructor(origin: Point, size: Size): this(origin.x, origin.y, size.width, size.height)
|
||||||
|
|
||||||
|
val right: Double by lazy {
|
||||||
|
left + width
|
||||||
|
}
|
||||||
|
val bottom: Double by lazy {
|
||||||
|
top + height
|
||||||
|
}
|
||||||
|
|
||||||
|
val midX: Double by lazy {
|
||||||
|
left + width / 2
|
||||||
|
}
|
||||||
|
val midY: Double by lazy {
|
||||||
|
top + height / 2
|
||||||
|
}
|
||||||
|
|
||||||
|
val origin: Point by lazy {
|
||||||
|
Point(left, top)
|
||||||
|
}
|
||||||
|
val center: Point by lazy {
|
||||||
|
Point(midX, midY)
|
||||||
|
}
|
||||||
|
|
||||||
|
val size: Size by lazy {
|
||||||
|
Size(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun contains(point: Point): Boolean {
|
||||||
|
return point.x in left..right && point.y in top..bottom
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class for specifying the size of objects.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
data class Size(val width: Double, val height: Double)
|
|
@ -0,0 +1,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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.shadowfacts.cacao.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
object EnumHelper {
|
||||||
|
|
||||||
|
fun <E: Enum<E>> next(value: E): E {
|
||||||
|
val constants = value.declaringClass.enumConstants
|
||||||
|
val index = constants.indexOf(value) + 1
|
||||||
|
return if (index < constants.size) constants[index] else constants.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <E: Enum<E>> previous(value: E): E {
|
||||||
|
val constants = value.declaringClass.enumConstants
|
||||||
|
val index = constants.indexOf(value) - 1
|
||||||
|
return if (index >= 0) constants[index] else constants.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package net.shadowfacts.cacao.util
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
import kotlin.NoSuchElementException
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A linear time algorithm for finding the lowest common ancestor of two nodes in a graph.
|
||||||
|
* Based on https://stackoverflow.com/a/6342546/4731558
|
||||||
|
*
|
||||||
|
* Works be finding the path from each node back to the root node.
|
||||||
|
* The LCA will then be the node after which the paths diverge.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
object LowestCommonAncestor {
|
||||||
|
|
||||||
|
fun <Node> find(node1: Node, node2: Node, parent: Node.() -> Node?): Node? {
|
||||||
|
@Suppress("NAME_SHADOWING") var node1: Node? = node1
|
||||||
|
@Suppress("NAME_SHADOWING") var node2: Node? = node2
|
||||||
|
|
||||||
|
val parent1 = LinkedList<Node>()
|
||||||
|
while (node1 != null) {
|
||||||
|
parent1.push(node1)
|
||||||
|
node1 = node1.parent()
|
||||||
|
}
|
||||||
|
|
||||||
|
val parent2 = LinkedList<Node>()
|
||||||
|
while (node2 != null) {
|
||||||
|
parent2.push(node2)
|
||||||
|
node2 = node2.parent()
|
||||||
|
}
|
||||||
|
|
||||||
|
// paths don't converge on the same root element
|
||||||
|
if (parent1.first != parent2.first) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
var oldNode: Node? = null
|
||||||
|
while (node1 == node2 && parent1.isNotEmpty() && parent2.isNotEmpty()) {
|
||||||
|
oldNode = node1
|
||||||
|
node1 = parent1.popOrNull()
|
||||||
|
node2 = parent2.popOrNull()
|
||||||
|
}
|
||||||
|
return if (node1 == node2) node1!!
|
||||||
|
else oldNode!!
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <T> LinkedList<T>.popOrNull(): T? {
|
||||||
|
return try {
|
||||||
|
pop()
|
||||||
|
} catch (e: NoSuchElementException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
package net.shadowfacts.cacao.util
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
enum class MouseButton {
|
||||||
|
LEFT, RIGHT, MIDDLE, UNKNOWN;
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromMC(button: Int): MouseButton {
|
||||||
|
return when (button) {
|
||||||
|
0 -> LEFT
|
||||||
|
1 -> RIGHT
|
||||||
|
2 -> MIDDLE
|
||||||
|
else -> UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package net.shadowfacts.cacao.util
|
||||||
|
|
||||||
|
import no.birkett.kiwi.Constraint
|
||||||
|
import no.birkett.kiwi.Term
|
||||||
|
import no.birkett.kiwi.Variable
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets all the variables used by this constraint.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
fun Constraint.getVariables(): List<Variable> {
|
||||||
|
return expression.terms.map(Term::getVariable)
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package net.shadowfacts.cacao.util.properties
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ObservableLateInitProperty<T: Any>(val observer: (T) -> Unit) {
|
||||||
|
|
||||||
|
lateinit var storage: T
|
||||||
|
|
||||||
|
val isInitialized: Boolean
|
||||||
|
get() = this::storage.isInitialized
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): T {
|
||||||
|
return storage
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun setValue(thisRef: Any, property: KProperty<*>, value: T) {
|
||||||
|
storage = value
|
||||||
|
observer(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.shadowfacts.cacao.util.properties
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ObservableLazyProperty<Value>(val create: () -> Value, val onCreate: () -> Unit) {
|
||||||
|
|
||||||
|
var storage: Value? = null
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
|
||||||
|
if (storage == null) {
|
||||||
|
storage = create()
|
||||||
|
onCreate()
|
||||||
|
}
|
||||||
|
return storage!!
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package net.shadowfacts.cacao.util.properties
|
||||||
|
|
||||||
|
import kotlin.reflect.KProperty
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ResettableLazyProperty<Value>(val initializer: () -> Value) {
|
||||||
|
var value: Value? = null
|
||||||
|
|
||||||
|
val isInitialized: Boolean
|
||||||
|
get() = value != null
|
||||||
|
|
||||||
|
operator fun getValue(thisRef: Any, property: KProperty<*>): Value {
|
||||||
|
if (value == null) {
|
||||||
|
value = initializer()
|
||||||
|
}
|
||||||
|
return value!!
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset() {
|
||||||
|
value = null
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,61 @@
|
||||||
|
package net.shadowfacts.cacao.util.texture
|
||||||
|
|
||||||
|
import net.minecraft.util.Identifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper class that represents a texture that can be divided into nine pieces (4 corners, 4 edges, and the center)
|
||||||
|
* and can be drawn at any size by combining and repeating those pieces.
|
||||||
|
*
|
||||||
|
* It also provides convenience [Texture] objects that represent the different patches.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
* @param texture The base [Texture] object.
|
||||||
|
* @param cornerWidth The width of each corner (and therefore the width of the vertical edges).
|
||||||
|
* @param cornerHeight The height of each corner (and therefore the height of the horizontal edges.)
|
||||||
|
* @param centerWidth The width of the center patch.
|
||||||
|
* @param centerHeight The height of the center patch.
|
||||||
|
*/
|
||||||
|
data class NinePatchTexture(val texture: Texture, val cornerWidth: Int, val cornerHeight: Int, val centerWidth: Int, val centerHeight: Int) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val PANEL_BG = NinePatchTexture(Texture(Identifier("textures/gui/demo_background.png"), 0, 0), 5, 5, 238, 156)
|
||||||
|
|
||||||
|
val BUTTON_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 66), 3, 3, 194, 14)
|
||||||
|
val BUTTON_HOVERED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 86), 3, 3, 194, 14)
|
||||||
|
val BUTTON_DISABLED_BG = NinePatchTexture(Texture(Identifier("textures/gui/widgets.png"), 0, 46), 3, 3, 194, 14)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Corners
|
||||||
|
val topLeft by lazy {
|
||||||
|
texture
|
||||||
|
}
|
||||||
|
val topRight by lazy {
|
||||||
|
Texture(texture.location, texture.u + cornerWidth + centerWidth, texture.v, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
val bottomLeft by lazy {
|
||||||
|
Texture(texture.location, texture.u, texture.v + cornerHeight + centerHeight, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
val bottomRight by lazy {
|
||||||
|
Texture(texture.location, topRight.u, bottomLeft.v, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Edges
|
||||||
|
val topMiddle by lazy {
|
||||||
|
Texture(texture.location, texture.u + cornerWidth, texture.v, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
val bottomMiddle by lazy {
|
||||||
|
Texture(texture.location, topMiddle.u, bottomLeft.v, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
val leftMiddle by lazy {
|
||||||
|
Texture(texture.location, texture.u, texture.v + cornerHeight, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
val rightMiddle by lazy {
|
||||||
|
Texture(texture.location, topRight.u, leftMiddle.v, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center
|
||||||
|
val center by lazy {
|
||||||
|
Texture(texture.location, texture.u + cornerWidth, texture.v + cornerHeight, texture.width, texture.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
package net.shadowfacts.cacao.util.texture
|
||||||
|
|
||||||
|
import net.minecraft.util.Identifier
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class that represents a texture.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
* @param location The identifier representing the resource-pack location of the texture image.
|
||||||
|
* @param u The X coordinate in pixels of where the texture starts within the image.
|
||||||
|
* @param v The Y coordinate in pixels of where the texture starts within the image.
|
||||||
|
* @param width The width in pixels of the entire image.
|
||||||
|
* @param height The height in pixels of the entire image.
|
||||||
|
*/
|
||||||
|
data class Texture(val location: Identifier, val u: Int, val v: Int, val width: Int = 256, val height: Int = 256)
|
|
@ -0,0 +1,36 @@
|
||||||
|
package net.shadowfacts.cacao.view
|
||||||
|
|
||||||
|
import net.minecraft.client.util.math.MatrixStack
|
||||||
|
import net.shadowfacts.cacao.geometry.BezierCurve
|
||||||
|
import net.shadowfacts.cacao.geometry.Point
|
||||||
|
import net.shadowfacts.cacao.util.Color
|
||||||
|
import net.shadowfacts.cacao.util.RenderHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class BezierCurveView(val curve: BezierCurve): View() {
|
||||||
|
|
||||||
|
private val points by lazy {
|
||||||
|
val step = 0.05
|
||||||
|
var t = 0.0
|
||||||
|
val points = mutableListOf<Point>()
|
||||||
|
while (t <= 1) {
|
||||||
|
points.add(curve.point(t))
|
||||||
|
t += step
|
||||||
|
}
|
||||||
|
points
|
||||||
|
}
|
||||||
|
|
||||||
|
var lineWidth = 3f
|
||||||
|
var lineColor = Color.BLACK
|
||||||
|
|
||||||
|
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
|
||||||
|
RenderHelper.scale(bounds.width, bounds.height)
|
||||||
|
for ((index, point) in points.withIndex()) {
|
||||||
|
val next = points.getOrNull(index + 1) ?: break
|
||||||
|
RenderHelper.drawLine(point, next, zIndex, lineWidth, lineColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,212 @@
|
||||||
|
package net.shadowfacts.cacao.view
|
||||||
|
|
||||||
|
import net.shadowfacts.kiwidsl.dsl
|
||||||
|
import net.shadowfacts.cacao.LayoutVariable
|
||||||
|
import net.shadowfacts.cacao.geometry.Axis
|
||||||
|
import net.shadowfacts.cacao.geometry.AxisPosition
|
||||||
|
import net.shadowfacts.cacao.geometry.AxisPosition.*
|
||||||
|
import no.birkett.kiwi.Constraint
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view that lays out its children in a stack along either the horizontal for vertical axes.
|
||||||
|
* This view does not have any content of its own.
|
||||||
|
*
|
||||||
|
* Only arranged subviews will be laid out in the stack mode, normal subviews must perform their own layout.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
* @param axis The primary axis that this stack lays out its children along.
|
||||||
|
* @param distribution The mode by which this stack lays out its children along the axis perpendicular to the
|
||||||
|
* primary [axis].
|
||||||
|
*/
|
||||||
|
open class StackView(
|
||||||
|
val axis: Axis,
|
||||||
|
val distribution: Distribution = Distribution.FILL,
|
||||||
|
val spacing: Double = 0.0
|
||||||
|
): View() {
|
||||||
|
|
||||||
|
// the internal mutable, list of arranged subviews
|
||||||
|
private val _arrangedSubviews = LinkedList<View>()
|
||||||
|
/**
|
||||||
|
* The list of arranged subviews belonging to this stack view.
|
||||||
|
* This list should never be mutated directly, only be calling the [addArrangedSubview]/[removeArrangedSubview]
|
||||||
|
* methods.
|
||||||
|
*/
|
||||||
|
val arrangedSubviews: List<View> = _arrangedSubviews
|
||||||
|
|
||||||
|
private var leadingConnection: Constraint? = null
|
||||||
|
private var trailingConnection: Constraint? = null
|
||||||
|
private var arrangedSubviewConnections = mutableListOf<Constraint>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an arranged subview to this view.
|
||||||
|
* Arranged subviews are laid out according to the stack. If you wish to add a subview that is laid out separately,
|
||||||
|
* use the normal [addSubview] method.
|
||||||
|
*
|
||||||
|
* @param view The view to add.
|
||||||
|
* @param index The index in this stack to add the view at.
|
||||||
|
* By default, adds the view to the end of the stack.
|
||||||
|
* @return The view that was added, as a convenience.
|
||||||
|
*/
|
||||||
|
fun <T: View> addArrangedSubview(view: T, index: Int = arrangedSubviews.size): T {
|
||||||
|
addSubview(view)
|
||||||
|
_arrangedSubviews.add(index, view)
|
||||||
|
|
||||||
|
addConstraintsForArrangedView(view, index)
|
||||||
|
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addConstraintsForArrangedView(view: View, index: Int) {
|
||||||
|
if (index == 0) {
|
||||||
|
if (leadingConnection != null) {
|
||||||
|
solver.removeConstraint(leadingConnection)
|
||||||
|
}
|
||||||
|
solver.dsl {
|
||||||
|
leadingConnection = anchor(LEADING) equalTo anchor(LEADING, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (index == arrangedSubviews.size - 1) {
|
||||||
|
if (trailingConnection != null) {
|
||||||
|
solver.removeConstraint(trailingConnection)
|
||||||
|
}
|
||||||
|
solver.dsl {
|
||||||
|
trailingConnection = anchor(TRAILING, view) equalTo anchor(TRAILING)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (arrangedSubviews.size > 1) {
|
||||||
|
solver.dsl {
|
||||||
|
val previous = arrangedSubviews.getOrNull(index - 1)
|
||||||
|
val next = arrangedSubviews.getOrNull(index + 1)
|
||||||
|
if (next != null) {
|
||||||
|
arrangedSubviewConnections.add(index, anchor(TRAILING, view) equalTo (anchor(LEADING, next) + spacing))
|
||||||
|
}
|
||||||
|
if (previous != null) {
|
||||||
|
arrangedSubviewConnections.add(index - 1, anchor(TRAILING, previous) equalTo (anchor(LEADING, view) - spacing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
solver.dsl {
|
||||||
|
when (distribution) {
|
||||||
|
Distribution.LEADING ->
|
||||||
|
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
|
||||||
|
Distribution.TRAILING ->
|
||||||
|
perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
|
||||||
|
Distribution.FILL -> {
|
||||||
|
perpAnchor(LEADING) equalTo perpAnchor(LEADING, view)
|
||||||
|
perpAnchor(TRAILING) equalTo perpAnchor(TRAILING, view)
|
||||||
|
}
|
||||||
|
Distribution.CENTER ->
|
||||||
|
perpAnchor(CENTER) equalTo perpAnchor(CENTER, view)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun anchor(position: AxisPosition, view: View = this): LayoutVariable {
|
||||||
|
return view.getAnchor(axis, position)
|
||||||
|
}
|
||||||
|
private fun perpAnchor(position: AxisPosition, view: View = this): LayoutVariable {
|
||||||
|
return view.getAnchor(axis.perpendicular, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the modes of how content is distributed in a stack view along the perpendicular axis (i.e. the
|
||||||
|
* non-primary axis).
|
||||||
|
*
|
||||||
|
* ASCII-art examples are shown below in a stack view with the primary axis [Axis.VERTICAL].
|
||||||
|
*/
|
||||||
|
enum class Distribution {
|
||||||
|
/**
|
||||||
|
* The leading edges of arranged subviews are pinned to the leading edge of the stack view.
|
||||||
|
* ```
|
||||||
|
* ┌─────────────────────────────┐
|
||||||
|
* │┌─────────────┐ │
|
||||||
|
* ││ │ │
|
||||||
|
* ││ │ │
|
||||||
|
* ││ │ │
|
||||||
|
* │└─────────────┘ │
|
||||||
|
* │┌─────────┐ │
|
||||||
|
* ││ │ │
|
||||||
|
* ││ │ │
|
||||||
|
* ││ │ │
|
||||||
|
* │└─────────┘ │
|
||||||
|
* │┌───────────────────────────┐│
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* │└───────────────────────────┘│
|
||||||
|
* └─────────────────────────────┘
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
LEADING,
|
||||||
|
/**
|
||||||
|
* The centers of the arranged subviews are pinned to the center of the stack view.
|
||||||
|
* ```
|
||||||
|
* ┌─────────────────────────────┐
|
||||||
|
* │ ┌─────────────┐ │
|
||||||
|
* │ │ │ │
|
||||||
|
* │ │ │ │
|
||||||
|
* │ │ │ │
|
||||||
|
* │ └─────────────┘ │
|
||||||
|
* │ ┌─────────┐ │
|
||||||
|
* │ │ │ │
|
||||||
|
* │ │ │ │
|
||||||
|
* │ │ │ │
|
||||||
|
* │ └─────────┘ │
|
||||||
|
* │┌───────────────────────────┐│
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* │└───────────────────────────┘│
|
||||||
|
* └─────────────────────────────┘
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
CENTER,
|
||||||
|
/**
|
||||||
|
* The trailing edges of arranged subviews are pinned to the leading edge of the stack view.
|
||||||
|
* ```
|
||||||
|
* ┌─────────────────────────────┐
|
||||||
|
* │ ┌─────────────┐│
|
||||||
|
* │ │ ││
|
||||||
|
* │ │ ││
|
||||||
|
* │ │ ││
|
||||||
|
* │ └─────────────┘│
|
||||||
|
* │ ┌─────────┐│
|
||||||
|
* │ │ ││
|
||||||
|
* │ │ ││
|
||||||
|
* │ │ ││
|
||||||
|
* │ └─────────┘│
|
||||||
|
* │┌───────────────────────────┐│
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* │└───────────────────────────┘│
|
||||||
|
* └─────────────────────────────┘
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
TRAILING,
|
||||||
|
/**
|
||||||
|
* The arranged subviews fill the perpendicular axis of the stack view.
|
||||||
|
* ```
|
||||||
|
* ┌─────────────────────────────┐
|
||||||
|
* │┌───────────────────────────┐│
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* │└───────────────────────────┘│
|
||||||
|
* │┌───────────────────────────┐│
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* │└───────────────────────────┘│
|
||||||
|
* │┌───────────────────────────┐│
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* ││ ││
|
||||||
|
* │└───────────────────────────┘│
|
||||||
|
* └─────────────────────────────┘
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
FILL
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package net.shadowfacts.cacao.view
|
||||||
|
|
||||||
|
import net.minecraft.client.util.math.MatrixStack
|
||||||
|
import net.shadowfacts.cacao.geometry.Point
|
||||||
|
import net.shadowfacts.cacao.util.RenderHelper
|
||||||
|
import net.shadowfacts.cacao.util.texture.Texture
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper class for drawing a [Texture] in a view.
|
||||||
|
* `TextureView` will draw the given texture filling the bounds of the view.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class TextureView(var texture: Texture): View() {
|
||||||
|
|
||||||
|
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
|
||||||
|
RenderHelper.draw(matrixStack, bounds, texture)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,46 @@
|
||||||
|
package net.shadowfacts.cacao.view.button
|
||||||
|
|
||||||
|
import net.minecraft.util.Identifier
|
||||||
|
import net.shadowfacts.cacao.geometry.Point
|
||||||
|
import net.shadowfacts.cacao.geometry.Size
|
||||||
|
import net.shadowfacts.cacao.util.MouseButton
|
||||||
|
import net.shadowfacts.cacao.util.texture.Texture
|
||||||
|
import net.shadowfacts.cacao.view.TextureView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A button for toggling between on/off states.
|
||||||
|
*
|
||||||
|
* @author shadowfacts
|
||||||
|
* @param initialState Whether the button starts as on or off.
|
||||||
|
*/
|
||||||
|
class ToggleButton(initialState: Boolean): AbstractButton<ToggleButton>(TextureView(if (initialState) ON else OFF).apply {
|
||||||
|
intrinsicContentSize = Size(19.0, 19.0)
|
||||||
|
}, padding = 0.0) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val ON = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 0)
|
||||||
|
val OFF = Texture(Identifier("asmr", "textures/gui/toggle.png"), 0, 19)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val textureView: TextureView
|
||||||
|
get() = content as TextureView
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The button's current on/off state.
|
||||||
|
* Updating this property updates the button's texture.
|
||||||
|
*/
|
||||||
|
var state: Boolean = initialState
|
||||||
|
set(value) {
|
||||||
|
field = value
|
||||||
|
textureView.texture = if (value) ON else OFF
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
|
||||||
|
if (!disabled && (mouseButton == MouseButton.LEFT || mouseButton == MouseButton.RIGHT)) {
|
||||||
|
state = !state
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.mouseClicked(point, mouseButton)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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) {
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -8,11 +8,7 @@ import net.minecraft.util.Identifier
|
||||||
import net.shadowfacts.phycon.PhysicalConnectivity
|
import net.shadowfacts.phycon.PhysicalConnectivity
|
||||||
import net.shadowfacts.phycon.network.DeviceBlock
|
import net.shadowfacts.phycon.network.DeviceBlock
|
||||||
import net.shadowfacts.phycon.network.DeviceBlockEntity
|
import net.shadowfacts.phycon.network.DeviceBlockEntity
|
||||||
import net.shadowfacts.phycon.network.block.redstone.RedstoneControllerBlockEntity
|
import net.shadowfacts.phycon.screen.console.DeviceConsoleScreen
|
||||||
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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author shadowfacts
|
* @author shadowfacts
|
||||||
|
@ -40,11 +36,8 @@ class ConsoleItem: Item(Settings()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun openScreen(be: DeviceBlockEntity) {
|
private fun openScreen(be: DeviceBlockEntity) {
|
||||||
val screen = when (be) {
|
// val screen = TestCacaoScreen()
|
||||||
is ActivationController.ActivatableDevice -> ActivatableDeviceConsoleScreen(be)
|
val screen = DeviceConsoleScreen(be)
|
||||||
is RedstoneControllerBlockEntity -> RedstoneControllerConsoleScreen(be)
|
|
||||||
else -> DeviceConsoleScreen(be)
|
|
||||||
}
|
|
||||||
MinecraftClient.getInstance().openScreen(screen)
|
MinecraftClient.getInstance().openScreen(screen)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -11,6 +11,13 @@
|
||||||
"item.phycon.console": "Console",
|
"item.phycon.console": "Console",
|
||||||
|
|
||||||
"gui.phycon.terminal_buffer": "Buffer",
|
"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.high": "High",
|
||||||
"gui.phycon.redstone_mode.low": "Low",
|
"gui.phycon.redstone_mode.low": "Low",
|
||||||
"gui.phycon.redstone_mode.toggle": "Toggle",
|
"gui.phycon.redstone_mode.toggle": "Toggle",
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
"package": "net.shadowfacts.phycon.mixin.client",
|
"package": "net.shadowfacts.phycon.mixin.client",
|
||||||
"compatibilityLevel": "JAVA_8",
|
"compatibilityLevel": "JAVA_8",
|
||||||
"mixins": [
|
"mixins": [
|
||||||
"MixinHandledScreen"
|
"MixinHandledScreen",
|
||||||
|
"TextFieldWidgetAccessor"
|
||||||
],
|
],
|
||||||
"injectors": {
|
"injectors": {
|
||||||
"defaultRequire": 1
|
"defaultRequire": 1
|
||||||
|
|
|
@ -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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
package net.shadowfacts.cacao.geometry
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class RectTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testTrailingEdges() {
|
||||||
|
val rect = Rect(25.0, 50.0, 100.0, 200.0)
|
||||||
|
assertEquals(125.0, rect.right)
|
||||||
|
assertEquals(250.0, rect.bottom)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testCenter() {
|
||||||
|
val rect = Rect(25.0, 50.0, 100.0, 200.0)
|
||||||
|
assertEquals(75.0, rect.midX)
|
||||||
|
assertEquals(150.0, rect.midY)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPoints() {
|
||||||
|
val rect = Rect(25.0, 50.0, 100.0, 200.0)
|
||||||
|
assertEquals(Point(25.0, 50.0), rect.origin)
|
||||||
|
assertEquals(Point(75.0, 150.0), rect.center)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testSize() {
|
||||||
|
val rect = Rect(25.0, 50.0, 100.0, 200.0)
|
||||||
|
assertEquals(Size(100.0, 200.0), rect.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,25 @@
|
||||||
|
package net.shadowfacts.cacao.util
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ColorTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun fromRGB() {
|
||||||
|
val color = Color(0x123456)
|
||||||
|
assertEquals(0x12, color.red)
|
||||||
|
assertEquals(0x34, color.green)
|
||||||
|
assertEquals(0x56, color.blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun toARGB() {
|
||||||
|
val color = Color(red = 0x12, green = 0x34, blue = 0x56, alpha = 0x78)
|
||||||
|
assertEquals(0x78123456, color.argb)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,27 @@
|
||||||
|
package net.shadowfacts.cacao.util
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class EnumHelperTests {
|
||||||
|
|
||||||
|
enum class MyEnum {
|
||||||
|
ONE, TWO
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testNext() {
|
||||||
|
assertEquals(MyEnum.TWO, EnumHelper.next(MyEnum.ONE))
|
||||||
|
assertEquals(MyEnum.ONE, EnumHelper.next(MyEnum.TWO))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testPrev() {
|
||||||
|
assertEquals(MyEnum.ONE, EnumHelper.previous(MyEnum.TWO))
|
||||||
|
assertEquals(MyEnum.TWO, EnumHelper.previous(MyEnum.ONE))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,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))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,33 @@
|
||||||
|
package net.shadowfacts.cacao.util.properties
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ObservableLateInitPropertyTests {
|
||||||
|
|
||||||
|
class MyClass(callback: (String) -> Unit) {
|
||||||
|
val delegate = ObservableLateInitProperty(callback)
|
||||||
|
var prop by delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testObservation() {
|
||||||
|
val future = CompletableFuture<String>()
|
||||||
|
val obj = MyClass { future.complete(it) }
|
||||||
|
obj.prop = "test"
|
||||||
|
assertEquals("test", future.getNow(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInitialized() {
|
||||||
|
val obj = MyClass {}
|
||||||
|
assertFalse(obj.delegate.isInitialized)
|
||||||
|
obj.prop = "test"
|
||||||
|
assertTrue(obj.delegate.isInitialized)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
package net.shadowfacts.cacao.util.properties
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ResettableLazyPropertyTests {
|
||||||
|
|
||||||
|
class MyClass {
|
||||||
|
var iteration = 1
|
||||||
|
val delegate = ResettableLazyProperty { "test ${iteration++}" }
|
||||||
|
val prop by delegate
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testResets() {
|
||||||
|
val obj = MyClass()
|
||||||
|
assertEquals("test 1", obj.prop)
|
||||||
|
obj.delegate.reset()
|
||||||
|
assertEquals("test 2", obj.prop)
|
||||||
|
assertEquals("test 2", obj.prop)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testIsInitialized() {
|
||||||
|
val obj = MyClass()
|
||||||
|
assertFalse(obj.delegate.isInitialized)
|
||||||
|
assertEquals("test 1", obj.prop)
|
||||||
|
assertTrue(obj.delegate.isInitialized)
|
||||||
|
obj.delegate.reset()
|
||||||
|
assertFalse(obj.delegate.isInitialized)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,261 @@
|
||||||
|
package net.shadowfacts.cacao.view
|
||||||
|
|
||||||
|
import net.shadowfacts.cacao.CacaoScreen
|
||||||
|
import net.shadowfacts.kiwidsl.dsl
|
||||||
|
import net.shadowfacts.cacao.window.Window
|
||||||
|
import net.shadowfacts.cacao.geometry.Axis
|
||||||
|
import net.shadowfacts.cacao.geometry.Rect
|
||||||
|
import net.shadowfacts.cacao.geometry.Size
|
||||||
|
import net.shadowfacts.cacao.viewcontroller.ViewController
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class StackViewLayoutTests {
|
||||||
|
|
||||||
|
lateinit var screen: CacaoScreen
|
||||||
|
lateinit var viewController: ViewController
|
||||||
|
lateinit var window: Window
|
||||||
|
|
||||||
|
val view: View
|
||||||
|
get() = viewController.view
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
screen = CacaoScreen()
|
||||||
|
viewController = object: ViewController() {
|
||||||
|
override fun loadView() {
|
||||||
|
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window = screen.addWindow(Window(viewController))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVerticalLayout() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.VERTICAL))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.topAnchor equalTo 0
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(one.topAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
|
||||||
|
assertEquals(50.0, one.bottomAnchor.value)
|
||||||
|
assertEquals(50.0, two.topAnchor.value)
|
||||||
|
assertEquals(125.0, two.bottomAnchor.value)
|
||||||
|
assertEquals(125.0, three.topAnchor.value)
|
||||||
|
assertEquals(175.0, three.bottomAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(175.0, stack.heightAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHorizontalLayout() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.HORIZONTAL))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.leftAnchor equalTo 0
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(one.leftAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
|
||||||
|
assertEquals(50.0, one.rightAnchor.value)
|
||||||
|
assertEquals(50.0, two.leftAnchor.value)
|
||||||
|
assertEquals(125.0, two.rightAnchor.value)
|
||||||
|
assertEquals(125.0, three.leftAnchor.value)
|
||||||
|
assertEquals(175.0, three.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(175.0, stack.widthAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVerticalLayoutWithLeading() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.LEADING))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(100.0, 100.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.topAnchor equalTo 0
|
||||||
|
stack.leftAnchor equalTo 0
|
||||||
|
stack.rightAnchor equalTo 100
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(one.leftAnchor.value))
|
||||||
|
assertEquals(50.0, one.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(two.leftAnchor.value))
|
||||||
|
assertEquals(75.0, two.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(three.leftAnchor.value))
|
||||||
|
assertEquals(100.0, three.rightAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVerticalLayoutWithTrailing() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.TRAILING))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(100.0, 100.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.topAnchor equalTo 0
|
||||||
|
stack.leftAnchor equalTo 0
|
||||||
|
stack.rightAnchor equalTo 100
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(50.0, one.leftAnchor.value)
|
||||||
|
assertEquals(100.0, one.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(25.0, two.leftAnchor.value)
|
||||||
|
assertEquals(100.0, two.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(three.leftAnchor.value))
|
||||||
|
assertEquals(100.0, three.rightAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVerticalLayoutWithCenter() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(100.0, 100.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.topAnchor equalTo 0
|
||||||
|
stack.leftAnchor equalTo 0
|
||||||
|
stack.rightAnchor equalTo 100
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(25.0, one.leftAnchor.value)
|
||||||
|
assertEquals(75.0, one.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(12.5, two.leftAnchor.value)
|
||||||
|
assertEquals(87.5, two.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(three.leftAnchor.value))
|
||||||
|
assertEquals(100.0, three.rightAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVerticalLayoutWithFill() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.FILL))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(100.0, 100.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.topAnchor equalTo 0
|
||||||
|
stack.leftAnchor equalTo 0
|
||||||
|
stack.rightAnchor equalTo 100
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(one.leftAnchor.value))
|
||||||
|
assertEquals(100.0, one.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(two.leftAnchor.value))
|
||||||
|
assertEquals(100.0, two.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(three.leftAnchor.value))
|
||||||
|
assertEquals(100.0, three.rightAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testVerticalLayoutWithSpacing() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.VERTICAL, spacing = 10.0))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.topAnchor equalTo 0
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(one.topAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
|
||||||
|
assertEquals(50.0, one.bottomAnchor.value)
|
||||||
|
assertEquals(60.0, two.topAnchor.value)
|
||||||
|
assertEquals(135.0, two.bottomAnchor.value)
|
||||||
|
assertEquals(145.0, three.topAnchor.value)
|
||||||
|
assertEquals(195.0, three.bottomAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(195.0, stack.heightAnchor.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHorizontalLayoutWithSpacing() {
|
||||||
|
val stack = view.addSubview(StackView(Axis.HORIZONTAL, spacing = 10.0))
|
||||||
|
val one = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
val two = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(75.0, 75.0)
|
||||||
|
})
|
||||||
|
val three = stack.addArrangedSubview(View().apply {
|
||||||
|
intrinsicContentSize = Size(50.0, 50.0)
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
stack.leftAnchor equalTo 0
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertEquals(0.0, abs(one.leftAnchor.value)) // sometimes -0.0, which fails the assertion but is actually ok
|
||||||
|
assertEquals(50.0, one.rightAnchor.value)
|
||||||
|
assertEquals(60.0, two.leftAnchor.value)
|
||||||
|
assertEquals(135.0, two.rightAnchor.value)
|
||||||
|
assertEquals(145.0, three.leftAnchor.value)
|
||||||
|
assertEquals(195.0, three.rightAnchor.value)
|
||||||
|
|
||||||
|
assertEquals(195.0, stack.widthAnchor.value)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,95 @@
|
||||||
|
package net.shadowfacts.cacao.view
|
||||||
|
|
||||||
|
import net.shadowfacts.cacao.CacaoScreen
|
||||||
|
import net.shadowfacts.cacao.window.Window
|
||||||
|
import net.shadowfacts.cacao.geometry.Point
|
||||||
|
import net.shadowfacts.cacao.geometry.Rect
|
||||||
|
import net.shadowfacts.cacao.util.MouseButton
|
||||||
|
import net.shadowfacts.cacao.viewcontroller.ViewController
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ViewClickTests {
|
||||||
|
|
||||||
|
lateinit var screen: CacaoScreen
|
||||||
|
lateinit var window: Window
|
||||||
|
lateinit var viewController: ViewController
|
||||||
|
|
||||||
|
val view: View
|
||||||
|
get() = viewController.view
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
screen = CacaoScreen()
|
||||||
|
viewController = object: ViewController() {
|
||||||
|
override fun loadView() {
|
||||||
|
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window = screen.addWindow(Window(viewController))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClickInsideRootView() {
|
||||||
|
val mouse = CompletableFuture<Point>()
|
||||||
|
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
|
||||||
|
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
|
||||||
|
mouse.complete(point)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assertTrue(window.mouseClicked(Point(75.0, 75.0), MouseButton.LEFT))
|
||||||
|
|
||||||
|
assertEquals(Point(25.0, 25.0), mouse.getNow(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClickOutsideRootView() {
|
||||||
|
val clicked = CompletableFuture<Boolean>()
|
||||||
|
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
|
||||||
|
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
|
||||||
|
clicked.complete(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assertFalse(window.mouseClicked(Point(25.0, 25.0), MouseButton.LEFT))
|
||||||
|
|
||||||
|
assertFalse(clicked.getNow(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClickInsideNestedView() {
|
||||||
|
val mouse = CompletableFuture<Point>()
|
||||||
|
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
|
||||||
|
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
|
||||||
|
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
|
||||||
|
mouse.complete(point)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assertTrue(window.mouseClicked(Point(100.0, 100.0), MouseButton.LEFT))
|
||||||
|
|
||||||
|
assertEquals(Point(25.0, 25.0), mouse.getNow(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClickOutsideNestedView() {
|
||||||
|
val clicked = CompletableFuture<Boolean>()
|
||||||
|
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
|
||||||
|
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
|
||||||
|
override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
|
||||||
|
clicked.complete(true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
assertFalse(window.mouseClicked(Point(0.0, 0.0), MouseButton.LEFT))
|
||||||
|
|
||||||
|
assertFalse(clicked.getNow(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package net.shadowfacts.cacao.view
|
||||||
|
|
||||||
|
import net.minecraft.client.util.math.MatrixStack
|
||||||
|
import net.shadowfacts.cacao.CacaoScreen
|
||||||
|
import net.shadowfacts.cacao.window.Window
|
||||||
|
import net.shadowfacts.cacao.geometry.Point
|
||||||
|
import net.shadowfacts.cacao.geometry.Rect
|
||||||
|
import net.shadowfacts.cacao.viewcontroller.ViewController
|
||||||
|
import org.junit.jupiter.api.Assertions.assertEquals
|
||||||
|
import org.junit.jupiter.api.BeforeAll
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ViewHoverTests {
|
||||||
|
|
||||||
|
lateinit var screen: CacaoScreen
|
||||||
|
lateinit var viewController: ViewController
|
||||||
|
lateinit var window: Window
|
||||||
|
|
||||||
|
val view: View
|
||||||
|
get() = viewController.view
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@BeforeAll
|
||||||
|
@JvmStatic
|
||||||
|
fun setupAll() {
|
||||||
|
System.setProperty("cacao.drawing.disabled", "true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
screen = CacaoScreen()
|
||||||
|
viewController = object: ViewController() {
|
||||||
|
override fun loadView() {
|
||||||
|
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window = screen.addWindow(Window(viewController))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHoverRootView() {
|
||||||
|
val point = CompletableFuture<Point>()
|
||||||
|
view.addSubview(object: View(Rect(50.0, 50.0, 100.0, 100.0)) {
|
||||||
|
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
|
||||||
|
point.complete(mouse)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.draw(MatrixStack(), Point(75.0, 75.0), 0f)
|
||||||
|
|
||||||
|
assertEquals(Point(25.0, 25.0), point.getNow(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testHoverNestedView() {
|
||||||
|
val point = CompletableFuture<Point>()
|
||||||
|
val root = view.addSubview(View(Rect(50.0, 50.0, 100.0, 100.0)))
|
||||||
|
root.addSubview(object: View(Rect(25.0, 25.0, 50.0, 50.0)) {
|
||||||
|
override fun drawContent(matrixStack: MatrixStack, mouse: Point, delta: Float) {
|
||||||
|
point.complete(mouse)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.draw(MatrixStack(), Point(100.0, 100.0), 0f)
|
||||||
|
|
||||||
|
assertEquals(Point(25.0, 25.0), point.getNow(null))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package net.shadowfacts.cacao.view.button
|
||||||
|
|
||||||
|
import net.shadowfacts.cacao.CacaoScreen
|
||||||
|
import net.shadowfacts.cacao.window.Window
|
||||||
|
import net.shadowfacts.cacao.geometry.Point
|
||||||
|
import net.shadowfacts.cacao.geometry.Rect
|
||||||
|
import net.shadowfacts.cacao.geometry.Size
|
||||||
|
import net.shadowfacts.cacao.util.MouseButton
|
||||||
|
import net.shadowfacts.cacao.view.View
|
||||||
|
import net.shadowfacts.cacao.viewcontroller.ViewController
|
||||||
|
import net.shadowfacts.kiwidsl.dsl
|
||||||
|
import org.junit.jupiter.api.Assertions.assertFalse
|
||||||
|
import org.junit.jupiter.api.Assertions.assertTrue
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.util.concurrent.CompletableFuture
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class ButtonClickTests {
|
||||||
|
|
||||||
|
lateinit var screen: CacaoScreen
|
||||||
|
lateinit var window: Window
|
||||||
|
lateinit var viewController: ViewController
|
||||||
|
|
||||||
|
val view: View
|
||||||
|
get() = viewController.view
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
screen = CacaoScreen()
|
||||||
|
viewController = object: ViewController() {
|
||||||
|
override fun loadView() {
|
||||||
|
view = View(Rect(0.0, 0.0, 1000.0, 1000.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window = screen.addWindow(Window(viewController))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClickInsideButton() {
|
||||||
|
val clicked = CompletableFuture<Boolean>()
|
||||||
|
val content = View().apply {
|
||||||
|
intrinsicContentSize = Size(25.0, 25.0)
|
||||||
|
}
|
||||||
|
val button = view.addSubview(Button(content).apply {
|
||||||
|
handler = {
|
||||||
|
clicked.complete(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
button.leftAnchor equalTo 0
|
||||||
|
button.topAnchor equalTo 0
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertTrue(window.mouseClicked(Point(5.0, 5.0), MouseButton.LEFT))
|
||||||
|
assertTrue(clicked.getNow(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun testClickOutsideButton() {
|
||||||
|
val clicked = CompletableFuture<Boolean>()
|
||||||
|
val content = View().apply {
|
||||||
|
intrinsicContentSize = Size(25.0, 25.0)
|
||||||
|
}
|
||||||
|
val button = view.addSubview(Button(content).apply {
|
||||||
|
handler = {
|
||||||
|
clicked.complete(true)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
window.solver.dsl {
|
||||||
|
button.leftAnchor equalTo 0
|
||||||
|
button.topAnchor equalTo 0
|
||||||
|
}
|
||||||
|
window.layout()
|
||||||
|
|
||||||
|
assertFalse(window.mouseClicked(Point(50.0, 50.0), MouseButton.LEFT))
|
||||||
|
assertFalse(clicked.getNow(false))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package net.shadowfacts.kiwidsl
|
||||||
|
|
||||||
|
import no.birkett.kiwi.Solver
|
||||||
|
import no.birkett.kiwi.Variable
|
||||||
|
import org.junit.jupiter.api.Assertions.*
|
||||||
|
import org.junit.jupiter.api.BeforeEach
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author shadowfacts
|
||||||
|
*/
|
||||||
|
class DSLTest {
|
||||||
|
|
||||||
|
val EPSILON = 1.0e-8
|
||||||
|
|
||||||
|
lateinit var solver: Solver
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
fun setup() {
|
||||||
|
solver = Solver()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simpleNew() {
|
||||||
|
val x = Variable("x")
|
||||||
|
solver.dsl {
|
||||||
|
(x + 2) equalTo 20
|
||||||
|
}
|
||||||
|
solver.updateVariables()
|
||||||
|
assertEquals(x.value, 18.0, EPSILON)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simple0() {
|
||||||
|
val x = Variable("x")
|
||||||
|
val y = Variable("y")
|
||||||
|
solver.dsl {
|
||||||
|
x equalTo 20
|
||||||
|
(x + 2) equalTo (y + 10)
|
||||||
|
}
|
||||||
|
solver.updateVariables()
|
||||||
|
assertEquals(x.value, 20.0, EPSILON)
|
||||||
|
assertEquals(y.value, 12.0, EPSILON)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun simple1() {
|
||||||
|
val x = Variable("x")
|
||||||
|
val y = Variable("y")
|
||||||
|
solver.dsl {
|
||||||
|
x equalTo y
|
||||||
|
}
|
||||||
|
solver.updateVariables()
|
||||||
|
assertEquals(x.value, y.value, EPSILON)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun casso1() {
|
||||||
|
val x = Variable("x")
|
||||||
|
val y = Variable("y")
|
||||||
|
solver.dsl {
|
||||||
|
x lessThanOrEqualTo y
|
||||||
|
y equalTo (x + 3)
|
||||||
|
x.equalTo(10, strength = WEAK)
|
||||||
|
y.equalTo(10, strength = WEAK)
|
||||||
|
}
|
||||||
|
solver.updateVariables()
|
||||||
|
if (abs(x.value - 10.0) < EPSILON) {
|
||||||
|
assertEquals(10.0, x.value, EPSILON)
|
||||||
|
assertEquals(13.0, y.value, EPSILON)
|
||||||
|
} else {
|
||||||
|
assertEquals(7.0, x.value, EPSILON)
|
||||||
|
assertEquals(10.0, y.value, EPSILON)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue