From 38badb316d777c7c54b622cd0614c4ac421d0583 Mon Sep 17 00:00:00 2001
From: HollowHorizon <artemm@xaker.ru>
Date: Fri, 6 Dec 2024 22:20:03 +0300
Subject: [PATCH] better word selection & text area line customization

---
 .../de/fabmax/kool/modules/ui2/TextArea.kt    | 65 ++++++++++---------
 .../fabmax/kool/util/TextCaretNavigation.kt   | 17 +++--
 2 files changed, 46 insertions(+), 36 deletions(-)

diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/TextArea.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/TextArea.kt
index 49b0e10cc..0087d176c 100644
--- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/TextArea.kt
+++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/modules/ui2/TextArea.kt
@@ -172,38 +172,45 @@ open class TextAreaNode(parent: UiNode?, surface: UiSurface) : BoxNode(parent, s
         selectionHandler.updateSelectionRange()
         linesHolder.indices(lineProvider.size) { lineIndex ->
             val line = lineProvider[lineIndex]
-            AttributedText(line) {
-                modifier.width(Grow.MinFit)
-
-                if (this@TextAreaNode.modifier.onSelectionChanged != null) {
-                    modifier
-                        .onClick {
-                            when (it.pointer.leftButtonRepeatedClickCount) {
-                                1 -> selectionHandler.onSelectStart(this, lineIndex, it, false)
-                                2 -> selectionHandler.selectWord(this, line.text, lineIndex, it)
-                                3 -> selectionHandler.selectLine(this, line.text, lineIndex)
-                            }
-                        }
-                        .onDragStart { selectionHandler.onSelectStart(this, lineIndex, it, true) }
-                        .onDrag { selectionHandler.onDrag(it) }
-                        .onDragEnd { selectionHandler.onSelectEnd() }
-                        .onPointer { selectionHandler.onPointer(this, lineIndex, it) }
-
-                    modifier.padding(start = textAreaMod.lineStartPadding, end = textAreaMod.lineEndPadding)
-                    if (lineIndex == 0) {
-                        modifier
-                            .textAlignY(AlignmentY.Bottom)
-                            .padding(top = textAreaMod.firstLineTopPadding)
-                    }
-                    if (lineIndex == lineProvider.lastIndex) {
-                        modifier
-                            .textAlignY(AlignmentY.Top)
-                            .padding(bottom = textAreaMod.lastLineBottomPadding)
-                    }
+            setupTextLine(line, lineIndex, textAreaMod, lineProvider)
+        }
+    }
 
-                    selectionHandler.applySelectionRange(this, line, lineIndex)
+    protected fun UiScope.setupTextLine(
+        line: TextLine,
+        lineIndex: Int,
+        textAreaMod: TextAreaModifier,
+        lineProvider: TextLineProvider,
+    ) = AttributedText(line) {
+        modifier.width(Grow.MinFit)
+
+        if (this@TextAreaNode.modifier.onSelectionChanged != null) {
+            modifier
+                .onClick {
+                    when (it.pointer.leftButtonRepeatedClickCount) {
+                        1 -> selectionHandler.onSelectStart(this, lineIndex, it, false)
+                        2 -> selectionHandler.selectWord(this, line.text, lineIndex, it)
+                        3 -> selectionHandler.selectLine(this, line.text, lineIndex)
+                    }
                 }
+                .onDragStart { selectionHandler.onSelectStart(this, lineIndex, it, true) }
+                .onDrag { selectionHandler.onDrag(it) }
+                .onDragEnd { selectionHandler.onSelectEnd() }
+                .onPointer { selectionHandler.onPointer(this, lineIndex, it) }
+
+            modifier.padding(start = textAreaMod.lineStartPadding, end = textAreaMod.lineEndPadding)
+            if (lineIndex == 0) {
+                modifier
+                    .textAlignY(AlignmentY.Bottom)
+                    .padding(top = textAreaMod.firstLineTopPadding)
             }
+            if (lineIndex == lineProvider.lastIndex) {
+                modifier
+                    .textAlignY(AlignmentY.Top)
+                    .padding(bottom = textAreaMod.lastLineBottomPadding)
+            }
+
+            selectionHandler.applySelectionRange(this, line, lineIndex)
         }
     }
 
diff --git a/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/TextCaretNavigation.kt b/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/TextCaretNavigation.kt
index 979752297..bb75b10e4 100644
--- a/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/TextCaretNavigation.kt
+++ b/kool-core/src/commonMain/kotlin/de/fabmax/kool/util/TextCaretNavigation.kt
@@ -3,17 +3,20 @@ package de.fabmax.kool.util
 import de.fabmax.kool.math.clamp
 
 object TextCaretNavigation {
+    private val LIMITING_CHARS = charArrayOf(' ', '(', '{', '[', '<', ')', '}', ']', '>', ',', '.')
+
+    private fun Char.isLimitingChar() = this in LIMITING_CHARS
 
     fun startOfWord(text: String, caretPos: Int): Int {
         var i = caretPos.clamp(0, text.lastIndex)
-        while (i > 0 && !text[i].isWhitespace()) i--
-        if (text[i].isWhitespace()) i++
+        while (i > 0 && !text[i].isLimitingChar()) i--
+        if (text[i].isLimitingChar()) i++
         return i
     }
 
     fun endOfWord(text: String, caretPos: Int): Int {
         var i = caretPos.clamp(0, text.lastIndex)
-        while (i < text.length && !text[i].isWhitespace()) i++
+        while (i < text.length && !text[i].isLimitingChar()) i++
         return i
     }
 
@@ -21,9 +24,9 @@ object TextCaretNavigation {
         var i = (caretPos - 1).clamp(0, text.lastIndex)
         return when {
             i == 0 -> 0
-            !text[i].isWhitespace() -> startOfWord(text, i)
+            !text[i].isLimitingChar() -> startOfWord(text, i)
             else -> {
-                while (i > 0 && text[i].isWhitespace()) i--
+                while (i > 0 && text[i].isLimitingChar()) i--
                 startOfWord(text, i)
             }
         }
@@ -33,9 +36,9 @@ object TextCaretNavigation {
         var i = (caretPos + 1).clamp(0, text.length)
         return when {
             i == text.length -> text.length
-            !text[i].isWhitespace() -> endOfWord(text, i)
+            !text[i].isLimitingChar() -> endOfWord(text, i)
             else -> {
-                while (i < text.length && text[i].isWhitespace()) i++
+                while (i < text.length && text[i].isLimitingChar()) i++
                 endOfWord(text, i)
             }
         }