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) } } }