Text input selection test

This commit is contained in:
DBotThePony 2023-01-25 21:03:22 +07:00
parent 409c5bb443
commit b62ac72bc5
Signed by: DBot
GPG Key ID: DCC23B5715498507

View File

@ -1,10 +1,16 @@
package ru.dbotthepony.mc.otm.client.screen.panels package ru.dbotthepony.mc.otm.client.screen.panels
import com.mojang.blaze3d.platform.GlStateManager
import com.mojang.blaze3d.platform.InputConstants import com.mojang.blaze3d.platform.InputConstants
import com.mojang.blaze3d.systems.RenderSystem
import com.mojang.blaze3d.vertex.DefaultVertexFormat
import com.mojang.blaze3d.vertex.PoseStack import com.mojang.blaze3d.vertex.PoseStack
import com.mojang.blaze3d.vertex.VertexFormat
import it.unimi.dsi.fastutil.chars.CharOpenHashSet import it.unimi.dsi.fastutil.chars.CharOpenHashSet
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap import it.unimi.dsi.fastutil.ints.Int2ObjectAVLTreeMap
import it.unimi.dsi.fastutil.ints.Int2ObjectMap
import net.minecraft.client.gui.screens.Screen import net.minecraft.client.gui.screens.Screen
import net.minecraft.client.renderer.GameRenderer
import ru.dbotthepony.mc.otm.client.isCtrlDown import ru.dbotthepony.mc.otm.client.isCtrlDown
import ru.dbotthepony.mc.otm.client.isKeyDown import ru.dbotthepony.mc.otm.client.isKeyDown
import ru.dbotthepony.mc.otm.client.isShiftDown import ru.dbotthepony.mc.otm.client.isShiftDown
@ -12,6 +18,8 @@ import ru.dbotthepony.mc.otm.client.minecraft
import ru.dbotthepony.mc.otm.client.render.DynamicBufferSource import ru.dbotthepony.mc.otm.client.render.DynamicBufferSource
import ru.dbotthepony.mc.otm.client.render.TextAlign import ru.dbotthepony.mc.otm.client.render.TextAlign
import ru.dbotthepony.mc.otm.client.render.drawAligned import ru.dbotthepony.mc.otm.client.render.drawAligned
import ru.dbotthepony.mc.otm.client.render.tesselator
import ru.dbotthepony.mc.otm.core.addAll
import ru.dbotthepony.mc.otm.core.math.RGBAColor import ru.dbotthepony.mc.otm.core.math.RGBAColor
import ru.dbotthepony.mc.otm.milliTime import ru.dbotthepony.mc.otm.milliTime
@ -23,10 +31,39 @@ open class TextInputPanel<out S : Screen>(
width: Float = 10f, width: Float = 10f,
height: Float = 10f, height: Float = 10f,
) : EditablePanel<S>(screen, parent, x, y, width, height) { ) : EditablePanel<S>(screen, parent, x, y, width, height) {
data class TextSelection(val start: Int, val end: Int) { private data class TextSelection(val start: Int, val end: Int) {
init { val isInversed get() = start > end
require(start <= end) { "$start <= $end" } val isNotEmpty get() = start != end
require(start >= 0) { "$start >= 0" }
val actualStart get() = if (isInversed) this.end else this.start
val actualEnd get() = if (isInversed) this.start else this.end
fun sub(line: String): Pair<String, String> {
val before = line.substring(0, actualStart.coerceIn(0, line.length))
val selected = line.substring(actualStart.coerceIn(0, line.length), actualEnd.coerceAtMost(line.length))
return before to selected
}
fun coversEntireString(value: String?): Boolean {
if (value == null)
return false
return value.length <= actualEnd && actualStart <= 0
}
fun coversEntireLine(value: String?): Boolean {
if (value == null)
return true
return value.length < actualEnd && actualStart <= 0
}
fun coversNewline(value: String?): Boolean {
if (value == null)
return actualEnd == Int.MAX_VALUE
return value.length < actualEnd
} }
} }
@ -34,10 +71,13 @@ open class TextInputPanel<out S : Screen>(
private val lines = ArrayList(this@TextInputPanel.lines) // ultra fast copy private val lines = ArrayList(this@TextInputPanel.lines) // ultra fast copy
private val cursorLine = this@TextInputPanel.cursorLine private val cursorLine = this@TextInputPanel.cursorLine
private val cursorCharacter = this@TextInputPanel.cursorCharacter private val cursorCharacter = this@TextInputPanel.cursorCharacter
private val selections = Int2ObjectAVLTreeMap(this@TextInputPanel.selections)
fun apply() { fun apply() {
this@TextInputPanel.lines.clear() this@TextInputPanel.lines.clear()
this@TextInputPanel.lines.addAll(lines) this@TextInputPanel.lines.addAll(lines)
this@TextInputPanel.selections.clear()
this@TextInputPanel.selections.putAll(selections)
this@TextInputPanel.cursorCharacter = cursorCharacter this@TextInputPanel.cursorCharacter = cursorCharacter
this@TextInputPanel.cursorLine = cursorLine this@TextInputPanel.cursorLine = cursorLine
} }
@ -51,6 +91,7 @@ open class TextInputPanel<out S : Screen>(
if (lines != other.lines) return false if (lines != other.lines) return false
if (cursorLine != other.cursorLine) return false if (cursorLine != other.cursorLine) return false
if (cursorCharacter != other.cursorCharacter) return false if (cursorCharacter != other.cursorCharacter) return false
if (selections != other.selections) return false
return true return true
} }
@ -73,7 +114,7 @@ open class TextInputPanel<out S : Screen>(
private var textCache: String? = null private var textCache: String? = null
private val lines = ArrayList<String>() private val lines = ArrayList<String>()
private val selections = Int2ObjectOpenHashMap<TextSelection>() private val selections = Int2ObjectAVLTreeMap<TextSelection>()
private val undo = ArrayDeque<Snapshot>() private val undo = ArrayDeque<Snapshot>()
private val redo = ArrayDeque<Snapshot>() private val redo = ArrayDeque<Snapshot>()
@ -96,6 +137,11 @@ open class TextInputPanel<out S : Screen>(
undo.addLast(snapshot) undo.addLast(snapshot)
} }
private fun pushbackSnapshotIfNoTimer() {
if (snapshotTimer == null)
pushbackSnapshot()
}
private fun recordHistory(hard: Boolean = false) { private fun recordHistory(hard: Boolean = false) {
if (hard) { if (hard) {
pushbackSnapshot() pushbackSnapshot()
@ -173,6 +219,19 @@ open class TextInputPanel<out S : Screen>(
lines[index] = value lines[index] = value
} }
private fun selectionIndex(index: Int) = object : Int2ObjectMap.Entry<TextSelection> {
override fun setValue(newValue: TextSelection): TextSelection? {
return selections.put(index, newValue)
}
override fun getIntKey(): Int {
return index
}
override val value: TextSelection
get() = selections[index] ?: throw NoSuchElementException()
}
fun insertLine(index: Int, value: String = "") { fun insertLine(index: Int, value: String = "") {
lines.ensureCapacity(index) lines.ensureCapacity(index)
@ -180,7 +239,8 @@ open class TextInputPanel<out S : Screen>(
lines.add("") lines.add("")
} }
val upperLines = selections.int2ObjectEntrySet().filter { it.intKey >= index } val upperLines = ArrayList<Int2ObjectMap.Entry<TextSelection>>()
upperLines.addAll(selections.int2ObjectEntrySet().iterator(selectionIndex(index)))
for (entry in upperLines) { for (entry in upperLines) {
selections.remove(entry.intKey) selections.remove(entry.intKey)
@ -207,7 +267,9 @@ open class TextInputPanel<out S : Screen>(
lines.removeAt(index) lines.removeAt(index)
selections.remove(index) selections.remove(index)
val upperLines = selections.int2ObjectEntrySet().filter { it.intKey > index }
val upperLines = ArrayList<Int2ObjectMap.Entry<TextSelection>>()
upperLines.addAll(selections.int2ObjectEntrySet().iterator(selectionIndex(index)))
for (entry in upperLines) { for (entry in upperLines) {
selections.remove(entry.intKey) selections.remove(entry.intKey)
@ -220,7 +282,7 @@ open class TextInputPanel<out S : Screen>(
return true return true
} }
fun moveCursors(line: Int, character: Int, moveBy: Int) { protected fun moveCursors(line: Int, character: Int, moveBy: Int) {
@Suppress("name_shadowing") @Suppress("name_shadowing")
val moveBy = if (character + moveBy < 0) -character else moveBy val moveBy = if (character + moveBy < 0) -character else moveBy
@ -252,9 +314,24 @@ open class TextInputPanel<out S : Screen>(
} }
} }
fun wipeSelection() { protected fun wipeSelection() {
if (selections.isEmpty())
return
pushbackSnapshotIfNoTimer()
val inversed = ArrayList(selections.int2ObjectEntrySet()) val inversed = ArrayList(selections.int2ObjectEntrySet())
inversed.sortByDescending { it.intKey }
var downTo = inversed.size.ushr(1)
if (inversed.size and 1 == 1) downTo++
for (i in inversed.size - 1 downTo downTo) {
val i2 = inversed.size - i
val a = inversed[i]
val b = inversed[i2]
inversed[i] = b
inversed[i2] = a
}
for ((lineNumber, selection) in inversed) { for ((lineNumber, selection) in inversed) {
if (selection.start != selection.end) { if (selection.start != selection.end) {
@ -264,7 +341,7 @@ open class TextInputPanel<out S : Screen>(
} }
} }
selections.clear() // selections.clear()
} }
protected open fun textChanged(oldText: String, newText: String) {} protected open fun textChanged(oldText: String, newText: String) {}
@ -294,18 +371,38 @@ open class TextInputPanel<out S : Screen>(
lines.addAll(value.split(NEWLINES)) lines.addAll(value.split(NEWLINES))
} }
fun advanceCursorLeft() { data class CursorAdvanceResult(
val oldLine: Int,
val newLine: Int,
val oldCharacter: Int,
val newCharacter: Int,
val couldHaveChangedLine: Boolean
) {
val linesChanged get() = oldLine != newLine
val charsChanged get() = linesChanged || oldCharacter != newCharacter
val advancedChars get() = newCharacter - oldCharacter
}
fun advanceCursorLeft(greedy: Boolean = false): CursorAdvanceResult {
val oldLine = cursorLine
val oldChar = cursorCharacter
val line = this[cursorLine] val line = this[cursorLine]
var couldHaveChangedLine = false
if (line != null && cursorCharacter > line.length) { if (line != null && cursorCharacter > line.length) {
cursorCharacter = line.length - 1 cursorCharacter = line.length - 1
} }
if (cursorCharacter > 0) { if (cursorCharacter > 0) {
cursorCharacter-- if (greedy && line != null) {
cursorCharacter = greedyAdvanceLeft(line, cursorCharacter)
} else {
cursorCharacter--
}
} else if (cursorLine > 0) { } else if (cursorLine > 0) {
cursorLine-- cursorLine--
cursorCharacter = 0 cursorCharacter = 0
couldHaveChangedLine = true
@Suppress("name_shadowing") @Suppress("name_shadowing")
val line = this[cursorLine] val line = this[cursorLine]
@ -313,15 +410,29 @@ open class TextInputPanel<out S : Screen>(
if (line != null) { if (line != null) {
cursorCharacter = line.length cursorCharacter = line.length
} }
} else {
couldHaveChangedLine = true
} }
return CursorAdvanceResult(oldLine, cursorLine, oldChar, cursorCharacter, couldHaveChangedLine = couldHaveChangedLine)
} }
fun advanceCursorRight() { fun advanceCursorRight(greedy: Boolean = false): CursorAdvanceResult {
cursorCharacter++ val oldLine = cursorLine
val oldChar = cursorCharacter
var couldHaveChangedLine = false
val line = this[cursorLine] val line = this[cursorLine]
if (greedy && line != null && cursorCharacter + 1 < line.length) {
cursorCharacter = greedyAdvanceRight(line, cursorCharacter)
} else {
cursorCharacter++
}
if (line != null && cursorCharacter > line.length) { if (line != null && cursorCharacter > line.length) {
couldHaveChangedLine = true
if (lines.size <= cursorLine + 1) { if (lines.size <= cursorLine + 1) {
cursorCharacter = line.length cursorCharacter = line.length
} else { } else {
@ -331,6 +442,8 @@ open class TextInputPanel<out S : Screen>(
} else if (line == null) { } else if (line == null) {
cursorCharacter = 0 cursorCharacter = 0
} }
return CursorAdvanceResult(oldLine, cursorLine, oldChar, cursorCharacter, couldHaveChangedLine = couldHaveChangedLine)
} }
override fun keyPressedInternal(key: Int, scancode: Int, mods: Int): Boolean { override fun keyPressedInternal(key: Int, scancode: Int, mods: Int): Boolean {
@ -394,7 +507,6 @@ open class TextInputPanel<out S : Screen>(
if (multiLine) { if (multiLine) {
val lines = if (selections.isNotEmpty()) selections.keys else listOf(cursorLine) val lines = if (selections.isNotEmpty()) selections.keys else listOf(cursorLine)
if (minecraft.window.isKeyDown(InputConstants.KEY_RSHIFT) || minecraft.window.isKeyDown(InputConstants.KEY_LSHIFT)) { if (minecraft.window.isKeyDown(InputConstants.KEY_RSHIFT) || minecraft.window.isKeyDown(InputConstants.KEY_LSHIFT)) {
var hit = false var hit = false
@ -429,36 +541,54 @@ open class TextInputPanel<out S : Screen>(
} }
if (key == InputConstants.KEY_LEFT) { if (key == InputConstants.KEY_LEFT) {
if (minecraft.window.isCtrlDown) { if (!minecraft.window.isShiftDown) {
val line = this[cursorLine] advanceCursorLeft(minecraft.window.isCtrlDown)
selections.clear()
if (line == null || cursorCharacter <= 0) {
advanceCursorLeft()
} else {
if (cursorCharacter >= line.length)
cursorCharacter = line.length - 1
cursorCharacter = greedyAdvanceLeft(line, cursorCharacter)
}
} else { } else {
advanceCursorLeft() if (this[cursorLine] == null)
return true
val existing = selections[cursorLine]
val result = advanceCursorLeft(minecraft.window.isCtrlDown)
if (!result.linesChanged)
selections[result.oldLine] = TextSelection(
existing?.start ?: result.oldCharacter,
result.newCharacter)
else {
this[result.newLine]?.let {
val existingNewline = selections[result.newLine]
if (existingNewline == null) {
selections[result.newLine] = TextSelection(
Int.MAX_VALUE,
it.length)
} else {
if (!existingNewline.isInversed) {
selections[result.newLine] = TextSelection(
existingNewline.start,
it.length)
}
}
}
}
} }
return true return true
} else if (key == InputConstants.KEY_RIGHT) { } else if (key == InputConstants.KEY_RIGHT) {
if (minecraft.window.isCtrlDown) { if (!minecraft.window.isShiftDown) {
val line = this[cursorLine] advanceCursorRight(minecraft.window.isCtrlDown)
selections.clear()
if (line == null || cursorCharacter + 1 >= line.length) {
advanceCursorRight()
} else {
if (cursorCharacter < 0)
cursorCharacter = 0
cursorCharacter = greedyAdvanceRight(line, cursorCharacter)
}
} else { } else {
advanceCursorRight() if (this[cursorLine] == null)
return true
val existing = selections[cursorLine]
val result = advanceCursorRight(minecraft.window.isCtrlDown)
selections[result.oldLine] = TextSelection(
existing?.start ?: result.oldCharacter,
if (result.couldHaveChangedLine) Int.MAX_VALUE else result.newCharacter)
} }
return true return true
@ -467,23 +597,30 @@ open class TextInputPanel<out S : Screen>(
cursorLine-- cursorLine--
} }
selections.clear()
return true return true
} else if (key == InputConstants.KEY_DOWN) { } else if (key == InputConstants.KEY_DOWN) {
if (cursorLine < lines.size - 1) { if (cursorLine < lines.size - 1) {
cursorLine++ cursorLine++
} }
selections.clear()
return true return true
} }
if (key == InputConstants.KEY_BACKSPACE) { if (key == InputConstants.KEY_BACKSPACE) {
wipeSelection()
val line = this[cursorLine]
if (cursorLine <= 0 && cursorCharacter <= 0) { if (cursorLine <= 0 && cursorCharacter <= 0) {
return true return true
} else if (cursorCharacter <= 0) { } else if (cursorCharacter <= 0 || line?.length == 0) {
val line = this[cursorLine]
if (line == null) { if (line == null) {
cursorCharacter = this[--cursorLine]?.length ?: 0 cursorCharacter = this[--cursorLine]?.length ?: 0
recordHistory()
} else { } else {
removeLine(cursorLine) removeLine(cursorLine)
val newLine = this[cursorLine] val newLine = this[cursorLine]
@ -497,17 +634,23 @@ open class TextInputPanel<out S : Screen>(
recordHistory() recordHistory()
} }
} else { } else {
val line = this[cursorLine]
if (line != null) { if (line != null) {
var cursorCharacter = cursorCharacter pushbackSnapshotIfNoTimer()
if (cursorCharacter >= line.length) // remove from very end
cursorCharacter = line.length - 1 if (cursorCharacter >= line.length) {
val newLine = line.substring(0, line.length - 1)
val newLine = line.substring(0, cursorCharacter - 1) + line.substring(cursorCharacter) moveCursors(cursorLine, cursorCharacter, -1)
moveCursors(cursorLine, cursorCharacter, -1) this[cursorLine] = newLine
this[cursorLine] = newLine recordHistory()
} else {
val newLine = line.substring(0, cursorCharacter - 1) + line.substring(cursorCharacter)
moveCursors(cursorLine, cursorCharacter, -1)
this[cursorLine] = newLine
recordHistory()
}
} else if (cursorLine > 0) {
cursorLine--
recordHistory() recordHistory()
} }
} }
@ -516,6 +659,8 @@ open class TextInputPanel<out S : Screen>(
} }
if (key == InputConstants.KEY_DELETE) { if (key == InputConstants.KEY_DELETE) {
wipeSelection()
if (cursorLine !in 0 until lines.size) { if (cursorLine !in 0 until lines.size) {
cursorLine = lines.size - 1 cursorLine = lines.size - 1
return true return true
@ -540,8 +685,7 @@ open class TextInputPanel<out S : Screen>(
return true return true
} }
if (snapshotTimer == null) pushbackSnapshotIfNoTimer()
pushbackSnapshot()
val bottomLine = this[cursorLine + 1]!! val bottomLine = this[cursorLine + 1]!!
cursorCharacter = line.length cursorCharacter = line.length
@ -550,8 +694,7 @@ open class TextInputPanel<out S : Screen>(
recordHistory() recordHistory()
} else { } else {
if (snapshotTimer == null) pushbackSnapshotIfNoTimer()
pushbackSnapshot()
val cursorCharacter = cursorCharacter val cursorCharacter = cursorCharacter
moveCursors(cursorLine, cursorCharacter, -1) moveCursors(cursorLine, cursorCharacter, -1)
@ -572,10 +715,19 @@ open class TextInputPanel<out S : Screen>(
redo() redo()
} }
if (key == InputConstants.KEY_A && minecraft.window.isCtrlDown) {
selections.clear()
for ((i, line) in lines.withIndex()) {
selections[i] = TextSelection(0, Int.MAX_VALUE)
}
}
return true return true
} }
override fun charTypedInternal(codepoint: Char, mods: Int): Boolean { override fun charTypedInternal(codepoint: Char, mods: Int): Boolean {
wipeSelection()
var line = this[cursorLine] var line = this[cursorLine]
if (line == null) { if (line == null) {
@ -589,8 +741,7 @@ open class TextInputPanel<out S : Screen>(
else else
line = line.substring(0, cursorCharacter) + codepoint + line.substring(cursorCharacter) line = line.substring(0, cursorCharacter) + codepoint + line.substring(cursorCharacter)
if (snapshotTimer == null) pushbackSnapshotIfNoTimer()
pushbackSnapshot()
set(cursorLine, line) set(cursorLine, line)
moveCursors(cursorLine, cursorCharacter, 1) moveCursors(cursorLine, cursorCharacter, 1)
@ -602,7 +753,9 @@ open class TextInputPanel<out S : Screen>(
override fun innerRender(stack: PoseStack, mouseX: Float, mouseY: Float, partialTick: Float) { override fun innerRender(stack: PoseStack, mouseX: Float, mouseY: Float, partialTick: Float) {
var y = 0f var y = 0f
for (line in lines) { for ((i, line) in lines.withIndex()) {
val selection = selections[i]
font.drawAligned( font.drawAligned(
poseStack = stack, poseStack = stack,
buffer = BUFFER, buffer = BUFFER,
@ -613,6 +766,42 @@ open class TextInputPanel<out S : Screen>(
color = textColor color = textColor
) )
if (selection != null && (selection.isNotEmpty || selection.coversEntireLine(line))) {
val (before, selected) = selection.sub(line)
var x = 0f
if (before.isNotEmpty()) {
x = font.width(before).toFloat()
}
val width = if (selection.coversNewline(line)) this.width - x else font.width(selected).toFloat()
RenderSystem.setShader(GameRenderer::getPositionShader)
RenderSystem.setShaderColor(0.0f, 0.0f, 1.0f, 1.0f)
RenderSystem.disableTexture()
RenderSystem.enableColorLogicOp()
RenderSystem.logicOp(GlStateManager.LogicOp.OR_REVERSE)
RenderSystem.disableDepthTest()
RenderSystem.defaultBlendFunc()
val builder = tesselator.builder
builder.begin(VertexFormat.Mode.QUADS, DefaultVertexFormat.POSITION)
builder.vertex(stack.last().pose(), x, y + font.lineHeight + 2f, 0f).endVertex()
builder.vertex(stack.last().pose(), x + width, y + font.lineHeight + 2f, 0f).endVertex()
builder.vertex(stack.last().pose(), x + width, y, 0f).endVertex()
builder.vertex(stack.last().pose(), x, y, 0f).endVertex()
tesselator.end()
RenderSystem.setShaderColor(1.0f, 1.0f, 1.0f, 1.0f)
RenderSystem.disableColorLogicOp()
RenderSystem.enableTexture()
RenderSystem.enableDepthTest()
}
y += font.lineHeight + 2f y += font.lineHeight + 2f
} }
@ -678,6 +867,7 @@ open class TextInputPanel<out S : Screen>(
} }
override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean { override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
selections.clear()
requestFocus() requestFocus()
return true return true
} }
@ -745,7 +935,7 @@ open class TextInputPanel<out S : Screen>(
private fun greedyAdvanceLeft(input: String, position: Int): Int { private fun greedyAdvanceLeft(input: String, position: Int): Int {
if (position <= 1) if (position <= 1)
return 0 return -1
@Suppress("name_shadowing") @Suppress("name_shadowing")
var position = position var position = position
@ -768,6 +958,12 @@ open class TextInputPanel<out S : Screen>(
private fun greedyAdvanceRight(input: String, position: Int): Int { private fun greedyAdvanceRight(input: String, position: Int): Int {
@Suppress("name_shadowing") @Suppress("name_shadowing")
var position = position var position = position
if (position < 0)
position = 0
else if (position >= input.length)
return position + 1
var type = CharType[input[position]] var type = CharType[input[position]]
while (position < input.length) { while (position < input.length) {