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>( 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 } } }