mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
Add support for TextInput text selection
This commit is contained in:
parent
9511f586a6
commit
c796b74ea3
@ -68,7 +68,9 @@ TextInputBorderDisabled = #666666
|
||||
TextInputBorderFocused = #336666
|
||||
TextInputBackground = #aaaaaa
|
||||
TextInputBackgroundActive = #bbbbbb
|
||||
TextInputBackgroundSelected = #336666
|
||||
TextInputForeground = #333333
|
||||
TextInputForegroundSelected = #aaaaaa
|
||||
TextInputForegroundDisabled = #888888
|
||||
TextInputBorderError = #aa8888
|
||||
TextInputBorderErrorDisabled = #aa8888
|
||||
|
||||
@ -80,7 +80,7 @@ object UiHandler extends Logging {
|
||||
_clipboard.getData(DataFlavor.stringFlavor).toString
|
||||
} catch {
|
||||
case _: UnsupportedFlavorException =>
|
||||
logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with empty string.")
|
||||
logger.debug("Trying to paste non-Unicode content from clipboard! Replaced with an empty string.")
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,8 +7,8 @@ import ocelot.desktop.graphics.{Font, Graphics}
|
||||
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.{Cursor, Text}
|
||||
import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent}
|
||||
import ocelot.desktop.ui.widget.TextInput.{Cursor, Selector, Text}
|
||||
import ocelot.desktop.ui.widget.traits.HoverAnimation
|
||||
import ocelot.desktop.util.{DrawUtils, Register, Watcher}
|
||||
import ocelot.desktop.util.animation.ColorAnimation
|
||||
@ -18,30 +18,35 @@ import org.lwjgl.input.Keyboard
|
||||
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
|
||||
private val CursorBlinkTime = 2f
|
||||
private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled")
|
||||
private val BackgroundSelectedColor: Color = ColorScheme("TextInputBackgroundSelected")
|
||||
private val ForegroundSelectedColor: Color = ColorScheme("TextInputForegroundSelected")
|
||||
override protected val HoverAnimationColorDefault: Color = ColorScheme("TextInputBackground")
|
||||
override protected val HoverAnimationColorActive: Color = ColorScheme("TextInputBackgroundActive")
|
||||
|
||||
// model
|
||||
private val _text: Text = new Text(initialText.codePoints().toArray)
|
||||
private val cursor: Cursor = new Cursor()
|
||||
private val selector: Selector = new Selector()
|
||||
|
||||
// view
|
||||
private var isFocused = false
|
||||
private var scroll = 0f
|
||||
private var blinkTimer = 0f
|
||||
private var cursorOffset = 0f
|
||||
private var selectorStartOffset = 0f
|
||||
private var selectorEndOffset = 0f
|
||||
|
||||
private val enabledRegister = Register.sampling(enabled)
|
||||
|
||||
cursor.onChange(position => {
|
||||
cursorOffset = charsWidth(_text.chars, 0, position)
|
||||
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(_text.chars, 0, _text.chars.length)
|
||||
val areaWidth = size.width - 16
|
||||
if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth
|
||||
adjustScroll()
|
||||
})
|
||||
|
||||
selector.onChange((from, to) => {
|
||||
selectorStartOffset = charsWidth(_text.chars, 0, from min to)
|
||||
selectorEndOffset = charsWidth(_text.chars, 0, from max to)
|
||||
})
|
||||
|
||||
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
|
||||
@ -60,7 +65,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
final def isInputValid: Boolean = validator(text)
|
||||
|
||||
def text: String = new String(_text.chars, 0, _text.chars.length)
|
||||
def text_=(value: String): Unit = _text.chars = value.codePoints().toArray
|
||||
def text_=(value: String): Unit = {
|
||||
_text.chars = value.codePoints().toArray
|
||||
selector.active = false
|
||||
}
|
||||
|
||||
private def selectedText: String = new String(_text.chars, selector.start, selector.length)
|
||||
|
||||
protected var placeholder: Array[Int] = Array.empty
|
||||
def placeholder_=(value: String): Unit = placeholder = value.codePoints().toArray
|
||||
@ -88,6 +98,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
override def minimumSize: Size2D = Size2D(200, 24)
|
||||
override def maximumSize: Size2D = Size2D(Float.PositiveInfinity, 24)
|
||||
override def receiveAllMouseEvents = true
|
||||
override def receiveDragEvents: Boolean = true
|
||||
|
||||
protected def font: Font = Font.NormalFont
|
||||
|
||||
@ -108,53 +119,125 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
}
|
||||
|
||||
eventHandlers += {
|
||||
case MouseEvent(MouseEvent.State.Released, MouseEvent.Button.Left) =>
|
||||
if (isFocused && !clippedBounds.contains(UiHandler.mousePosition)) {
|
||||
unfocus()
|
||||
}
|
||||
|
||||
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Left) if enabled =>
|
||||
focus()
|
||||
val inBounds = clippedBounds.contains(UiHandler.mousePosition)
|
||||
if (isFocused && !inBounds) unfocus()
|
||||
if (!isFocused && inBounds) focus()
|
||||
if (isFocused) {
|
||||
val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x)
|
||||
cursor.position = _text.chars.length.min(pos).max(0)
|
||||
selector.from = cursor.position
|
||||
selector.active = false
|
||||
}
|
||||
|
||||
case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mouse) if isFocused =>
|
||||
val pos = pixelToCursorPosition(mouse.x - bounds.x)
|
||||
selector.active = true
|
||||
selector.to = pos
|
||||
cursor.position = pos
|
||||
event.consume()
|
||||
|
||||
case event @ DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, mouse) if isFocused =>
|
||||
val pos = pixelToCursorPosition(mouse.x - bounds.x)
|
||||
selector.to = pos
|
||||
cursor.position = pos
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
||||
if (cursor.position > 0) cursor.position -= 1
|
||||
if (KeyEvents.isShiftDown && !selector.active) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) {
|
||||
cursor.position = selector.start
|
||||
selector.active = false
|
||||
} else if (cursor.position > 0) {
|
||||
cursor.position -= 1
|
||||
if (selector.active) selector.to = cursor.position
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
||||
if (cursor.position < _text.chars.length) cursor.position += 1
|
||||
if (KeyEvents.isShiftDown && !selector.active) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) {
|
||||
cursor.position = selector.end
|
||||
selector.active = false
|
||||
} else if (cursor.position < _text.chars.length) {
|
||||
cursor.position += 1
|
||||
if (selector.active) selector.to = cursor.position
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
|
||||
if (!selector.active && KeyEvents.isShiftDown) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) selector.active = false
|
||||
cursor.position = 0
|
||||
if (selector.active && KeyEvents.isShiftDown) selector.to = cursor.position
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
|
||||
if (!selector.active && KeyEvents.isShiftDown) {
|
||||
selector.active = true
|
||||
selector.from = cursor.position
|
||||
}
|
||||
if (selector.active && !KeyEvents.isShiftDown) selector.active = false
|
||||
cursor.position = _text.chars.length
|
||||
if (selector.active && KeyEvents.isShiftDown) selector.to = cursor.position
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
|
||||
if (selector.active) {
|
||||
deleteSelection()
|
||||
} else {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
if (!lhs.isEmpty) {
|
||||
_text.chars = lhs.take(lhs.length - 1) ++ rhs
|
||||
cursor.position -= 1
|
||||
}
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
|
||||
if (selector.active) {
|
||||
deleteSelection()
|
||||
} else {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
if (!rhs.isEmpty) {
|
||||
_text.chars = lhs ++ rhs.drop(1)
|
||||
}
|
||||
}
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_A, _)
|
||||
if isFocused && KeyEvents.isControlDown =>
|
||||
selectAll()
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_C, _)
|
||||
if isFocused && KeyEvents.isControlDown && selector.active =>
|
||||
UiHandler.clipboard = selectedText
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_X, _)
|
||||
if isFocused && KeyEvents.isControlDown && selector.active =>
|
||||
UiHandler.clipboard = selectedText
|
||||
deleteSelection()
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_INSERT, _) if isFocused =>
|
||||
if (selector.active) deleteSelection()
|
||||
writeString(UiHandler.clipboard)
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_V, _)
|
||||
if isFocused && KeyEvents.isControlDown =>
|
||||
if (selector.active) deleteSelection()
|
||||
writeString(UiHandler.clipboard)
|
||||
event.consume()
|
||||
|
||||
@ -163,6 +246,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
event.consume()
|
||||
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl =>
|
||||
if (selector.active) deleteSelection()
|
||||
writeChar(char)
|
||||
event.consume()
|
||||
}
|
||||
@ -182,6 +266,18 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
pos
|
||||
}
|
||||
|
||||
private def selectAll(): Unit = {
|
||||
selector.active = true
|
||||
selector.from = 0
|
||||
selector.to = _text.chars.length
|
||||
}
|
||||
|
||||
private def deleteSelection(): Unit = {
|
||||
selector.active = false
|
||||
_text.chars = _text.chars.take(selector.start) ++ _text.chars.drop(selector.end)
|
||||
cursor.position = selector.start
|
||||
}
|
||||
|
||||
private def writeString(string: String): Unit = {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
val array = string.codePoints().toArray
|
||||
@ -195,15 +291,28 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
cursor.position += 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply a set of corrections to the scroll to make sure the cursor and text stay visible
|
||||
*/
|
||||
private def adjustScroll(): Unit = {
|
||||
// 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(_text.chars, 0, _text.chars.length)
|
||||
val areaWidth = size.width - 16
|
||||
if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth
|
||||
}
|
||||
|
||||
|
||||
override def update(): Unit = {
|
||||
super.update()
|
||||
|
||||
// process state changes
|
||||
if (_text.didChange) {
|
||||
if (_text.changed()) {
|
||||
onInput(text)
|
||||
updateAnimationTargets()
|
||||
if (cursor.position > _text.chars.length) cursor.position = _text.chars.length
|
||||
adjustScroll()
|
||||
}
|
||||
|
||||
if (enabledRegister.update()) {
|
||||
@ -247,12 +356,21 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
|
||||
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
|
||||
|
||||
if (selector.active) {
|
||||
val width = selectorEndOffset - selectorStartOffset
|
||||
g.rect(position.x + selectorStartOffset + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor)
|
||||
}
|
||||
|
||||
g.background = Color.Transparent
|
||||
g.foreground = if (_text.chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor
|
||||
val foreground = if (_text.chars.nonEmpty || isFocused) foregroundAnimation.color else PlaceholderForegroundColor
|
||||
g.foreground = foreground
|
||||
|
||||
var charOffset = 0
|
||||
val charsToDisplay = if (_text.chars.nonEmpty || isFocused) _text.chars else placeholder
|
||||
for (char <- charsToDisplay) {
|
||||
if (selector.active) {
|
||||
g.foreground = if (charOffset >= selectorStartOffset && charOffset < selectorEndOffset) ForegroundSelectedColor else foreground
|
||||
}
|
||||
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
|
||||
charOffset += g.font.charWidth(char)
|
||||
}
|
||||
@ -272,4 +390,25 @@ object TextInput {
|
||||
def position: Int = value
|
||||
def position_=(newValue: Int): Unit = value = newValue
|
||||
}
|
||||
class Selector {
|
||||
private val fromWatcher = Watcher(0)
|
||||
private val toWatcher = Watcher(0)
|
||||
|
||||
def from: Int = fromWatcher.value
|
||||
def from_=(x: Int): Unit = fromWatcher.value = x
|
||||
|
||||
def to: Int = toWatcher.value
|
||||
def to_=(x: Int): Unit = toWatcher.value = x
|
||||
|
||||
def start: Int = from min to
|
||||
def end: Int = from max to
|
||||
def length: Int = end - start
|
||||
|
||||
var active: Boolean = false
|
||||
|
||||
def onChange(callback: (Int, Int) => Unit): Unit = {
|
||||
fromWatcher.onChange(callback(_, to))
|
||||
toWatcher.onChange(callback(from, _))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,13 +12,13 @@ class Watcher[T](initialValue: T) {
|
||||
def value: T = _value
|
||||
def value_=(newValue: T): Unit = {
|
||||
dirty = _value != newValue
|
||||
_value = newValue
|
||||
_callback.foreach(_(newValue))
|
||||
_value = newValue
|
||||
}
|
||||
|
||||
def onChange(callback: T => Unit): Unit = _callback = Some(callback)
|
||||
|
||||
def didChange: Boolean = {
|
||||
def changed(): Boolean = {
|
||||
if (dirty) {
|
||||
dirty = false
|
||||
true
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user