177 lines
5.2 KiB
Kotlin
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
|
|
}
|
|
}
|
|
|
|
}
|