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

180 lines
5.3 KiB
Kotlin

package net.shadowfacts.cacao.view.textfield
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.screen.TickableElement
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
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(), TickableElement {
/**
* 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.setHasBorder(value)
}
private lateinit var originInWindow: Point
private var minecraftWidget = ProxyWidget()
init {
minecraftWidget.text = initialText
minecraftWidget.setTextPredicate { this.validate(it) }
minecraftWidget.setHasBorder(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) {
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 || (isFirstResponder && keyCode != GLFW.GLFW_KEY_ESCAPE)
}
override 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
}
}
}