Cacao: Add text field
This commit is contained in:
parent
277bcb71ee
commit
9e3366cbfb
|
@ -6,9 +6,11 @@ import net.minecraft.client.util.math.MatrixStack
|
|||
import net.minecraft.sound.SoundEvents
|
||||
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.util.RenderHelper
|
||||
import net.shadowfacts.cacao.window.Window
|
||||
import org.lwjgl.glfw.GLFW
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
|
@ -55,6 +57,7 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
|
|||
*/
|
||||
override fun removeWindow(window: Window) {
|
||||
_windows.remove(window)
|
||||
// todo: VC callbacks
|
||||
}
|
||||
|
||||
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) {
|
||||
if (client != null) {
|
||||
// workaround this.minecraft sometimes being null causing a crash
|
||||
|
@ -102,4 +116,35 @@ open class CacaoScreen: Screen(LiteralText("CacaoScreen")), AbstractCacaoScreen
|
|||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -3,6 +3,7 @@ package net.shadowfacts.cacao.view
|
|||
import net.minecraft.client.util.math.MatrixStack
|
||||
import net.shadowfacts.kiwidsl.dsl
|
||||
import net.shadowfacts.cacao.LayoutVariable
|
||||
import net.shadowfacts.cacao.Responder
|
||||
import net.shadowfacts.cacao.window.Window
|
||||
import net.shadowfacts.cacao.geometry.*
|
||||
import net.shadowfacts.cacao.util.*
|
||||
|
@ -19,14 +20,18 @@ import kotlin.collections.HashSet
|
|||
*
|
||||
* @author shadowfacts
|
||||
*/
|
||||
open class View() {
|
||||
open class View(): Responder {
|
||||
|
||||
/**
|
||||
* The window whose view hierarchy this view belongs to.
|
||||
* Not initialized until the root view in this hierarchy has been added to a hierarchy,
|
||||
* 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> {
|
||||
for (v in subviews) {
|
||||
|
@ -354,12 +359,25 @@ open class View() {
|
|||
* @return Whether the mouse click was handled by this view or any subviews.
|
||||
*/
|
||||
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) {
|
||||
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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package net.shadowfacts.cacao.window
|
|||
import net.minecraft.client.util.math.MatrixStack
|
||||
import net.shadowfacts.cacao.AbstractCacaoScreen
|
||||
import net.shadowfacts.cacao.CacaoScreen
|
||||
import net.shadowfacts.cacao.Responder
|
||||
import net.shadowfacts.cacao.geometry.Point
|
||||
import net.shadowfacts.cacao.util.MouseButton
|
||||
import net.shadowfacts.cacao.view.View
|
||||
|
@ -73,6 +74,13 @@ open class Window(
|
|||
*/
|
||||
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
|
||||
// stored so that they can be removed when the screen is resized
|
||||
private var widthConstraint: Constraint? = null
|
||||
|
|
|
@ -13,6 +13,7 @@ import net.shadowfacts.cacao.util.texture.NinePatchTexture
|
|||
import net.shadowfacts.cacao.util.texture.Texture
|
||||
import net.shadowfacts.cacao.view.*
|
||||
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.ViewController
|
||||
import net.shadowfacts.kiwidsl.dsl
|
||||
|
@ -23,70 +24,78 @@ import net.shadowfacts.kiwidsl.dsl
|
|||
class TestCacaoScreen: CacaoScreen() {
|
||||
|
||||
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() {
|
||||
override fun loadView() {
|
||||
view = View()
|
||||
}
|
||||
|
||||
override fun viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
val tabs = arrayOf(
|
||||
Tab(Label("A"), AViewController()),
|
||||
Tab(Label("B"), BViewController()),
|
||||
)
|
||||
val tabVC = TabViewController(tabs)
|
||||
embedChild(tabVC, pinEdges = false)
|
||||
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))
|
||||
|
||||
val field = TextField("Test") {
|
||||
println("new value: ${it.text}")
|
||||
}
|
||||
stack.addArrangedSubview(field)
|
||||
|
||||
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
|
||||
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
|
||||
|
||||
field.widthAnchor equalTo stack.widthAnchor
|
||||
field.heightAnchor equalTo 20
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
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(
|
||||
|
|
Loading…
Reference in New Issue