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.sound.SoundEvents
import net.minecraft.text.LiteralText import net.minecraft.text.LiteralText
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.util.RenderHelper import net.shadowfacts.cacao.util.RenderHelper
import net.shadowfacts.cacao.window.Window import net.shadowfacts.cacao.window.Window
import org.lwjgl.glfw.GLFW
import java.util.* import java.util.*
/** /**
@ -55,6 +57,7 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
*/ */
override fun removeWindow(window: Window) { override fun removeWindow(window: Window) {
_windows.remove(window) _windows.remove(window)
// todo: VC callbacks
} }
override fun init() { 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) { override fun render(matrixStack: MatrixStack, mouseX: Int, mouseY: Int, delta: Float) {
if (client != null) { if (client != null) {
// workaround this.minecraft sometimes being null causing a crash // workaround this.minecraft sometimes being null causing a crash
@ -102,4 +116,35 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
return result == true 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.minecraft.client.util.math.MatrixStack
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
import net.shadowfacts.cacao.LayoutVariable import net.shadowfacts.cacao.LayoutVariable
import net.shadowfacts.cacao.Responder
import net.shadowfacts.cacao.window.Window import net.shadowfacts.cacao.window.Window
import net.shadowfacts.cacao.geometry.* import net.shadowfacts.cacao.geometry.*
import net.shadowfacts.cacao.util.* import net.shadowfacts.cacao.util.*
@ -19,14 +20,18 @@ import kotlin.collections.HashSet
* *
* @author shadowfacts * @author shadowfacts
*/ */
open class View() { open class View(): Responder {
/** /**
* The window whose view hierarchy this view belongs to. * The window whose view hierarchy this view belongs to.
* Not initialized until the root view in this hierarchy has been added to a hierarchy, * Not initialized until the root view in this hierarchy has been added to a hierarchy,
* using it before that will throw a runtime exception. * 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> { private val solverDelegate = ObservableLateInitProperty<Solver> {
for (v in subviews) { for (v in subviews) {
@ -354,12 +359,25 @@ open class View() {
* @return Whether the mouse click was handled by this view or any subviews. * @return Whether the mouse click was handled by this view or any subviews.
*/ */
open fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { 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) { if (view != null) {
val pointInView = convert(point, to = view) 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 { 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.minecraft.client.util.math.MatrixStack
import net.shadowfacts.cacao.AbstractCacaoScreen import net.shadowfacts.cacao.AbstractCacaoScreen
import net.shadowfacts.cacao.CacaoScreen import net.shadowfacts.cacao.CacaoScreen
import net.shadowfacts.cacao.Responder
import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.geometry.Point
import net.shadowfacts.cacao.util.MouseButton import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View import net.shadowfacts.cacao.view.View
@ -73,6 +74,13 @@ open class Window(
*/ */
val centerYAnchor = Variable("centerY") 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 // 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 // stored so that they can be removed when the screen is resized
private var widthConstraint: Constraint? = null 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.util.texture.Texture
import net.shadowfacts.cacao.view.* import net.shadowfacts.cacao.view.*
import net.shadowfacts.cacao.view.button.Button 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.TabViewController
import net.shadowfacts.cacao.viewcontroller.ViewController import net.shadowfacts.cacao.viewcontroller.ViewController
import net.shadowfacts.kiwidsl.dsl import net.shadowfacts.kiwidsl.dsl
@ -23,70 +24,78 @@ import net.shadowfacts.kiwidsl.dsl
class TestCacaoScreen: CacaoScreen() { class TestCacaoScreen: CacaoScreen() {
init { 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() { val viewController = object: ViewController() {
override fun loadView() {
view = View()
}
override fun viewDidLoad() { override fun viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
val tabs = arrayOf( val stack = view.addSubview(StackView(Axis.VERTICAL, StackView.Distribution.CENTER, spacing = 4.0)).apply {
Tab(Label("A"), AViewController()), backgroundColor = Color.WHITE
Tab(Label("B"), BViewController()), }
) val birch = stack.addArrangedSubview(TextureView(Texture(Identifier("textures/block/birch_log_top.png"), 0, 0, 16, 16))).apply {
val tabVC = TabViewController(tabs) intrinsicContentSize = Size(50.0, 50.0)
embedChild(tabVC, pinEdges = false) }
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 { view.solver.dsl {
tabVC.view.centerXAnchor equalTo view.centerXAnchor stack.topAnchor equalTo 0
tabVC.view.centerYAnchor equalTo view.centerYAnchor stack.centerXAnchor equalTo window!!.centerXAnchor
tabVC.view.widthAnchor equalTo 200 stack.widthAnchor equalTo 100
tabVC.view.heightAnchor equalTo 150
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)) 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( data class Tab(