PhysicalConnectivity/src/main/kotlin/net/shadowfacts/cacao/view/textfield/AbstractTextField.kt

177 lines
5.2 KiB
Kotlin

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.KeyModifiers
import net.shadowfacts.cacao.util.MouseButton
import net.shadowfacts.cacao.view.View
import net.shadowfacts.phycon.mixin.client.TextFieldWidgetAccessor
import org.lwjgl.glfw.GLFW
/**
* 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)
}
/**
* Whether the Minecraft builtin black background and border are drawn. Defaults to true.
*/
var drawBackground = true
set(value) {
field = value
minecraftWidget.setDrawsBackground(value)
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setDrawsBackground(drawBackground)
}
/**
* 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) {
matrixStack.push()
matrixStack.translate(-originInWindow.x, -originInWindow.y, 0.0)
val mouseXInWindow = (mouse.x + originInWindow.x).toInt()
val mouseYInWindow = (mouse.y + originInWindow.y).toInt()
minecraftWidget.render(matrixStack, mouseXInWindow, mouseYInWindow, delta)
matrixStack.pop()
}
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.setTextFieldFocused(true)
}
override fun didResignFirstResponder() {
super.didResignFirstResponder()
minecraftWidget.setTextFieldFocused(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 || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE)
}
fun tick() {
minecraftWidget.tick()
}
// 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
}
}
}