mirror of
https://gitlab.com/cc-ru/ocelot/ocelot-desktop.git
synced 2025-12-20 02:59:19 +01:00
Implement text navigation via Ctrl keybings
This commit is contained in:
parent
7f0fccae80
commit
8f291c4a80
@ -1,6 +1,6 @@
|
||||
package ocelot.desktop.color
|
||||
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry.Vector3D
|
||||
|
||||
import java.nio.ByteBuffer
|
||||
|
||||
@ -1,9 +0,0 @@
|
||||
package ocelot.desktop.geometry
|
||||
|
||||
object FloatUtils {
|
||||
implicit class ExtendedFloat(val v: Float) extends AnyVal {
|
||||
def lerp(that: Float, alpha: Float): Float = v * (1 - alpha) + that * alpha
|
||||
|
||||
def clamped(min: Float = 0f, max: Float = 1f): Float = v.min(max).max(min)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
package ocelot.desktop.geometry
|
||||
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
|
||||
object Vector3D {
|
||||
val Zero: Vector3D = Vector3D(0, 0, 0)
|
||||
|
||||
@ -2,7 +2,7 @@ package ocelot.desktop.node
|
||||
|
||||
import ocelot.desktop.audio.{SoundBuffers, SoundCategory, SoundSource}
|
||||
import ocelot.desktop.color.Color
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.graphics.{Graphics, IconSource}
|
||||
import ocelot.desktop.inventory.item.SelfDestructingCardItem
|
||||
import ocelot.desktop.node.BoomCardFxHandler.{ExpandIntensity, ExpandPeriod, FlickerAlpha, FlickerDuty, GlowAlpha, MaxSize, MinSize}
|
||||
|
||||
@ -2,7 +2,7 @@ package ocelot.desktop.node
|
||||
|
||||
import ocelot.desktop.ColorScheme
|
||||
import ocelot.desktop.entity.traits.OcelotInterface
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry.Vector2D
|
||||
import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.node.OcelotLogParticleNode._
|
||||
|
||||
@ -3,7 +3,7 @@ package ocelot.desktop.node.nodes
|
||||
import ocelot.desktop.color.RGBAColorNorm
|
||||
import ocelot.desktop.entity.OcelotBlock
|
||||
import ocelot.desktop.entity.traits.OcelotInterface
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.graphics.{Graphics, IconSource}
|
||||
import ocelot.desktop.node.Node.HighlightThickness
|
||||
import ocelot.desktop.node.nodes.OcelotBlockNode.ActivityFadeOutMs
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package ocelot.desktop.ui.widget
|
||||
|
||||
import ocelot.desktop.ColorScheme
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
|
||||
import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.ui.UiHandler
|
||||
|
||||
@ -3,7 +3,7 @@ package ocelot.desktop.ui.widget
|
||||
import ocelot.desktop.ColorScheme
|
||||
import ocelot.desktop.audio.{ClickSoundSource, SoundSource}
|
||||
import ocelot.desktop.color.Color
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry.Size2D
|
||||
import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.ui.event.handlers.MouseHandler
|
||||
|
||||
@ -11,12 +11,11 @@ import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent}
|
||||
import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text}
|
||||
import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry}
|
||||
import ocelot.desktop.ui.widget.traits.HoverAnimation
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedInt
|
||||
import ocelot.desktop.util.animation.ColorAnimation
|
||||
import ocelot.desktop.util.{BaseWatcher, DrawUtils, Register, Watcher}
|
||||
import org.lwjgl.input.Keyboard
|
||||
|
||||
import java.lang.Character.isWhitespace
|
||||
|
||||
class TextInput(val initialText: String = "") extends Widget with MouseHandler with HoverAnimation {
|
||||
private val CursorBlinkTime = 2f
|
||||
private val PlaceholderForegroundColor: Color = ColorScheme("TextInputForegroundDisabled")
|
||||
@ -210,6 +209,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_LEFT, _) if isFocused =>
|
||||
handleKeyMovement(selection match {
|
||||
case Some(Selection.Ordered(start, _)) if !KeyEvents.isShiftDown => start
|
||||
case _ if KeyEvents.isControlDown => findWordBoundary(cursor.position - 1, forward = false, wordStart = true)
|
||||
case _ => cursor.position - 1
|
||||
})
|
||||
|
||||
@ -218,6 +218,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
case event @ KeyEvent(KeyEvent.State.Press | KeyEvent.State.Repeat, Keyboard.KEY_RIGHT, _) if isFocused =>
|
||||
handleKeyMovement(selection match {
|
||||
case Some(Selection.Ordered(_, end)) if !KeyEvents.isShiftDown => end
|
||||
case _ if KeyEvents.isControlDown => findWordBoundary(cursor.position + 1, forward = true, wordStart = false)
|
||||
case _ => cursor.position + 1
|
||||
})
|
||||
|
||||
@ -235,11 +236,14 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
if (selection.nonEmpty) {
|
||||
deleteSelection()
|
||||
} else {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
if (!lhs.isEmpty) {
|
||||
_text.chars = lhs.take(lhs.length - 1) ++ rhs
|
||||
cursor.position -= 1
|
||||
val start = if (KeyEvents.isControlDown) {
|
||||
findWordBoundary(cursor.position - 1, forward = false, wordStart = true)
|
||||
} else {
|
||||
(cursor.position - 1).clamped(0, _text.chars.length)
|
||||
}
|
||||
|
||||
deleteRange(start, cursor.position)
|
||||
cursor.position = start
|
||||
}
|
||||
|
||||
event.consume()
|
||||
@ -248,10 +252,13 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
if (selection.nonEmpty) {
|
||||
deleteSelection()
|
||||
} else {
|
||||
val (lhs, rhs) = _text.chars.splitAt(cursor.position)
|
||||
if (!rhs.isEmpty) {
|
||||
_text.chars = lhs ++ rhs.drop(1)
|
||||
val end = if (KeyEvents.isControlDown) {
|
||||
findWordBoundary(cursor.position + 1, forward = true, wordStart = false)
|
||||
} else {
|
||||
(cursor.position + 1).clamped(0, _text.chars.length)
|
||||
}
|
||||
|
||||
deleteRange(cursor.position, end)
|
||||
}
|
||||
|
||||
event.consume()
|
||||
@ -331,11 +338,42 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
selection = Selection(0, _text.chars.length)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a word boundary closest to the `initialPosition` going in the specified direction.
|
||||
*
|
||||
* If `wordStart` is `true`, the word boundary begins a word. Otherwise, it ends a word (one past the last character
|
||||
* of the word). If no boundary is found, the function returns the furthest index in the specified direction.
|
||||
*
|
||||
* May return the `initialPosition` if it's already at the boundary.
|
||||
*/
|
||||
private def findWordBoundary(initialPosition: Int, forward: Boolean, wordStart: Boolean): Int = {
|
||||
import Character.isLetterOrDigit
|
||||
|
||||
val start = initialPosition.clamped(0, _text.chars.length)
|
||||
|
||||
val indices = if (forward) {
|
||||
start.to(_text.chars.length)
|
||||
} else {
|
||||
start.to(0, -1)
|
||||
}
|
||||
|
||||
val isBoundaryCodepoints: (Int, Int) => Boolean = if (wordStart) {
|
||||
(prev, current) => !isLetterOrDigit(prev) && isLetterOrDigit(current)
|
||||
} else {
|
||||
(prev, current) => isLetterOrDigit(prev) && !isLetterOrDigit(current)
|
||||
}
|
||||
|
||||
def isBoundary(idx: Int): Boolean = {
|
||||
idx == 0 || idx == _text.chars.length || isBoundaryCodepoints(_text.chars(idx - 1), _text.chars(idx))
|
||||
}
|
||||
|
||||
indices.find(isBoundary).getOrElse(indices.end)
|
||||
}
|
||||
|
||||
private def selectWord(): Unit = {
|
||||
val from = 0 max (_text.chars.lastIndexWhere(isWhitespace, cursor.position - 1) + 1)
|
||||
val to = _text.chars.indexWhere(isWhitespace, cursor.position)
|
||||
val clampedTo = if (to >= 0 && to < _text.chars.length) to else _text.chars.length
|
||||
selection = Selection(from, clampedTo)
|
||||
val from = findWordBoundary(cursor.position, forward = false, wordStart = true)
|
||||
val to = findWordBoundary(cursor.position, forward = true, wordStart = false)
|
||||
selection = Selection(from, to)
|
||||
}
|
||||
|
||||
private def copySelection(): Unit = {
|
||||
@ -354,9 +392,15 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w
|
||||
|
||||
private def deleteSelection(): Unit = {
|
||||
for (Selection.Ordered(start, end) <- selection) {
|
||||
_text.chars = _text.chars.take(start) ++ _text.chars.drop(end)
|
||||
deleteRange(start, end)
|
||||
cursor.position = start
|
||||
}
|
||||
|
||||
selection = None
|
||||
}
|
||||
|
||||
private def deleteRange(start: Int, end: Int): Unit = {
|
||||
_text.chars = _text.chars.take(start) ++ _text.chars.drop(end)
|
||||
}
|
||||
|
||||
private def writeString(string: String): Unit = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
package ocelot.desktop.ui.widget
|
||||
|
||||
import ocelot.desktop.color.Color
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry._
|
||||
import ocelot.desktop.graphics.scene.{Camera3D, Scene3D}
|
||||
import ocelot.desktop.graphics.{Graphics, IconSource, Viewport3D}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
package ocelot.desktop.ui.widget.contextmenu
|
||||
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry.Vector2D
|
||||
import ocelot.desktop.graphics.Graphics
|
||||
import ocelot.desktop.ui.UiHandler
|
||||
|
||||
17
src/main/scala/ocelot/desktop/util/NumberUtils.scala
Normal file
17
src/main/scala/ocelot/desktop/util/NumberUtils.scala
Normal file
@ -0,0 +1,17 @@
|
||||
package ocelot.desktop.util
|
||||
|
||||
object NumberUtils {
|
||||
implicit class ExtendedFloat(val v: Float) extends AnyVal {
|
||||
def lerp(that: Float, alpha: Float): Float = v * (1 - alpha) + that * alpha
|
||||
|
||||
def clamped(min: Float = 0f, max: Float = 1f): Float = v.min(max).max(min)
|
||||
}
|
||||
|
||||
implicit class ExtendedInt(val v: Int) extends AnyVal {
|
||||
def clamped(min: Int, max: Int): Int = v.min(max).max(min)
|
||||
}
|
||||
|
||||
implicit class ExtendedLong(val v: Long) extends AnyVal {
|
||||
def clamped(min: Long, max: Long): Float = v.min(max).max(min)
|
||||
}
|
||||
}
|
||||
@ -2,7 +2,7 @@ package ocelot.desktop.windows
|
||||
|
||||
import ocelot.desktop.OcelotDesktop
|
||||
import ocelot.desktop.color.{Color, IntColor}
|
||||
import ocelot.desktop.geometry.FloatUtils.ExtendedFloat
|
||||
import ocelot.desktop.util.NumberUtils.ExtendedFloat
|
||||
import ocelot.desktop.geometry._
|
||||
import ocelot.desktop.graphics.IconSource
|
||||
import ocelot.desktop.graphics.mesh.{Mesh3D, MeshBuilder3D}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user