mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-19 18:49:19 +01:00
Split TextInput state from presentation
This commit is contained in:
parent
3bee61832f
commit
605023118f
@ -8,8 +8,9 @@ import ocelot.desktop.ui.UiHandler
|
||||
import ocelot.desktop.ui.event.handlers.MouseHandler
|
||||
import ocelot.desktop.ui.event.sources.KeyEvents
|
||||
import ocelot.desktop.ui.event.{KeyEvent, MouseEvent}
|
||||
import ocelot.desktop.ui.widget.TextInput.State
|
||||
import ocelot.desktop.ui.widget.traits.HoverAnimation
|
||||
import ocelot.desktop.util.{DrawUtils, Register}
|
||||
import ocelot.desktop.util.{DrawUtils, Register, Watcher}
|
||||
import ocelot.desktop.util.animation.ColorAnimation
|
||||
import org.lwjgl.input.Keyboard
|
||||
|
||||
@ -20,19 +21,16 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
override protected val HoverAnimationColorDefault: Color = ColorScheme("TextInputBackground")
|
||||
override protected val HoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive")
|
||||
|
||||
// text state
|
||||
private var chars = initialText.toCharArray
|
||||
private var textChanged = false
|
||||
// model
|
||||
private val state: State = State(initialText.toCharArray)
|
||||
|
||||
// cursor state
|
||||
private var cursorPos = 0
|
||||
private var cursorOffset = 0f
|
||||
|
||||
// view state
|
||||
// view
|
||||
private var isFocused = false
|
||||
private var scroll = 0f
|
||||
private var blinkTimer = 0f
|
||||
private var cursorOffset = 0f
|
||||
|
||||
private val stateWatcher = Watcher(state)
|
||||
private val enabledRegister = Register.sampling(enabled)
|
||||
|
||||
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
|
||||
@ -50,11 +48,10 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
def validator(text: String): Boolean = true
|
||||
final def isInputValid: Boolean = validator(text)
|
||||
|
||||
def text: String = chars.mkString
|
||||
def text: String = state.chars.mkString
|
||||
def text_=(value: String): Unit = {
|
||||
chars = value.toCharArray
|
||||
adjustCursor(0)
|
||||
// note: we are not setting `textChanged = true` here to avoid potential recursion with `onInput` callback
|
||||
state.chars = value.toCharArray
|
||||
state.cursorPosition = 0
|
||||
}
|
||||
|
||||
protected var placeholder: Array[Char] = "".toCharArray
|
||||
@ -110,53 +107,37 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
|
||||
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled =>
|
||||
focus()
|
||||
val absoluteX = UiHandler.mousePosition.x - bounds.x + scroll - 4
|
||||
var width = 0
|
||||
var pos = 0
|
||||
while (width < absoluteX && pos < chars.length) {
|
||||
width += charWidth(chars(pos))
|
||||
if (width < absoluteX) pos += 1
|
||||
}
|
||||
adjustCursor(chars.length.min(pos).max(0))
|
||||
val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x)
|
||||
state.cursorPosition = state.chars.length.min(pos).max(0)
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
||||
if (cursorPos > 0) adjustCursor(cursorPos - 1)
|
||||
if (state.cursorPosition > 0) state.cursorPosition -= 1
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
||||
if (cursorPos < chars.length) adjustCursor(cursorPos + 1)
|
||||
if (state.cursorPosition < state.chars.length) state.cursorPosition += 1
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
|
||||
adjustCursor(0)
|
||||
state.cursorPosition = 0
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
|
||||
adjustCursor(chars.length)
|
||||
state.cursorPosition = state.chars.length
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
|
||||
val (lhs, rhs) = chars.splitAt(cursorPos)
|
||||
val (lhs, rhs) = state.chars.splitAt(state.cursorPosition)
|
||||
if (!lhs.isEmpty) {
|
||||
chars = lhs.take(lhs.length - 1) ++ rhs
|
||||
textChanged = true
|
||||
adjustCursor(cursorPos - 1)
|
||||
// if the text overflows - scroll it into visibility, it will feel nicer
|
||||
val cw = charWidth(lhs.last)
|
||||
scroll = (scroll - cw).max(0)
|
||||
state.chars = lhs.take(lhs.length - 1) ++ rhs
|
||||
state.cursorPosition -= 1
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
|
||||
val (lhs, rhs) = chars.splitAt(cursorPos)
|
||||
val (lhs, rhs) = state.chars.splitAt(state.cursorPosition)
|
||||
if (!rhs.isEmpty) {
|
||||
val cw = charWidth(rhs.head)
|
||||
chars = lhs ++ rhs.drop(1)
|
||||
textChanged = true
|
||||
blinkTimer = 0
|
||||
if (rhs.drop(1).map(charWidth).sum < size.width - 16) {
|
||||
scroll = (scroll - cw).max(0)
|
||||
}
|
||||
state.chars = lhs ++ rhs.drop(1)
|
||||
}
|
||||
event.consume()
|
||||
|
||||
@ -178,44 +159,51 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
event.consume()
|
||||
}
|
||||
|
||||
/**
|
||||
* Will try to find out which cursor position corresponds to the point on the screen.
|
||||
* @param x widget-local coordinate
|
||||
*/
|
||||
private def pixelToCursorPosition(x: Float): Int = {
|
||||
val absoluteX = x + scroll - 4
|
||||
var width = 0
|
||||
var pos = 0
|
||||
while (width < absoluteX && pos < state.chars.length) {
|
||||
width += charWidth(state.chars(pos))
|
||||
if (width < absoluteX) pos += 1
|
||||
}
|
||||
pos
|
||||
}
|
||||
|
||||
private def writeString(string: String): Unit = {
|
||||
val (lhs, rhs) = chars.splitAt(cursorPos)
|
||||
val (lhs, rhs) = state.chars.splitAt(state.cursorPosition)
|
||||
val array = string.toCharArray
|
||||
chars = lhs ++ array ++ rhs
|
||||
textChanged = true
|
||||
adjustCursor(cursorPos + string.length)
|
||||
state.chars = lhs ++ array ++ rhs
|
||||
state.cursorPosition += string.length
|
||||
}
|
||||
|
||||
private def writeChar(char: Char): Unit = {
|
||||
val (lhs, rhs) = chars.splitAt(cursorPos)
|
||||
chars = lhs ++ Array(char) ++ rhs
|
||||
textChanged = true
|
||||
adjustCursor(cursorPos + 1)
|
||||
val (lhs, rhs) = state.chars.splitAt(state.cursorPosition)
|
||||
state.chars = lhs ++ Array(char) ++ rhs
|
||||
state.cursorPosition += 1
|
||||
}
|
||||
|
||||
private def adjustCursor(position: Int): Unit = {
|
||||
cursorPos = position
|
||||
cursorOffset = charsWidth(chars, 0, cursorPos)
|
||||
blinkTimer = 0
|
||||
adjustScroll()
|
||||
}
|
||||
|
||||
private def adjustScroll(): Unit = {
|
||||
if (cursorOffset < scroll)
|
||||
scroll = cursorOffset
|
||||
if (cursorOffset - scroll > size.width - 16)
|
||||
scroll = cursorOffset - size.width + 16
|
||||
}
|
||||
|
||||
override def update(): Unit = {
|
||||
super.update()
|
||||
|
||||
// process state changes
|
||||
if (textChanged) {
|
||||
if (stateWatcher.didChange) {
|
||||
onInput(text)
|
||||
updateAnimationTargets()
|
||||
textChanged = false
|
||||
cursorOffset = charsWidth(state.chars, 0, state.cursorPosition)
|
||||
blinkTimer = 0
|
||||
// make cursor visible
|
||||
if (cursorOffset < scroll) scroll = cursorOffset
|
||||
if (cursorOffset - scroll > size.width - 16) scroll = cursorOffset - size.width + 16
|
||||
// apply pressure from the left (to maximize visible text, for nicer editing experience)
|
||||
val fullTextWidth = charsWidth(state.chars, 0, state.chars.length)
|
||||
val areaWidth = size.width - 16
|
||||
if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth
|
||||
}
|
||||
if (enabledRegister.update()) {
|
||||
updateAnimationTargets()
|
||||
@ -237,7 +225,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
}
|
||||
|
||||
private def targetBorderColor: Color = ColorScheme(
|
||||
if (validator(chars.mkString)) {
|
||||
if (validator(text)) {
|
||||
if (isFocused) "TextInputBorderFocused"
|
||||
else if (!enabled) "TextInputBorderDisabled"
|
||||
else "TextInputBorder"
|
||||
@ -247,7 +235,6 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
else "TextInputBorderError"
|
||||
}
|
||||
)
|
||||
|
||||
private def targetForegroundColor: Color = ColorScheme(
|
||||
if (!enabled) "TextInputForegroundDisabled"
|
||||
else "TextInputForeground"
|
||||
@ -260,10 +247,10 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
|
||||
|
||||
g.background = Color.Transparent
|
||||
g.foreground = if (chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor
|
||||
g.foreground = if (state.chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor
|
||||
|
||||
var charOffset = 0
|
||||
val charsToDisplay = if (chars.nonEmpty || isFocused) chars else placeholder
|
||||
val charsToDisplay = if (state.chars.nonEmpty || isFocused) state.chars else placeholder
|
||||
for (char <- charsToDisplay) {
|
||||
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
|
||||
charOffset += g.font.charWidth(char)
|
||||
@ -274,3 +261,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TextInput {
|
||||
case class State(var chars: Array[Char], var cursorPosition: Int = 0)
|
||||
}
|
||||
|
||||
23
src/main/scala/ocelot/desktop/util/Watcher.scala
Normal file
23
src/main/scala/ocelot/desktop/util/Watcher.scala
Normal file
@ -0,0 +1,23 @@
|
||||
package ocelot.desktop.util
|
||||
|
||||
/**
|
||||
* Keeps a reference to an object
|
||||
* and tells whether there were any changes to the value since the last check.
|
||||
*/
|
||||
class Watcher[T](value: T) {
|
||||
private var hash: Int = value.hashCode()
|
||||
|
||||
def didChange: Boolean = {
|
||||
val newHash = value.hashCode()
|
||||
if (hash != newHash) {
|
||||
hash = newHash
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object Watcher {
|
||||
def apply[T](value: T) = new Watcher(value)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user