mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
Reimplement TextInput's selection state
This commit is contained in:
parent
c9f8f4a123
commit
ca8ef6eee1
@ -1,5 +1,8 @@
|
|||||||
package ocelot.desktop.ui.event
|
package ocelot.desktop.ui.event
|
||||||
|
|
||||||
|
case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value)(val stateChanged: Boolean)
|
||||||
|
extends CapturingEvent
|
||||||
|
|
||||||
object MouseEvent {
|
object MouseEvent {
|
||||||
object State extends Enumeration {
|
object State extends Enumeration {
|
||||||
val Pressed, Released = Value
|
val Pressed, Released = Value
|
||||||
@ -10,6 +13,14 @@ object MouseEvent {
|
|||||||
val Right: Button.Value = Value(1)
|
val Right: Button.Value = Value(1)
|
||||||
val Middle: Button.Value = Value(2)
|
val Middle: Button.Value = Value(2)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case class MouseEvent(state: MouseEvent.State.Value, button: MouseEvent.Button.Value) extends CapturingEvent
|
object StateChanged {
|
||||||
|
def unapply(event: MouseEvent): Option[(State.Value, Button.Value)] = {
|
||||||
|
if (event.stateChanged) {
|
||||||
|
MouseEvent.unapply(event)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ trait MouseHandler extends Widget {
|
|||||||
protected def receiveDragEvents: Boolean = false
|
protected def receiveDragEvents: Boolean = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* If `true`, drag events, once they start, will be sent on every update cycle when if the mouse does not move.
|
* If `true`, drag events, once they start, will be sent on every update cycle even if the mouse does not move.
|
||||||
*/
|
*/
|
||||||
protected def spamDragEvents: Boolean = true
|
protected def spamDragEvents: Boolean = true
|
||||||
|
|
||||||
|
|||||||
@ -26,13 +26,13 @@ object MouseEvents {
|
|||||||
if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) {
|
if (MouseEvent.Button.values.map(_.id).contains(buttonIdx)) {
|
||||||
val button = MouseEvent.Button(buttonIdx)
|
val button = MouseEvent.Button(buttonIdx)
|
||||||
val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released
|
val state = if (Mouse.getEventButtonState) MouseEvent.State.Pressed else MouseEvent.State.Released
|
||||||
_events += MouseEvent(state, button)
|
val changed = state match {
|
||||||
state match {
|
|
||||||
case MouseEvent.State.Pressed =>
|
case MouseEvent.State.Pressed =>
|
||||||
_pressedButtons += button
|
_pressedButtons.add(button)
|
||||||
case MouseEvent.State.Released =>
|
case MouseEvent.State.Released =>
|
||||||
_pressedButtons -= button
|
_pressedButtons.remove(button)
|
||||||
}
|
}
|
||||||
|
_events += MouseEvent(state, button)(changed)
|
||||||
}
|
}
|
||||||
|
|
||||||
val delta = Mouse.getEventDWheel
|
val delta = Mouse.getEventDWheel
|
||||||
@ -49,7 +49,7 @@ object MouseEvents {
|
|||||||
|
|
||||||
def releaseButtons(): Unit = {
|
def releaseButtons(): Unit = {
|
||||||
for (button <- pressedButtons) {
|
for (button <- pressedButtons) {
|
||||||
_events += MouseEvent(MouseEvent.State.Released, button)
|
_events += MouseEvent(MouseEvent.State.Released, button)(stateChanged = true)
|
||||||
}
|
}
|
||||||
|
|
||||||
_pressedButtons.clear()
|
_pressedButtons.clear()
|
||||||
|
|||||||
@ -17,7 +17,6 @@ import org.lwjgl.input.Keyboard
|
|||||||
|
|
||||||
import java.lang.Character.isWhitespace
|
import java.lang.Character.isWhitespace
|
||||||
|
|
||||||
|
|
||||||
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
|
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
|
||||||
private val CursorBlinkTime = 2f
|
private val CursorBlinkTime = 2f
|
||||||
private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled")
|
private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled")
|
||||||
@ -29,33 +28,44 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
// model
|
// model
|
||||||
private val _text: Text = new Text(initialText.codePoints().toArray)
|
private val _text: Text = new Text(initialText.codePoints().toArray)
|
||||||
private val cursor: Cursor = new Cursor()
|
private val cursor: Cursor = new Cursor()
|
||||||
private val selection: Selection = new Selection()
|
private val selectionWatcher = new Watcher[Option[Selection]](None)
|
||||||
|
|
||||||
|
// updated after all events are processed so that event handlers can refer to the previous position.
|
||||||
|
private val prevCursorPosition = Register.sampling(cursor.position)
|
||||||
|
|
||||||
// view
|
// view
|
||||||
private var isFocused = false
|
private var isFocused = false
|
||||||
private var scroll = 0f
|
private var scroll = 0f
|
||||||
private var blinkTimer = 0f
|
private var blinkTimer = 0f
|
||||||
private var cursorOffset = 0f
|
private var cursorOffset = 0f
|
||||||
private var selectionStartOffset = 0f
|
private var selectionOffsets: Option[(Int, Int)] = None
|
||||||
private var selectionEndOffset = 0f
|
|
||||||
|
|
||||||
private val enabledRegister = Register.sampling(enabled)
|
private val enabledRegister = Register.sampling(enabled)
|
||||||
|
|
||||||
|
private def selection: Option[Selection] = selectionWatcher.value
|
||||||
|
private def selection_=(newValue: Option[Selection]): Unit = {
|
||||||
|
selectionWatcher.value = newValue
|
||||||
|
}
|
||||||
|
|
||||||
cursor.onChange(position => {
|
cursor.onChange(position => {
|
||||||
cursorOffset = charsWidth(_text.chars, 0, position)
|
cursorOffset = charsWidth(_text.chars, 0, position)
|
||||||
blinkTimer = 0
|
blinkTimer = 0
|
||||||
adjustScroll()
|
adjustScroll()
|
||||||
})
|
})
|
||||||
|
|
||||||
selection.onChange((from, to) => {
|
selectionWatcher.onChange(newValue => {
|
||||||
selectionStartOffset = charsWidth(_text.chars, 0, from min to)
|
selectionOffsets = newValue.map {
|
||||||
selectionEndOffset = charsWidth(_text.chars, 0, from max to)
|
case Selection.Ordered(start, end) =>
|
||||||
|
(
|
||||||
|
charsWidth(_text.chars, 0, start),
|
||||||
|
charsWidth(_text.chars, 0, end),
|
||||||
|
)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
|
private val foregroundAnimation = new ColorAnimation(targetForegroundColor, 7f)
|
||||||
private val borderAnimation = new ColorAnimation(targetBorderColor, 7f)
|
private val borderAnimation = new ColorAnimation(targetBorderColor, 7f)
|
||||||
|
|
||||||
|
|
||||||
// public API
|
// public API
|
||||||
// -------------------------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------------------------
|
||||||
def onInput(text: String): Unit = {}
|
def onInput(text: String): Unit = {}
|
||||||
@ -70,11 +80,14 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
def text: String = new String(_text.chars, 0, _text.chars.length)
|
def text: String = new String(_text.chars, 0, _text.chars.length)
|
||||||
def text_=(value: String): Unit = {
|
def text_=(value: String): Unit = {
|
||||||
_text.chars = value.codePoints().toArray
|
_text.chars = value.codePoints().toArray
|
||||||
selection.active = false
|
selection = None
|
||||||
cursor.position = cursor.position max 0 min _text.chars.length
|
cursor.position = cursor.position max 0 min _text.chars.length
|
||||||
}
|
}
|
||||||
|
|
||||||
private def selectedText: String = new String(_text.chars, selection.start, selection.length)
|
private def selectedText: String = selection match {
|
||||||
|
case Some(Selection.Ordered(start, end)) => new String(_text.chars, start, end)
|
||||||
|
case None => ""
|
||||||
|
}
|
||||||
|
|
||||||
protected var placeholder: Array[Int] = Array.empty
|
protected var placeholder: Array[Int] = Array.empty
|
||||||
def placeholder_=(value: String): Unit = placeholder = value.codePoints().toArray
|
def placeholder_=(value: String): Unit = placeholder = value.codePoints().toArray
|
||||||
@ -96,7 +109,6 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// widget management
|
// widget management
|
||||||
// -------------------------------------------------------------------------------------------------------------------
|
// -------------------------------------------------------------------------------------------------------------------
|
||||||
override def minimumSize: Size2D = Size2D(200, 24)
|
override def minimumSize: Size2D = Size2D(200, 24)
|
||||||
@ -118,7 +130,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
* @param from inclusive
|
* @param from inclusive
|
||||||
* @param to exclusive
|
* @param to exclusive
|
||||||
*/
|
*/
|
||||||
//noinspection SameParameterValue
|
// noinspection SameParameterValue
|
||||||
private def charsWidth(chars: Array[Int], from: Int, to: Int): Int = {
|
private def charsWidth(chars: Array[Int], from: Int, to: Int): Int = {
|
||||||
var width = 0
|
var width = 0
|
||||||
for (index <- (from max 0) until (to min chars.length)) {
|
for (index <- (from max 0) until (to min chars.length)) {
|
||||||
@ -128,79 +140,77 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
}
|
}
|
||||||
|
|
||||||
eventHandlers += {
|
eventHandlers += {
|
||||||
case MouseEvent(MouseEvent.State.Pressed, button) if enabled =>
|
case MouseEvent.StateChanged(MouseEvent.State.Pressed, _) if enabled =>
|
||||||
val inBounds = mouseInBounds
|
val inBounds = mouseInBounds
|
||||||
if (isFocused && !inBounds) unfocus()
|
if (isFocused && !inBounds) unfocus()
|
||||||
if (!isFocused && inBounds) focus()
|
if (!isFocused && inBounds) focus()
|
||||||
|
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x)
|
val pos = pixelToCursorPosition(UiHandler.mousePosition.x - bounds.x)
|
||||||
val clamped = _text.chars.length.min(pos).max(0)
|
cursor.position = pos
|
||||||
// a bit of special logic: right click moves cursor position, but only if we clicked outside the current selection
|
|
||||||
// otherwise it would be impossible to do context operations on the selected text
|
|
||||||
if (button != MouseEvent.Button.Right || !selection.active || clamped < selection.from || clamped > selection.to) {
|
|
||||||
setCursorAndSelectionPosition(clamped)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
eventHandlers += {
|
eventHandlers += {
|
||||||
case MouseEvent(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused && mouseInBounds =>
|
case MouseEvent.StateChanged(MouseEvent.State.Pressed, MouseEvent.Button.Right) if isFocused && mouseInBounds =>
|
||||||
val menu = new ContextMenu
|
val menu = new ContextMenu
|
||||||
if (selection.active) {
|
|
||||||
|
if (selection.nonEmpty) {
|
||||||
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
|
menu.addEntry(ContextMenuEntry("Cut", IconSource.Icons.Cut) { cutSelection() })
|
||||||
menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { copySelection() })
|
menu.addEntry(ContextMenuEntry("Copy", IconSource.Icons.Copy) { copySelection() })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (UiHandler.clipboard.nonEmpty) {
|
if (UiHandler.clipboard.nonEmpty) {
|
||||||
menu.addEntry(ContextMenuEntry("Paste", IconSource.Icons.Paste) { pasteSelection() })
|
menu.addEntry(ContextMenuEntry("Paste", IconSource.Icons.Paste) { pasteSelection() })
|
||||||
}
|
}
|
||||||
if (menu.children.nonEmpty) {
|
|
||||||
menu.addSeparator()
|
if (_text.chars.nonEmpty) {
|
||||||
|
if (menu.children.nonEmpty) {
|
||||||
|
menu.addSeparator()
|
||||||
|
}
|
||||||
|
|
||||||
|
menu.addEntry(ContextMenuEntry("Select all") { selectAll() })
|
||||||
}
|
}
|
||||||
menu.addEntry(ContextMenuEntry("Select all") { selectAll() })
|
|
||||||
root.get.contextMenus.open(menu)
|
root.get.contextMenus.open(menu)
|
||||||
|
|
||||||
|
case MouseEvent.StateChanged(MouseEvent.State.Pressed, _) if isFocused && mouseInBounds =>
|
||||||
|
selection = None
|
||||||
|
|
||||||
case DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused && mouseInBounds =>
|
case DoubleClickEvent(MouseEvent.Button.Left, _) if isFocused && mouseInBounds =>
|
||||||
selectWord()
|
selectWord()
|
||||||
|
|
||||||
case event @ DragEvent(DragEvent.State.Start, MouseEvent.Button.Left, mouse) if isFocused =>
|
case DragEvent(DragEvent.State.Start | DragEvent.State.Drag, MouseEvent.Button.Left, mouse) if isFocused =>
|
||||||
val pos = pixelToCursorPosition(mouse.x - bounds.x)
|
val pos = pixelToCursorPosition(mouse.x - bounds.x)
|
||||||
selection.active = true
|
selection = Selection(selection.fold(prevCursorPosition.value)(_.start), pos)
|
||||||
selection.to = pos
|
|
||||||
cursor.position = 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)
|
|
||||||
selection.to = pos
|
|
||||||
cursor.position = pos
|
|
||||||
event.consume()
|
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
||||||
if (selection.active && !KeyEvents.isShiftDown) {
|
handleKeyMovement(selection match {
|
||||||
setCursorAndSelectionPosition(selection.start)
|
case Some(Selection.Ordered(start, _)) if !KeyEvents.isShiftDown => start
|
||||||
} else if (cursor.position > 0) {
|
case _ => cursor.position - 1
|
||||||
setCursorAndSelectionPosition(cursor.position - 1)
|
})
|
||||||
}
|
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
||||||
if (selection.active && !KeyEvents.isShiftDown) {
|
handleKeyMovement(selection match {
|
||||||
setCursorAndSelectionPosition(selection.end)
|
case Some(Selection.Ordered(_, end)) if !KeyEvents.isShiftDown => end
|
||||||
} else if (cursor.position < _text.chars.length) {
|
case _ => cursor.position + 1
|
||||||
setCursorAndSelectionPosition(cursor.position + 1)
|
})
|
||||||
}
|
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
|
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_HOME, _) if isFocused =>
|
||||||
setCursorAndSelectionPosition(0)
|
handleKeyMovement(0)
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
|
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_END, _) if isFocused =>
|
||||||
setCursorAndSelectionPosition(_text.chars.length)
|
handleKeyMovement(_text.chars.length)
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
|
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_BACK, _) if isFocused =>
|
||||||
if (selection.active) {
|
if (selection.nonEmpty) {
|
||||||
deleteSelection()
|
deleteSelection()
|
||||||
} else {
|
} else {
|
||||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||||
@ -209,10 +219,11 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
cursor.position -= 1
|
cursor.position -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
|
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_DELETE, _) if isFocused =>
|
||||||
if (selection.active) {
|
if (selection.nonEmpty) {
|
||||||
deleteSelection()
|
deleteSelection()
|
||||||
} else {
|
} else {
|
||||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||||
@ -220,6 +231,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
_text.chars = lhs ++ rhs.drop(1)
|
_text.chars = lhs ++ rhs.drop(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_A, _)
|
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_A, _)
|
||||||
@ -233,12 +245,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_C, _)
|
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_C, _)
|
||||||
if isFocused && KeyEvents.isControlDown && selection.active =>
|
if isFocused && KeyEvents.isControlDown && selection.nonEmpty =>
|
||||||
copySelection()
|
copySelection()
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_X, _)
|
case event @ KeyEvent(KeyEvent.State.Press, Keyboard.KEY_X, _)
|
||||||
if isFocused && KeyEvents.isControlDown && selection.active =>
|
if isFocused && KeyEvents.isControlDown && selection.nonEmpty =>
|
||||||
cutSelection()
|
cutSelection()
|
||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
@ -256,7 +268,10 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
event.consume()
|
event.consume()
|
||||||
|
|
||||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl =>
|
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, _, char) if isFocused && !char.isControl =>
|
||||||
if (selection.active) deleteSelection()
|
if (selection.nonEmpty) {
|
||||||
|
deleteSelection()
|
||||||
|
}
|
||||||
|
|
||||||
writeChar(char)
|
writeChar(char)
|
||||||
event.consume()
|
event.consume()
|
||||||
}
|
}
|
||||||
@ -276,36 +291,25 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
pos
|
pos
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private def handleKeyMovement(position: Int): Unit = {
|
||||||
* Generally you can just do `cursor.position = x`.
|
selection = if (KeyEvents.isShiftDown) {
|
||||||
* But this method will take care of correctly repositioning the selection as well.
|
Selection(selection.fold(cursor.position)(_.start), position)
|
||||||
*/
|
|
||||||
private def setCursorAndSelectionPosition(position: Int): Unit = {
|
|
||||||
if (!selection.active && KeyEvents.isShiftDown && cursor.position != position) {
|
|
||||||
selection.active = true
|
|
||||||
selection.from = cursor.position
|
|
||||||
} else if (selection.active && !KeyEvents.isShiftDown) {
|
|
||||||
selection.active = false
|
|
||||||
}
|
|
||||||
cursor.position = position
|
|
||||||
if (selection.active) {
|
|
||||||
selection.to = cursor.position
|
|
||||||
} else {
|
} else {
|
||||||
selection.from = cursor.position
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cursor.position = position
|
||||||
}
|
}
|
||||||
|
|
||||||
private def selectAll(): Unit = {
|
private def selectAll(): Unit = {
|
||||||
selection.active = true
|
selection = Selection(0, _text.chars.length)
|
||||||
selection.from = 0
|
|
||||||
selection.to = _text.chars.length
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private def selectWord(): Unit = {
|
private def selectWord(): Unit = {
|
||||||
selection.active = true
|
val from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
|
||||||
selection.from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
|
|
||||||
val to = _text.chars.indexWhere(isWhitespace, cursor.position)
|
val to = _text.chars.indexWhere(isWhitespace, cursor.position)
|
||||||
selection.to = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
|
val clampedTo = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
|
||||||
|
selection = Selection(from, clampedTo)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def copySelection(): Unit = {
|
private def copySelection(): Unit = {
|
||||||
@ -318,14 +322,15 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
}
|
}
|
||||||
|
|
||||||
private def pasteSelection(): Unit = {
|
private def pasteSelection(): Unit = {
|
||||||
if (selection.active) deleteSelection()
|
if (selection.nonEmpty) deleteSelection()
|
||||||
writeString(UiHandler.clipboard)
|
writeString(UiHandler.clipboard)
|
||||||
}
|
}
|
||||||
|
|
||||||
private def deleteSelection(): Unit = {
|
private def deleteSelection(): Unit = {
|
||||||
selection.active = false
|
for (Selection.Ordered(start, end) <- selection) {
|
||||||
_text.chars = _text.chars.take(selection.start) ++ _text.chars.drop(selection.end)
|
_text.chars = _text.chars.take(start) ++ _text.chars.drop(end)
|
||||||
cursor.position = selection.start
|
cursor.position = start
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private def writeString(string: String): Unit = {
|
private def writeString(string: String): Unit = {
|
||||||
@ -354,7 +359,6 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth
|
if (fullTextWidth > areaWidth && fullTextWidth - scroll < areaWidth) scroll = fullTextWidth - areaWidth
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override def update(): Unit = {
|
override def update(): Unit = {
|
||||||
super.update()
|
super.update()
|
||||||
|
|
||||||
@ -373,12 +377,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update everything
|
// update everything
|
||||||
|
prevCursorPosition.update()
|
||||||
foregroundAnimation.update()
|
foregroundAnimation.update()
|
||||||
borderAnimation.update()
|
borderAnimation.update()
|
||||||
blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime
|
blinkTimer = (blinkTimer + UiHandler.dt) % CursorBlinkTime
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private def updateAnimationTargets(): Unit = {
|
private def updateAnimationTargets(): Unit = {
|
||||||
foregroundAnimation.goto(targetForegroundColor)
|
foregroundAnimation.goto(targetForegroundColor)
|
||||||
borderAnimation.goto(targetBorderColor)
|
borderAnimation.goto(targetBorderColor)
|
||||||
@ -406,9 +410,9 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
|
|
||||||
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
|
g.setScissor(position.x + 4, position.y, size.width - 8f, size.height)
|
||||||
|
|
||||||
if (selection.active) {
|
for ((start, end) <- selectionOffsets) {
|
||||||
val width = selectionEndOffset - selectionStartOffset
|
val width = end - start
|
||||||
g.rect(position.x + selectionStartOffset + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor)
|
g.rect(position.x + start + 8 - scroll, position.y + 4, width, size.height - 8, BackgroundSelectedColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
g.background = Color.Transparent
|
g.background = Color.Transparent
|
||||||
@ -418,9 +422,10 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
|||||||
var charOffset = 0
|
var charOffset = 0
|
||||||
val charsToDisplay = if (_text.chars.nonEmpty || isFocused) _text.chars else placeholder
|
val charsToDisplay = if (_text.chars.nonEmpty || isFocused) _text.chars else placeholder
|
||||||
for (char <- charsToDisplay) {
|
for (char <- charsToDisplay) {
|
||||||
if (selection.active) {
|
for ((start, end) <- selectionOffsets) {
|
||||||
g.foreground = if (charOffset >= selectionStartOffset && charOffset < selectionEndOffset) ForegroundSelectedColor else foreground
|
g.foreground = if (charOffset >= start && charOffset < end) ForegroundSelectedColor else foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
|
g.char(position.x + 8 + charOffset - scroll, position.y + 4, char)
|
||||||
charOffset += g.font.charWidth(char)
|
charOffset += g.font.charWidth(char)
|
||||||
}
|
}
|
||||||
@ -436,29 +441,29 @@ object TextInput {
|
|||||||
def chars: Array[Int] = value
|
def chars: Array[Int] = value
|
||||||
def chars_=(newValue: Array[Int]): Unit = value = newValue
|
def chars_=(newValue: Array[Int]): Unit = value = newValue
|
||||||
}
|
}
|
||||||
|
|
||||||
class Cursor(initialValue: Int = 0) extends Watcher(initialValue) {
|
class Cursor(initialValue: Int = 0) extends Watcher(initialValue) {
|
||||||
def position: Int = value
|
def position: Int = value
|
||||||
def position_=(newValue: Int): Unit = value = newValue
|
def position_=(newValue: Int): Unit = value = newValue
|
||||||
}
|
}
|
||||||
class Selection {
|
|
||||||
private val fromWatcher = Watcher(0)
|
|
||||||
private val toWatcher = Watcher(0)
|
|
||||||
|
|
||||||
def from: Int = fromWatcher.value
|
case class Selection(start: Int, end: Int) {
|
||||||
def from_=(x: Int): Unit = fromWatcher.value = x
|
require(start != end)
|
||||||
|
}
|
||||||
|
|
||||||
def to: Int = toWatcher.value
|
object Selection {
|
||||||
def to_=(x: Int): Unit = toWatcher.value = x
|
def apply(start: Int, end: Int): Option[Selection] = {
|
||||||
|
Option.when(start != end) {
|
||||||
|
new Selection(start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
def start: Int = from min to
|
object Ordered {
|
||||||
def end: Int = from max to
|
def unapply(selection: Selection): Some[(Int, Int)] = {
|
||||||
def length: Int = end - start
|
val Selection(start, end) = selection
|
||||||
|
|
||||||
var active: Boolean = false
|
Some((start min end, start max end))
|
||||||
|
}
|
||||||
def onChange(callback: (Int, Int) => Unit): Unit = {
|
|
||||||
fromWatcher.onChange(callback(_, to))
|
|
||||||
toWatcher.onChange(callback(from, _))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user