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() }
var signText by syncher.string("", setter = { access, value ->
setChanged()
markDirtyFast()
access.accept(value)
}).delegate
@ -96,24 +96,25 @@ class HoloSignBlockEntity(blockPos: BlockPos, blockState: BlockState) : MatteryB
override fun loadAdditional(nbt: CompoundTag, registry: HolderLookup.Provider) {
super.loadAdditional(nbt, registry)
if (!isLocked) {
signText = truncate(signText)
}
signText = truncate(signText, isLocked)
}
companion object {
const val DEFAULT_MAX_NEWLINES = 10
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")
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 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)) {
if (lines[i].length > DEFAULT_MAX_LINE_LENGTH) {
result.add(lines[i].substring(0, DEFAULT_MAX_LINE_LENGTH))
for (i in 0 until lines.size.coerceAtMost(maxLines)) {
if (lines[i].length > maxLength) {
result.add(lines[i].substring(0, maxLength))
} else {
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.core.TranslatableComponent
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.screen.panels.button.BooleanButtonPanel
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) {
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.onClose { onClose() }
@ -25,9 +26,19 @@ class HoloSignScreen(menu: HoloSignMenu, inventory: Inventory, title: Component)
tooltips.add(TranslatableComponent("otm.gui.lock_holo_screen.tip"))
}
val input = NetworkedStringInputPanel(this, frame, backend = menu.text)
input.dock = Dock.FILL
input.isMultiLine = true
object : NetworkedStringInputPanel<HoloSignScreen>(this@HoloSignScreen, frame, backend = menu.text) {
init {
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)

View File

@ -616,7 +616,7 @@ open class ColorPickerPanel<out S : Screen>(
}
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.milliTime
import java.util.function.Predicate
import kotlin.math.abs
import kotlin.math.roundToInt
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 cursorLine = this@TextInputPanel.cursorLine
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
fun apply() {
@ -155,7 +156,19 @@ open class TextInputPanel<out S : Screen>(
open var isActive = true
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 {
scissor = true
@ -463,8 +476,6 @@ open class TextInputPanel<out S : Screen>(
lines.clear()
redo.clear()
undo.clear()
cursorLine = 0
cursorRow = 0
textCache = null
if (isMultiLine) {
@ -472,6 +483,9 @@ open class TextInputPanel<out S : Screen>(
} else {
lines.add(value.replace(NEWLINES, ""))
}
cursorLine = minOf(cursorLine, lines.size - 1)
cursorRow = minOf(this[cursorLine]?.length ?: 0, cursorRow)
}
data class CursorAdvanceResult(
@ -675,6 +689,12 @@ open class TextInputPanel<out S : Screen>(
if (key == InputConstants.KEY_RETURN) {
if (isMultiLine) {
if (lines.size >= maxLines) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
return true
}
if (!minecraft.window.isShiftDown && !minecraft.window.isCtrlDown)
wipeSelection()
@ -729,26 +749,34 @@ open class TextInputPanel<out S : Screen>(
}
if (key == InputConstants.KEY_LEFT) {
if (!minecraft.window.isShiftDown) {
advanceCursorLeft(minecraft.window.isCtrlDown)
selections.clear()
} else {
if (minecraft.window.isShiftDown) {
if (this[cursorLine] == null)
return true
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
} else if (key == InputConstants.KEY_RIGHT) {
if (!minecraft.window.isShiftDown) {
advanceCursorRight(minecraft.window.isCtrlDown)
selections.clear()
} else {
if (minecraft.window.isShiftDown) {
if (this[cursorLine] == null)
return true
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
@ -911,12 +939,31 @@ open class TextInputPanel<out S : Screen>(
wipeSelection()
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 (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)
val insert = minecraft.keyboardHandler.clipboard.replace("\t", " ").filter { acceptsCharacter(it, 0, index++) }.split(NEWLINES).toMutableList()
val actualLastSize = insert.lastOrNull()?.length ?: 0
val line = this[cursorLine]
insert[0] = trimLine(insert[0])
if (line == null) {
insertLine(cursorLine, insert[0])
cursorRow = insert[0].length
@ -925,9 +972,9 @@ open class TextInputPanel<out S : Screen>(
cursorRow = insert[0].length
} else {
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 {
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))
}
@ -935,21 +982,31 @@ open class TextInputPanel<out S : Screen>(
}
for (i in 1 until insert.size - 1) {
if (cursorLine + 1 >= maxLines) {
shouldPlayClick = true
break
}
insert[i] = trimLine(insert[i])
insertLine(++cursorLine, insert[i])
cursorRow = insert[i].length
}
if (insert.size >= 2) {
val line2 = this[++cursorLine]
val last = insert.last()
if (cursorLine + 1 < maxLines) {
val line2 = this[++cursorLine]
val last = insert.last()
if (line2 == null) {
insertLine(cursorLine, last)
if (line2 == null) {
insertLine(cursorLine, trimLine(last))
} else {
this[cursorLine] = trimLine(last + line2)
}
cursorRow = actualLastSize
} else {
this[cursorLine] = last + line2
shouldPlayClick = true
}
cursorRow = actualLastSize
}
} else {
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]
if (line == null) {
insertLine(cursorLine, insert)
cursorRow = insert.length
val trim = trimLine(insert)
insertLine(cursorLine, trim)
cursorRow = trim.length
} else if (line.isEmpty()) {
this[cursorLine] = insert
cursorRow = insert.length
val trim = trimLine(insert)
this[cursorLine] = trim
cursorRow = trim.length
} else {
this[cursorLine] = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.length))
cursorRow += insert.length
val potential = line.substring(0, cursorRow.coerceAtMost(line.length - 1)) + insert + line.substring(cursorRow.coerceAtMost(line.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()
triggerChangeCallback()
return true
@ -1078,6 +1144,11 @@ open class TextInputPanel<out S : Screen>(
if (!isMultiLine)
cursorLine = 0
else if (cursorLine >= maxLines) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
return true
}
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
index += cursorRow
if (!acceptsCharacter(codepoint, mods, index)) {
if (cursorRow >= maxLineLength || !acceptsCharacter(codepoint, mods, index)) {
playGuiClickSound()
rejectCharacterTimer = milliTime + SHAKE_MILLIS
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) {
val line = lines[i]
val selection = selections[i]
@ -1303,9 +1390,6 @@ open class TextInputPanel<out S : Screen>(
protected set
override fun mouseClickedInner(x: Double, y: Double, button: Int): Boolean {
if (!isActive)
return true
if (isEverFocused())
selections.clear()
@ -1321,7 +1405,7 @@ open class TextInputPanel<out S : Screen>(
isSelecting = true
tryToGrabMouseInput()
} else if (button == InputConstants.MOUSE_BUTTON_RIGHT && !isMultiLine) {
} else if (button == InputConstants.MOUSE_BUTTON_RIGHT && !isMultiLine && isActive) {
text = ""
}

View File

@ -39,7 +39,7 @@ class HoloSignMenu(
textAutoScale.filter { it.isCreative || !locked.value }
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)
textGreen.withConsumer { tile.textGreen = it.coerceIn(0f, 1f) }.withSupplier(tile::textGreen)
textBlue.withConsumer { tile.textBlue = it.coerceIn(0f, 1f) }.withSupplier(tile::textBlue)