Visual and audio feedback when trying to type stuff out of bounds

Add creative holo sign bounds
This commit is contained in:
DBotThePony 2025-01-18 13:45:00 +07:00
parent 082f1478f5
commit bc9a78c514
Signed by: DBot
GPG Key ID: DCC23B5715498507
5 changed files with 144 additions and 48 deletions

View File

@ -30,7 +30,7 @@ class HoloSignBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryB
override val redstoneControl = SynchronizedRedstoneControl(syncher) { _, _ -> setChanged() } override val redstoneControl = SynchronizedRedstoneControl(syncher) { _, _ -> setChanged() }
var signText by syncher.string("", setter = { access, value -> var signText by syncher.string("", setter = { access, value ->
setChanged() markDirtyFast()
access.accept(value) access.accept(value)
}).delegate }).delegate
@ -96,24 +96,25 @@ class HoloSignBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryB
override fun loadAdditional(nbt: CompoundTag, registry: HolderLookup.Provider) { override fun loadAdditional(nbt: CompoundTag, registry: HolderLookup.Provider) {
super.loadAdditional(nbt, registry) super.loadAdditional(nbt, registry)
signText = truncate(signText, isLocked)
if (!isLocked) {
signText = truncate(signText)
}
} }
companion object { companion object {
const val DEFAULT_MAX_NEWLINES = 10 const val DEFAULT_MAX_NEWLINES = 10
const val DEFAULT_MAX_LINE_LENGTH = 60 const val DEFAULT_MAX_LINE_LENGTH = 60
const val CREATIVE_MAX_NEWLINES = 60
const val CREATIVE_MAX_LINE_LENGTH = 240
private val NEWLINES = Regex("\r?\n") private val NEWLINES = Regex("\r?\n")
fun truncate(input: String): String { fun truncate(input: String, isLocked: Boolean): String {
val maxLines = if (isLocked) CREATIVE_MAX_NEWLINES else DEFAULT_MAX_NEWLINES
val maxLength = if (isLocked) CREATIVE_MAX_LINE_LENGTH else DEFAULT_MAX_LINE_LENGTH
val lines = input.split(NEWLINES) val lines = input.split(NEWLINES)
val result = ArrayList<String>(lines.size.coerceAtMost(DEFAULT_MAX_NEWLINES)) val result = ArrayList<String>(lines.size.coerceAtMost(maxLines))
for (i in 0 until lines.size.coerceAtMost(DEFAULT_MAX_NEWLINES)) { for (i in 0 until lines.size.coerceAtMost(maxLines)) {
if (lines[i].length > DEFAULT_MAX_LINE_LENGTH) { if (lines[i].length > maxLength) {
result.add(lines[i].substring(0, DEFAULT_MAX_LINE_LENGTH)) result.add(lines[i].substring(0, maxLength))
} else { } else {
result.add(lines[i]) result.add(lines[i])
} }

View File

@ -10,6 +10,7 @@ import ru.dbotthepony.mc.otm.client.screen.panels.button.makeDeviceControls
import ru.dbotthepony.mc.otm.client.screen.panels.input.NetworkedStringInputPanel import ru.dbotthepony.mc.otm.client.screen.panels.input.NetworkedStringInputPanel
import ru.dbotthepony.mc.otm.core.TranslatableComponent import ru.dbotthepony.mc.otm.core.TranslatableComponent
import ru.dbotthepony.kommons.math.RGBAColor import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.mc.otm.block.entity.decorative.HoloSignBlockEntity
import ru.dbotthepony.mc.otm.client.render.Widgets18 import ru.dbotthepony.mc.otm.client.render.Widgets18
import ru.dbotthepony.mc.otm.client.screen.panels.button.BooleanButtonPanel import ru.dbotthepony.mc.otm.client.screen.panels.button.BooleanButtonPanel
import ru.dbotthepony.mc.otm.client.screen.panels.button.ButtonPanel import ru.dbotthepony.mc.otm.client.screen.panels.button.ButtonPanel
@ -17,7 +18,7 @@ import ru.dbotthepony.mc.otm.menu.decorative.HoloSignMenu
class HoloSignScreen(menu: HoloSignMenu, inventory: Inventory, title: Component) : MatteryScreen<HoloSignMenu>(menu, title) { class HoloSignScreen(menu: HoloSignMenu, inventory: Inventory, title: Component) : MatteryScreen<HoloSignMenu>(menu, title) {
override fun makeMainFrame(): FramePanel<MatteryScreen<*>> { override fun makeMainFrame(): FramePanel<MatteryScreen<*>> {
val frame = FramePanel(this, null, 0f, 0f, 200f, 200f, getTitle()) val frame = FramePanel(this, null, 0f, 0f, minecraft!!.window.guiScaledWidth * 0.8f, minecraft!!.window.guiScaledHeight * 0.8f, getTitle())
frame.makeCloseButton() frame.makeCloseButton()
frame.onClose { onClose() } frame.onClose { onClose() }
@ -25,9 +26,19 @@ class HoloSignScreen(menu: HoloSignMenu, inventory: Inventory, title: Component)
tooltips.add(TranslatableComponent("otm.gui.lock_holo_screen.tip")) tooltips.add(TranslatableComponent("otm.gui.lock_holo_screen.tip"))
} }
val input = NetworkedStringInputPanel(this, frame, backend = menu.text) object : NetworkedStringInputPanel<HoloSignScreen>(this@HoloSignScreen, frame, backend = menu.text) {
input.dock = Dock.FILL init {
input.isMultiLine = true dock = Dock.FILL
isMultiLine = true
}
override var maxLineLength: Int
get() = if (menu.locked.value) HoloSignBlockEntity.CREATIVE_MAX_LINE_LENGTH else HoloSignBlockEntity.DEFAULT_MAX_LINE_LENGTH
set(value) {}
override var maxLines: Int
get() = if (menu.locked.value) HoloSignBlockEntity.CREATIVE_MAX_NEWLINES else HoloSignBlockEntity.DEFAULT_MAX_NEWLINES
set(value) {}
}
val controls = makeDeviceControls(this, frame, redstoneConfig = menu.redstone) val controls = makeDeviceControls(this, frame, redstoneConfig = menu.redstone)

View File

@ -616,7 +616,7 @@ open class ColorPickerPanel<out S : Screen>(
} }
override fun acceptsCharacter(codepoint: Char, mods: Int, index: Int): Boolean { override fun acceptsCharacter(codepoint: Char, mods: Int, index: Int): Boolean {
return RGBAColor.isHexCharacter(codepoint) return super.acceptsCharacter(codepoint, mods, index) && RGBAColor.isHexCharacter(codepoint)
} }
} }

View File

@ -33,6 +33,7 @@ import ru.dbotthepony.kommons.math.RGBAColor
import ru.dbotthepony.mc.otm.client.render.vertex import ru.dbotthepony.mc.otm.client.render.vertex
import ru.dbotthepony.mc.otm.milliTime import ru.dbotthepony.mc.otm.milliTime
import java.util.function.Predicate import java.util.function.Predicate
import kotlin.math.abs
import kotlin.math.roundToInt import kotlin.math.roundToInt
open class TextInputPanel<out S : Screen>( open class TextInputPanel<out S : Screen>(
@ -87,7 +88,7 @@ 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.cursorRow private val cursorCharacter = this@TextInputPanel.cursorRow
private val selections = Int2ObjectAVLTreeMap(this@TextInputPanel.selections) private val selections = this@TextInputPanel.selections.clone()
private val multiLine = this@TextInputPanel.isMultiLine private val multiLine = this@TextInputPanel.isMultiLine
fun apply() { fun apply() {
@ -155,7 +156,19 @@ open class TextInputPanel<out S : Screen>(
open var isActive = true open var isActive = true
override val cursorType: CursorType override val cursorType: CursorType
get() = if (isActive) CursorType.BEAM else CursorType.NOT_ALLOWED get() = CursorType.BEAM
open var maxLineLength = Int.MAX_VALUE
set(value) {
require(value in 1 .. Int.MAX_VALUE) { "Max line length value is out of bounds: $value" }
field = value
}
open var maxLines = Int.MAX_VALUE
set(value) {
require(value in 1 .. Int.MAX_VALUE) { "Max lines value is out of bounds: $value" }
field = value
}
init { init {
scissor = true scissor = true
@ -463,8 +476,6 @@ open class TextInputPanel<out S : Screen>(
lines.clear() lines.clear()
redo.clear() redo.clear()
undo.clear() undo.clear()
cursorLine = 0
cursorRow = 0
textCache = null textCache = null
if (isMultiLine) { if (isMultiLine) {
@ -472,6 +483,9 @@ open class TextInputPanel<out S : Screen>(
} else { } else {
lines.add(value.replace(NEWLINES, "")) lines.add(value.replace(NEWLINES, ""))
} }
cursorLine = minOf(cursorLine, lines.size - 1)
cursorRow = minOf(this[cursorLine]?.length ?: 0, cursorRow)
} }
data class CursorAdvanceResult( data class CursorAdvanceResult(
@ -675,6 +689,12 @@ open class TextInputPanel<out S : Screen>(
if (key == InputConstants.KEY_RETURN) { if (key == InputConstants.KEY_RETURN) {
if (isMultiLine) { if (isMultiLine) {
if (lines.size >= maxLines) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
return true
}
if (!minecraft.window.isShiftDown && !minecraft.window.isCtrlDown) if (!minecraft.window.isShiftDown && !minecraft.window.isCtrlDown)
wipeSelection() wipeSelection()
@ -729,26 +749,34 @@ open class TextInputPanel<out S : Screen>(
} }
if (key == InputConstants.KEY_LEFT) { if (key == InputConstants.KEY_LEFT) {
if (!minecraft.window.isShiftDown) { if (minecraft.window.isShiftDown) {
advanceCursorLeft(minecraft.window.isCtrlDown)
selections.clear()
} else {
if (this[cursorLine] == null) if (this[cursorLine] == null)
return true return true
simulateSelectLeft(minecraft.window.isCtrlDown) simulateSelectLeft(minecraft.window.isCtrlDown)
} else if (selections.isNotEmpty()) {
val (index, selection) = selections.firstEntry()
cursorLine = index
cursorRow = selection.cursor
selections.clear()
} else {
advanceCursorLeft(minecraft.window.isCtrlDown)
} }
return true return true
} else if (key == InputConstants.KEY_RIGHT) { } else if (key == InputConstants.KEY_RIGHT) {
if (!minecraft.window.isShiftDown) { if (minecraft.window.isShiftDown) {
advanceCursorRight(minecraft.window.isCtrlDown)
selections.clear()
} else {
if (this[cursorLine] == null) if (this[cursorLine] == null)
return true return true
simulateSelectRight(minecraft.window.isCtrlDown) simulateSelectRight(minecraft.window.isCtrlDown)
} else if (selections.isNotEmpty()) {
val (index, selection) = selections.lastEntry()
cursorLine = index
cursorRow = selection.cursor + selection.shift
selections.clear()
} else {
advanceCursorRight(minecraft.window.isCtrlDown)
} }
return true return true
@ -911,12 +939,31 @@ open class TextInputPanel<out S : Screen>(
wipeSelection() wipeSelection()
pushbackSnapshot() pushbackSnapshot()
var shouldPlayClick = false
fun trimLine(input: String): String {
if (input.length < maxLineLength) {
return input
} else {
shouldPlayClick = true
return input.substring(0, maxLineLength)
}
}
if (isMultiLine) { if (isMultiLine) {
if (cursorLine >= maxLines) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
return true
}
var index = cursorRow + (0 until cursorLine).iterator().map { this[it]?.length ?: 0 }.reduce(0, Int::plus) var index = cursorRow + (0 until cursorLine).iterator().map { this[it]?.length ?: 0 }.reduce(0, Int::plus)
val insert = minecraft.keyboardHandler.clipboard.replace("\t", " ").filter { acceptsCharacter(it, 0, index++) }.split(NEWLINES).toMutableList() val insert = minecraft.keyboardHandler.clipboard.replace("\t", " ").filter { acceptsCharacter(it, 0, index++) }.split(NEWLINES).toMutableList()
val actualLastSize = insert.lastOrNull()?.length ?: 0 val actualLastSize = insert.lastOrNull()?.length ?: 0
val line = this[cursorLine] val line = this[cursorLine]
insert[0] = trimLine(insert[0])
if (line == null) { if (line == null) {
insertLine(cursorLine, insert[0]) insertLine(cursorLine, insert[0])
cursorRow = insert[0].length cursorRow = insert[0].length
@ -925,9 +972,9 @@ open class TextInputPanel<out S : Screen>(
cursorRow = insert[0].length cursorRow = insert[0].length
} else { } else {
if (insert.size == 1) { if (insert.size == 1) {
this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert[0] + line.substring(cursorRow.coerceAtMost(line.length)) this[cursorLine] = trimLine(line.substring(0, cursorRow.coerceAtMost(line.length)) + insert[0] + line.substring(cursorRow.coerceAtMost(line.length)))
} else { } else {
this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert[0] this[cursorLine] = trimLine(line.substring(0, cursorRow.coerceAtMost(line.length)) + insert[0])
insert[insert.size - 1] += line.substring(cursorRow.coerceAtMost(line.length)) insert[insert.size - 1] += line.substring(cursorRow.coerceAtMost(line.length))
} }
@ -935,21 +982,31 @@ open class TextInputPanel<out S : Screen>(
} }
for (i in 1 until insert.size - 1) { for (i in 1 until insert.size - 1) {
if (cursorLine + 1 >= maxLines) {
shouldPlayClick = true
break
}
insert[i] = trimLine(insert[i])
insertLine(++cursorLine, insert[i]) insertLine(++cursorLine, insert[i])
cursorRow = insert[i].length cursorRow = insert[i].length
} }
if (insert.size >= 2) { if (insert.size >= 2) {
val line2 = this[++cursorLine] if (cursorLine + 1 < maxLines) {
val last = insert.last() val line2 = this[++cursorLine]
val last = insert.last()
if (line2 == null) { if (line2 == null) {
insertLine(cursorLine, last) insertLine(cursorLine, trimLine(last))
} else {
this[cursorLine] = trimLine(last + line2)
}
cursorRow = actualLastSize
} else { } else {
this[cursorLine] = last + line2 shouldPlayClick = true
} }
cursorRow = actualLastSize
} }
} else { } else {
var index = cursorRow + (0 until cursorLine).iterator().map { this[it]?.length ?: 0 }.reduce(0, Int::plus) var index = cursorRow + (0 until cursorLine).iterator().map { this[it]?.length ?: 0 }.reduce(0, Int::plus)
@ -957,17 +1014,26 @@ open class TextInputPanel<out S : Screen>(
val line = this[cursorLine] val line = this[cursorLine]
if (line == null) { if (line == null) {
insertLine(cursorLine, insert) val trim = trimLine(insert)
cursorRow = insert.length insertLine(cursorLine, trim)
cursorRow = trim.length
} else if (line.isEmpty()) { } else if (line.isEmpty()) {
this[cursorLine] = insert val trim = trimLine(insert)
cursorRow = insert.length this[cursorLine] = trim
cursorRow = trim.length
} else { } else {
this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.length)) val potential = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.length))
cursorRow += insert.length val final = trimLine(potential)
this[cursorLine] = final
cursorRow += maxOf(0, insert.length - (potential.length - final.length))
} }
} }
if (shouldPlayClick) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
}
pushbackSnapshot() pushbackSnapshot()
triggerChangeCallback() triggerChangeCallback()
return true return true
@ -1078,6 +1144,11 @@ open class TextInputPanel<out S : Screen>(
if (!isMultiLine) if (!isMultiLine)
cursorLine = 0 cursorLine = 0
else if (cursorLine >= maxLines) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
return true
}
var line = this[cursorLine] var line = this[cursorLine]
@ -1091,7 +1162,7 @@ open class TextInputPanel<out S : Screen>(
for (i in 0 until cursorLine) index += this[i]?.length ?: 0 for (i in 0 until cursorLine) index += this[i]?.length ?: 0
index += cursorRow index += cursorRow
if (!acceptsCharacter(codepoint, mods, index)) { if (cursorRow >= maxLineLength || !acceptsCharacter(codepoint, mods, index)) {
playGuiClickSound() playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS rejectCharacterTimer = milliTime + SHAKE_MILLIS
return true return true
@ -1195,6 +1266,22 @@ open class TextInputPanel<out S : Screen>(
) )
} }
if (maxLines < 100000) {
val p = maxLines * (font.lineHeight + rowSpacing) + dockPaddingTop
graphics.drawLine(0f, p, 1000000f, p, 2f, color = RGBAColor.GRAY)
}
// TODO: Minecraft font is not mono spaced, so we can't properly render per-line character bounds
if (selectedLine != null && abs(selectedLine.length - maxLineLength) < 200) {
val lineWidth = if (selectedLine.length >= maxLineLength) {
font.width(selectedLine.substring(0, maxLineLength)).toFloat()
} else {
font.width(selectedLine + "a".repeat(maxLineLength - selectedLine.length)).toFloat()
}
graphics.drawLine(lineWidth + 4f, 0f, lineWidth + 4f, 1000000f, 2f, color = RGBAColor.GRAY)
}
for (i in scrollLines until lines.size) { for (i in scrollLines until lines.size) {
val line = lines[i] val line = lines[i]
val selection = selections[i] val selection = selections[i]
@ -1303,9 +1390,6 @@ open class TextInputPanel<out S : Screen>(
protected set protected set
override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean { override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
if (!isActive)
return true
if (isEverFocused()) if (isEverFocused())
selections.clear() selections.clear()
@ -1321,7 +1405,7 @@ open class TextInputPanel<out S : Screen>(
isSelecting = true isSelecting = true
tryToGrabMouseInput() tryToGrabMouseInput()
} else if (button == InputConstants.MOUSE_BUTTON_RIGHT && !isMultiLine) { } else if (button == InputConstants.MOUSE_BUTTON_RIGHT && !isMultiLine && isActive) {
text = "" text = ""
} }

View File

@ -39,7 +39,7 @@ class HoloSignMenu(
textAutoScale.filter { it.isCreative || !locked.value } textAutoScale.filter { it.isCreative || !locked.value }
if (tile != null) { if (tile != null) {
text.withConsumer { if (tile.isLocked) tile.signText = it else tile.signText = HoloSignBlockEntity.truncate(it) }.withSupplier(tile::signText) text.withConsumer { tile.signText = HoloSignBlockEntity.truncate(it, tile.isLocked) }.withSupplier(tile::signText)
textRed.withConsumer { tile.textRed = it.coerceIn(0f, 1f) }.withSupplier(tile::textRed) textRed.withConsumer { tile.textRed = it.coerceIn(0f, 1f) }.withSupplier(tile::textRed)
textGreen.withConsumer { tile.textGreen = it.coerceIn(0f, 1f) }.withSupplier(tile::textGreen) textGreen.withConsumer { tile.textGreen = it.coerceIn(0f, 1f) }.withSupplier(tile::textGreen)
textBlue.withConsumer { tile.textBlue = it.coerceIn(0f, 1f) }.withSupplier(tile::textBlue) textBlue.withConsumer { tile.textBlue = it.coerceIn(0f, 1f) }.withSupplier(tile::textBlue)