diff --git a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala index 3de6af8..c25b1b6 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/TextInput.scala @@ -8,7 +8,7 @@ import ocelot.desktop.ui.UiHandler import ocelot.desktop.ui.event.handlers.MouseHandler import ocelot.desktop.ui.event.sources.KeyEvents import ocelot.desktop.ui.event.{DragEvent, KeyEvent, MouseEvent} -import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text} +import ocelot.desktop.ui.widget.TextInput.{Cursor, Selection, Text, isPunctuation} import ocelot.desktop.ui.widget.contextmenu.{ContextMenu, ContextMenuEntry} import ocelot.desktop.ui.widget.traits.HoverAnimation import ocelot.desktop.util.NumberUtils.ExtendedInt @@ -209,7 +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 _ if KeyEvents.isControlDown => findWordBoundary(cursor.position - 1, forward = false, wordStart = true, punctuationBoundaries = false) case _ => cursor.position - 1 }) @@ -218,7 +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 _ if KeyEvents.isControlDown => findWordBoundary(cursor.position + 1, forward = true, wordStart = false, punctuationBoundaries = false) case _ => cursor.position + 1 }) @@ -237,7 +237,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w deleteSelection() } else { val start = if (KeyEvents.isControlDown) { - findWordBoundary(cursor.position - 1, forward = false, wordStart = true) + findWordBoundary(cursor.position - 1, forward = false, wordStart = true, punctuationBoundaries = false) } else { (cursor.position - 1).clamped(0, _text.chars.length) } @@ -253,7 +253,7 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w deleteSelection() } else { val end = if (KeyEvents.isControlDown) { - findWordBoundary(cursor.position + 1, forward = true, wordStart = false) + findWordBoundary(cursor.position + 1, forward = true, wordStart = false, punctuationBoundaries = false) } else { (cursor.position + 1).clamped(0, _text.chars.length) } @@ -344,9 +344,12 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w * 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. * + * If `punctuationBoundaries` is enabled, a contiguous punctuation sequence is treated as a word for the purposes of + * boundary search. + * * May return the `initialPosition` if it's already at the boundary. */ - private def findWordBoundary(initialPosition: Int, forward: Boolean, wordStart: Boolean): Int = { + private def findWordBoundary(initialPosition: Int, forward: Boolean, wordStart: Boolean, punctuationBoundaries: Boolean): Int = { import Character.isLetterOrDigit val start = initialPosition.clamped(0, _text.chars.length) @@ -358,9 +361,15 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w } val isBoundaryCodepoints: (Int, Int) => Boolean = if (wordStart) { - (prev, current) => !isLetterOrDigit(prev) && isLetterOrDigit(current) + (prev, current) => ( + isLetterOrDigit(current) && !isLetterOrDigit(prev) + || punctuationBoundaries && isPunctuation(current) && !isPunctuation(prev) + ) } else { - (prev, current) => isLetterOrDigit(prev) && !isLetterOrDigit(current) + (prev, current) => ( + !isLetterOrDigit(current) && isLetterOrDigit(prev) + || punctuationBoundaries && !isPunctuation(current) && isPunctuation(prev) + ) } def isBoundary(idx: Int): Boolean = { @@ -371,8 +380,8 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w } private def selectWord(): Unit = { - val from = findWordBoundary(cursor.position, forward = false, wordStart = true) - val to = findWordBoundary(cursor.position, forward = true, wordStart = false) + val from = findWordBoundary(cursor.position, forward = false, wordStart = true, punctuationBoundaries = true) + val to = findWordBoundary(from + 1, forward = true, wordStart = false, punctuationBoundaries = true) selection = Selection(from, to) } @@ -507,6 +516,20 @@ class TextInput(val initialText: String = "") extends Widget with MouseHandler w } object TextInput { + private val PunctuationCategories = Array[Int]( + Character.CONNECTOR_PUNCTUATION, + Character.DASH_PUNCTUATION, + Character.END_PUNCTUATION, + Character.FINAL_QUOTE_PUNCTUATION, + Character.INITIAL_QUOTE_PUNCTUATION, + Character.OTHER_PUNCTUATION, + Character.START_PUNCTUATION, + ) + + private def isPunctuation(codepoint: Int): Boolean = { + PunctuationCategories.contains(Character.getType(codepoint)) + } + class Text(initialValue: Array[Int]) extends BaseWatcher(initialValue) { def chars: Array[Int] = value def chars_=(newValue: Array[Int]): Unit = value = newValue