mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
326 lines
9.3 KiB
Scala
326 lines
9.3 KiB
Scala
package ocelot.desktop.ui.widget
|
|
import ocelot.desktop.ColorScheme
|
|
import ocelot.desktop.color.Color
|
|
import ocelot.desktop.geometry.Size2D
|
|
import ocelot.desktop.graphics.Graphics
|
|
import ocelot.desktop.ui.UiHandler
|
|
import ocelot.desktop.ui.event.handlers.ClickHandler
|
|
import ocelot.desktop.ui.event.sources.KeyEvents
|
|
import ocelot.desktop.ui.event.{ClickEvent, KeyEvent, MouseEvent}
|
|
import ocelot.desktop.util.DrawUtils
|
|
import ocelot.desktop.util.animation.ColorAnimation
|
|
import org.lwjgl.input.Keyboard
|
|
|
|
import scala.collection.mutable.ArrayBuffer
|
|
|
|
class TextInput(val initialText: String = "") extends Widget with ClickHandler {
|
|
def onInput(text: String): Unit = {}
|
|
|
|
def onConfirm(): Unit = {
|
|
unfocus()
|
|
}
|
|
|
|
def validator(text: String): Boolean = true
|
|
final def isInputValid: Boolean = validator(text)
|
|
|
|
var isFocused = false
|
|
|
|
def text: String = chars.mkString
|
|
def text_=(value: String): Unit = chars = value.toCharArray
|
|
|
|
private var cursorPos = 0
|
|
private var cursorOffset = 0f
|
|
private var scroll = 0f
|
|
private val CursorBlinkTime = 2f
|
|
private var blinkTimer = 0f
|
|
private var chars = initialText.toCharArray
|
|
private var textWidth = 0
|
|
private var textChanged = false
|
|
private val events = ArrayBuffer[TextEvent]()
|
|
|
|
override def receiveMouseEvents = true
|
|
override def receiveAllMouseEvents = true
|
|
|
|
def enabled: Boolean = true
|
|
private var prevEnabled = enabled
|
|
|
|
eventHandlers += {
|
|
case MouseEvent(MouseEvent.State.Release, MouseEvent.Button.Left) =>
|
|
if (isFocused && !clippedBounds.contains(UiHandler.mousePosition))
|
|
unfocus()
|
|
|
|
case ClickEvent(MouseEvent.Button.Left, pos) if enabled =>
|
|
focus()
|
|
events += new CursorMoveTo(pos.x)
|
|
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
|
events += new CursorMoveLeft
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
|
events += new CursorMoveRight
|
|
case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
|
|
events += new CursorMoveStart
|
|
case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
|
|
events += new CursorMoveEnd
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
|
|
events += new EraseCharBack
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
|
|
events += new EraseCharFront
|
|
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused =>
|
|
for (char <- UiHandler.clipboard.toCharArray)
|
|
events += new WriteChar(char)
|
|
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, ch) if isFocused =>
|
|
if (KeyEvents.isDown(Keyboard.KEY_LCONTROL))
|
|
for (char <- UiHandler.clipboard.toCharArray)
|
|
events += new WriteChar(char)
|
|
else
|
|
events += new WriteChar(ch)
|
|
|
|
case KeyEvent(KeyEvent.State.Press, Keyboard.KEY_RETURN, _) if isFocused =>
|
|
onConfirm()
|
|
|
|
case KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused =>
|
|
if (!char.isControl)
|
|
events += new WriteChar(char)
|
|
}
|
|
|
|
def setInput(text: String): Unit = {
|
|
this.chars = text.toCharArray
|
|
cursorPos = 0
|
|
cursorOffset = 0
|
|
textWidth = 0
|
|
textChanged = true
|
|
}
|
|
|
|
override def minimumSize: Size2D = Size2D(200, 24)
|
|
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24)
|
|
|
|
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
|
|
private val backgroundAnimation = new ColorAnimation(targetBackgroundColor, 7f)
|
|
private val borderAnimation = new ColorAnimation(targetBorderColor, 7f)
|
|
|
|
private def updateAnimationTargets(): Unit = {
|
|
foregroundAnimation.goto(targetForegroundColor)
|
|
backgroundAnimation.goto(targetBackgroundColor)
|
|
borderAnimation.goto(targetBorderColor)
|
|
}
|
|
|
|
def focus(): Unit = {
|
|
if (!isFocused) {
|
|
if (enabled) {
|
|
isFocused = true
|
|
}
|
|
|
|
updateAnimationTargets()
|
|
}
|
|
|
|
blinkTimer = 0
|
|
}
|
|
|
|
def unfocus(): Unit = {
|
|
if (isFocused) {
|
|
isFocused = false
|
|
updateAnimationTargets()
|
|
}
|
|
}
|
|
|
|
private def targetBorderColor: Color = ColorScheme(
|
|
if (validator(chars.mkString)) {
|
|
if (isFocused) "TextInputBorderFocused"
|
|
else if (!enabled) "TextInputBorderDisabled"
|
|
else "TextInputBorder"
|
|
} else {
|
|
if (isFocused) "TextInputBorderErrorFocused"
|
|
else if (!enabled) "TextInputBorderErrorDisabled"
|
|
else "TextInputBorderError"
|
|
}
|
|
)
|
|
|
|
private def targetBackgroundColor: Color = ColorScheme(
|
|
if (!enabled) "TextInputBackgroundDisabled"
|
|
else "TextInputBackground"
|
|
)
|
|
|
|
private def targetForegroundColor: Color = ColorScheme(
|
|
if (!enabled) "TextInputForegroundDisabled"
|
|
else "TextInputForeground"
|
|
)
|
|
|
|
override def draw(g: Graphics): Unit = {
|
|
if (textWidth == 0f && chars.nonEmpty)
|
|
textWidth = chars.map(g.font.charWidth(_)).sum
|
|
|
|
textChanged = false
|
|
for (event <- events) event.handle(g)
|
|
events.clear()
|
|
|
|
if (textChanged) {
|
|
val str = chars.mkString
|
|
onInput(str)
|
|
updateAnimationTargets()
|
|
}
|
|
|
|
g.rect(bounds, backgroundAnimation.color)
|
|
DrawUtils.ring(g, position.x, position.y, size.width, size.height, thickness = 2, borderAnimation.color)
|
|
|
|
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
|
|
|
|
g.background = Color.Transparent
|
|
g.foreground = foregroundAnimation.color
|
|
|
|
var charOffset = 0
|
|
for (char <- chars) {
|
|
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
|
|
charOffset += g.font.charWidth(char)
|
|
}
|
|
|
|
if (isFocused && blinkTimer < CursorBlinkTime * 0.5f) {
|
|
g.rect(position.x + 7 + cursorOffset - scroll, position.y + 4, 2, 16, borderAnimation.color)
|
|
}
|
|
}
|
|
|
|
override def update(): Unit = {
|
|
super.update()
|
|
|
|
val nextEnabled = enabled
|
|
|
|
if (nextEnabled != prevEnabled) {
|
|
updateAnimationTargets()
|
|
prevEnabled = nextEnabled
|
|
}
|
|
|
|
if (isFocused && !enabled) {
|
|
unfocus()
|
|
}
|
|
|
|
foregroundAnimation.update()
|
|
backgroundAnimation.update()
|
|
borderAnimation.update()
|
|
blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime
|
|
}
|
|
|
|
private def charWidth(g: Graphics, c: Char): Int = g.font.charWidth(c)
|
|
|
|
//noinspection SameParameterValue
|
|
private def charsWidth(g: Graphics, chars: Array[Char], first: Int, last: Int): Int = {
|
|
var width = 0
|
|
for (index <- first to last) {
|
|
width += g.font.charWidth(chars(index))
|
|
}
|
|
width
|
|
}
|
|
|
|
private def adjustScroll(): Unit = {
|
|
if (cursorOffset < scroll)
|
|
scroll = cursorOffset
|
|
if (cursorOffset - scroll > size.width - 16)
|
|
scroll = cursorOffset - size.width + 16
|
|
}
|
|
|
|
private abstract class TextEvent {
|
|
def handle(g: Graphics): Unit
|
|
}
|
|
|
|
private class CursorMoveLeft extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
if (cursorPos == 0) return
|
|
|
|
cursorOffset -= charWidth(g, chars(cursorPos - 1))
|
|
cursorPos -= 1
|
|
blinkTimer = 0
|
|
|
|
adjustScroll()
|
|
}
|
|
}
|
|
|
|
private class CursorMoveRight extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
if (cursorPos >= chars.length) return
|
|
|
|
cursorOffset += charWidth(g, chars(cursorPos))
|
|
cursorPos += 1
|
|
blinkTimer = 0
|
|
|
|
adjustScroll()
|
|
}
|
|
}
|
|
|
|
private class CursorMoveStart extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
cursorPos = 0
|
|
cursorOffset = 0
|
|
blinkTimer = 0
|
|
scroll = 0
|
|
}
|
|
}
|
|
|
|
private class CursorMoveEnd extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
cursorPos = chars.length
|
|
cursorOffset = textWidth
|
|
blinkTimer = 0
|
|
scroll = (textWidth - size.width + 16).max(0)
|
|
}
|
|
}
|
|
|
|
private class CursorMoveTo(mouseX: Float) extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
val absoluteX = mouseX - bounds.x + scroll - 4
|
|
var width = 0
|
|
var pos = 0
|
|
while (width < absoluteX && pos < chars.length) {
|
|
width += g.font.charWidth(chars(pos))
|
|
if (width < absoluteX) pos += 1
|
|
}
|
|
|
|
cursorPos = chars.length.min(pos).max(0)
|
|
cursorOffset = if (cursorPos > 0) charsWidth(g, chars, 0, (cursorPos - 1).max(0)) else 0
|
|
blinkTimer = 0
|
|
adjustScroll()
|
|
}
|
|
}
|
|
|
|
private class EraseCharBack extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
val (lhs, rhs) = chars.splitAt(cursorPos)
|
|
if (lhs.isEmpty) return
|
|
|
|
val cw = charWidth(g, lhs.last)
|
|
chars = lhs.take(lhs.length - 1) ++ rhs
|
|
textChanged = true
|
|
textWidth -= cw
|
|
cursorOffset -= cw
|
|
cursorPos -= 1
|
|
blinkTimer = 0
|
|
scroll = (scroll - cw).max(0)
|
|
}
|
|
}
|
|
|
|
private class EraseCharFront extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
val (lhs, rhs) = chars.splitAt(cursorPos)
|
|
if (rhs.isEmpty) return
|
|
|
|
val cw = charWidth(g, rhs.head)
|
|
chars = lhs ++ rhs.drop(1)
|
|
textChanged = true
|
|
textWidth -= cw
|
|
blinkTimer = 0
|
|
|
|
if (rhs.drop(1).map(charWidth(g, _)).sum < size.width - 16)
|
|
scroll = (scroll - cw).max(0)
|
|
}
|
|
}
|
|
|
|
private class WriteChar(char: Char) extends TextEvent {
|
|
override def handle(g: Graphics): Unit = {
|
|
val (lhs, rhs) = chars.splitAt(cursorPos)
|
|
chars = lhs ++ Array(char) ++ rhs
|
|
textChanged = true
|
|
textWidth += charWidth(g, char)
|
|
(new CursorMoveRight).handle(g)
|
|
}
|
|
}
|
|
}
|