Cacao: Add text field

This commit is contained in:
Shadowfacts 2021-02-27 14:02:30 -05:00
parent 277bcb71ee
commit 9e3366cbfb
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
8 changed files with 357 additions and 59 deletions

View File

@ -6,9 +6,11 @@ import net.minecraft.client.util.math.MatrixStack
import net.minecraft.sound.SoundEvents
import net.minecraft.text.LiteralText
import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.window.Window
import org.lwjgl.glfw.GLFW
import java.util.*
/**
@ -55,6 +57,7 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
*/
override fun removeWindow(window: Window) {
_windows.remove(window)
// todo: VC callbacks
}
override fun init() {
@ -65,6 +68,17 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
}
}
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
@ -102,4 +116,35 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
return result == true
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.keyPressed(keyCode, modifiersSet) }) {
return true
}
return super.keyPressed(keyCode, scanCode, modifiers)
}
override fun keyReleased(i: Int, j: Int, k: Int): Boolean {
return super.keyReleased(i, j, k)
}
override fun charTyped(char: Char, modifiers: Int): Boolean {
val modifiersSet by lazy { KeyModifiers(modifiers) }
if (findResponder { it.charTyped(char, modifiersSet) }) {
return true
}
return super.charTyped(char, modifiers)
}
private fun findResponder(fn: (Responder) -> Boolean): Boolean {
var responder = windows.lastOrNull()?.firstResponder
while (responder != null) {
if (fn(responder)) {
return true
}
responder = responder.nextResponder
}
return false
}
}

View File

@ -0,0 +1,44 @@
package net.shadowfacts.cacao
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.window.Window
/**
* @author shadowfacts
*/
interface Responder {
val window: Window?
val isFirstResponder: Boolean
get() = window?.firstResponder === this
val nextResponder: Responder?
fun becomeFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot become first responder while not in Window")
}
window!!.firstResponder = this
}
fun didBecomeFirstResponder() {}
fun resignFirstResponder() {
if (window == null) {
throw RuntimeException("Cannot resign first responder while not in Window")
}
window!!.firstResponder = null
}
fun didResignFirstResponder() {}
fun charTyped(char: Char, modifiers: KeyModifiers): Boolean {
return false
}
fun keyPressed(keyCode: Int, modifiers: KeyModifiers): Boolean {
return false
}
}

View File

@ -0,0 +1,32 @@
package net.shadowfacts.cacao.util
import org.lwjgl.glfw.GLFW
/**
* @author shadowfacts
*/
class KeyModifiers(val value: Int) {
val shift: Boolean
get() = this[GLFW.GLFW_MOD_SHIFT]
val control: Boolean
get() = this[GLFW.GLFW_MOD_CONTROL]
val alt: Boolean
get() = this[GLFW.GLFW_MOD_ALT]
val command: Boolean
get() = this[GLFW.GLFW_MOD_SUPER]
val capsLock: Boolean
get() = this[GLFW.GLFW_MOD_CAPS_LOCK]
val numLock: Boolean
get() = this[GLFW.GLFW_MOD_NUM_LOCK]
private operator fun get(mod: Int): Boolean {
return (value and mod) == mod
}
}

View File

@ -3,6 +3,7 @@ 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.*
@ -19,14 +20,18 @@ import kotlin.collections.HashSet
*
* @author shadowfacts
*/
open class View() {
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.
*/
var window: Window? = null
override var window: Window? = null
override val nextResponder: Responder?
// todo: should the view controller be a Responder?
get() = superview
private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) {
@ -354,12 +359,25 @@ open class View() {
* @return Whether the mouse click was handled by this view or any subviews.
*/
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean {
val view = subviewsAtPoint(point).maxByOrNull(View::zIndex)
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)
return view.mouseClicked(pointInView, mouseButton)
result = view.mouseClicked(pointInView, mouseButton)
}
for (v in outside) {
val pointInV = convert(point, to = v)
v.mouseClickedOutside(pointInV, mouseButton)
}
return result
}
open fun mouseClickedOutside(point: Point, mouseButton: MouseButton) {
for (view in subviews) {
val pointInView = convert(point, to = view)
view.mouseClickedOutside(pointInView, mouseButton)
}
return false
}
open fun mouseDragged(startPoint: Point, delta: Point, mouseButton: MouseButton): Boolean {

View File

@ -0,0 +1,128 @@
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
/**
* @author shadowfacts
*/
abstract class AbstractTextField<Impl: AbstractTextField<Impl>>(
initialText: String
): View() {
var handler: ((Impl) -> Unit)? = null
var disabled = false
val focused: Boolean
get() = isFirstResponder
var text: String
get() = minecraftWidget.text
set(value) {
minecraftWidget.text = value
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
}
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?
class ProxyWidget: TextFieldWidget(MinecraftClient.getInstance().textRenderer, 0, 0, 0, 0, LiteralText("")) {
// AbstractButtonWidget.height is protected
fun setHeight(height: Int) {
this.height = height
}
}
}

View File

@ -0,0 +1,14 @@
package net.shadowfacts.cacao.view.textfield
/**
* @author shadowfacts
*/
class TextField(initialText: String): AbstractTextField<TextField>(initialText) {
constructor(initialText: String, handler: (TextField) -> Unit): this(initialText) {
this.handler = handler
}
override fun validate(proposedText: String): Boolean {
return true
}
}

View File

@ -3,6 +3,7 @@ package net.shadowfacts.cacao.window
import net.minecraft.client.util.math.MatrixStack
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.view.View
@ -73,6 +74,13 @@ open class Window(
*/
val centerYAnchor = Variable("centerY")
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

View File

@ -13,6 +13,7 @@ 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
@ -23,70 +24,78 @@ import net.shadowfacts.kiwidsl.dsl
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))
//
// 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
// }
//
// }
// }
// addWindow(Window(viewController))
val viewController = object: ViewController() {
override fun loadView() {
view = View()
}
override fun viewDidLoad() {
super.viewDidLoad()
val tabs = arrayOf(
Tab(Label("A"), AViewController()),
Tab(Label("B"), BViewController()),
)
val tabVC = TabViewController(tabs)
embedChild(tabVC, pinEdges = false)
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 {
tabVC.view.centerXAnchor equalTo view.centerXAnchor
tabVC.view.centerYAnchor equalTo view.centerYAnchor
tabVC.view.widthAnchor equalTo 200
tabVC.view.heightAnchor equalTo 150
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 = arrayOf(
// Tab(Label("A"), AViewController()),
// Tab(Label("B"), BViewController()),
// )
// 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(