@@ -3,6 +3,7 @@ package org.openrndr.panel.elements
33import org.openrndr.*
44import org.openrndr.color.ColorRGBa
55import org.openrndr.draw.Drawer
6+ import org.openrndr.draw.isolated
67import org.openrndr.events.Event
78import org.openrndr.extra.textwriter.Cursor
89import org.openrndr.extra.textwriter.writer
@@ -13,6 +14,7 @@ import org.openrndr.panel.style.borderColor
1314import org.openrndr.panel.style.color
1415import org.openrndr.panel.style.effectiveBackground
1516import org.openrndr.shape.Rectangle
17+ import org.openrndr.shape.bounds
1618import kotlin.math.max
1719import kotlin.math.min
1820import kotlin.reflect.KMutableProperty0
@@ -27,14 +29,16 @@ class TextInput : Element(ElementType("text-input")) {
2729 set(value) {
2830 if (ivalue != value) {
2931 ivalue = value
30- inputIndex = value.length - 1
32+ selectionEnd = value.lastIndex
33+ selectionStart = selectionEnd
3134 requestRedraw()
3235 }
3336 }
3437 get() = ivalue
3538
3639
37- private var inputIndex = value.length - 1
40+ private var selectionStart = value.lastIndex
41+ private var selectionEnd = value.lastIndex
3842
3943 class ValueChangedEvent (val source : TextInput , val oldValue : String , val newValue : String )
4044 class Events : AutoCloseable {
@@ -47,51 +51,96 @@ class TextInput : Element(ElementType("text-input")) {
4751
4852 val events = Events ()
4953
54+ // Used for jumping to the previous or next word in the text input
55+ private fun nextCharTypeDiffers (direction : Int ): Boolean {
56+ val a = selectionEnd + if (direction < 0 ) 1 else 0
57+ val b = a + direction
58+ return a !in 0 .. value.lastIndex ||
59+ b !in 0 .. value.lastIndex ||
60+ value[a].isLetterOrDigit() != value[b].isLetterOrDigit()
61+ }
62+
63+ // Updates inputIndex. direction 1 is right, -1 is left
64+ private fun moveSelectionEnd (direction : Int , useWordBoundary : Boolean = false) {
65+ do {
66+ selectionEnd = (selectionEnd + direction).coerceIn(- 1 , value.lastIndex)
67+ } while (useWordBoundary && ! nextCharTypeDiffers(direction))
68+ }
69+
70+ private fun String.dropRange (fromIndex : Int , toIndex : Int ) = this .filterIndexed { i, _ ->
71+ i !in fromIndex.. toIndex
72+ }
73+
74+ private fun deleteChars (direction : Int ) {
75+ if (value.isNotEmpty()) {
76+ val oldValue = value
77+
78+ if (selectionStart == selectionEnd) {
79+ selectionStart = (selectionEnd + direction).coerceIn(- 1 , value.lastIndex)
80+ }
81+
82+ if (selectionStart > selectionEnd) {
83+ selectionStart = selectionEnd.also { selectionEnd = selectionStart }
84+ }
85+
86+ val a = (min(selectionStart, selectionEnd) + 1 ).coerceIn(0 , value.lastIndex)
87+ val b = (max(selectionStart, selectionEnd)).coerceIn(0 , value.lastIndex)
88+
89+ ivalue = value.dropRange(a, b)
90+ selectionEnd = selectionStart
91+ events.valueChanged.trigger(ValueChangedEvent (this , oldValue, value))
92+ }
93+ }
94+
5095 init {
5196 keyboard.pressed.listen {
5297 if (KeyModifier .CTRL in it.modifiers || KeyModifier .SUPER in it.modifiers) {
53- if (it.name == " v" ) {
54- val oldValue = value
55- (root() as Body ).controlManager?.program?.clipboard?.contents?.let {
56- ivalue + = it
98+ when (it.name) {
99+ " v" -> {
100+ // Paste
101+ val oldValue = value
102+ (root() as Body ).controlManager?.program?.clipboard?.contents?.let {
103+ ivalue + = it
104+ }
105+ events.valueChanged.trigger(ValueChangedEvent (this , oldValue, value))
106+ it.cancelPropagation()
107+ }
108+
109+ " a" -> {
110+ // Select all
111+ selectionStart = - 1
112+ selectionEnd = value.lastIndex
57113 }
58- events.valueChanged.trigger(ValueChangedEvent (this , oldValue, value))
59- it.cancelPropagation()
60114 }
61115 }
62- when (it.key) {
63- KEY_HOME -> inputIndex = - 1
64- KEY_END -> inputIndex = value.length - 1
65- KEY_ARROW_LEFT -> inputIndex = max(- 1 , inputIndex - 1 )
66- KEY_ARROW_RIGHT -> inputIndex = min(value.length - 1 , inputIndex + 1 )
67116
68- KEY_DELETE -> {
69- if (value.isNotEmpty()) {
70- val oldValue = value
117+ when (it.key) {
118+ KEY_HOME -> {
119+ selectionEnd = - 1
120+ if (KeyModifier .SHIFT !in it.modifiers) selectionStart = selectionEnd
121+ }
71122
72- if (inputIndex == - 1 ) {
73- ivalue = value.drop(1 )
74- } else if (inputIndex < value.length - 1 ) {
75- ivalue = value.take(inputIndex + 1 ) + value.drop(inputIndex + 2 )
76- }
77- inputIndex = min(inputIndex, value.length - 1 )
78- events.valueChanged.trigger(ValueChangedEvent (this , oldValue, value))
79- }
123+ KEY_END -> {
124+ selectionEnd = value.length - 1
125+ if (KeyModifier .SHIFT !in it.modifiers) selectionStart = selectionEnd
80126 }
81127
82- KEY_BACKSPACE -> {
83- if (value.isNotEmpty()) {
84- val oldValue = value
128+ KEY_ARROW_LEFT -> {
129+ moveSelectionEnd(
130+ - 1 , KeyModifier .CTRL in it.modifiers || KeyModifier .SUPER in it.modifiers
131+ )
132+ if (KeyModifier .SHIFT !in it.modifiers) selectionStart = selectionEnd
133+ }
85134
86- if (inputIndex == value.length - 1 ) {
87- ivalue = value.dropLast(1 )
88- } else if (inputIndex > - 1 ) {
89- ivalue = value.take(inputIndex) + value.drop(inputIndex + 1 )
90- }
91- inputIndex = max(- 1 , inputIndex - 1 )
92- events.valueChanged.trigger(ValueChangedEvent (this , oldValue, value))
93- }
135+ KEY_ARROW_RIGHT -> {
136+ moveSelectionEnd(
137+ 1 , KeyModifier .CTRL in it.modifiers || KeyModifier .SUPER in it.modifiers
138+ )
139+ if (KeyModifier .SHIFT !in it.modifiers) selectionStart = selectionEnd
94140 }
141+
142+ KEY_DELETE -> deleteChars(1 )
143+ KEY_BACKSPACE -> deleteChars(- 1 )
95144 }
96145 requestRedraw()
97146 it.cancelPropagation()
@@ -100,8 +149,10 @@ class TextInput : Element(ElementType("text-input")) {
100149 keyboard.character.listen {
101150 it.cancelPropagation()
102151 val oldValue = value
103- ivalue = value.take(inputIndex + 1 ) + it.character.toString() + value.drop(inputIndex + 1 )
104- inputIndex++
152+ if (selectionStart != selectionEnd) deleteChars(0 )
153+ ivalue = value.take(selectionEnd + 1 ) + it.character.toString() + value.drop(selectionEnd + 1 )
154+ selectionEnd++
155+ selectionStart = selectionEnd
105156 events.valueChanged.trigger(ValueChangedEvent (this , oldValue, value))
106157 requestRedraw()
107158 }
@@ -124,37 +175,48 @@ class TextInput : Element(ElementType("text-input")) {
124175 val font = it.font(computedStyle)
125176 val textHeight = font.ascenderLength
126177 val yOffset = ((layout.screenHeight / 2 ) + textHeight / 2.0 - 2.0 ).round(0 )
178+ val xOffset = layout.contentBoundsAtOrigin.x
179+ var caretX: Double? = null
180+ val isInputActive = ElementPseudoClass (" active" ) in pseudoClasses
127181
128182 // drawer.rectangle(layout.contentBoundsAtOrigin)
129-
130183 drawer.drawStyle.clip = layout.contentBounds
131184 drawer.fontMap = font
132185 drawer.fill = ((computedStyle.color as ? Color .RGBa )?.color ? : ColorRGBa .WHITE )
133-
134- val xOffset = layout.contentBoundsAtOrigin.x
135- var caretX: Double? = null
186+ var selectionArea: Rectangle ? = null
136187 writer(drawer) {
137188 cursor = Cursor (xOffset, yOffset)
138189 text(value, visible = false )
139190 glyphRectangles = glyphOutput.rectangles
140191
141192 var scroll = 2.0
142- if (ElementPseudoClass ( " active " ) in pseudoClasses ) {
193+ if (isInputActive ) {
143194 caretX = if (glyphRectangles.isNotEmpty()) {
144- if (inputIndex == glyphRectangles.lastIndex)
195+ if (selectionEnd == glyphRectangles.lastIndex)
145196 glyphRectangles.last().second.position(1.0 , 0.0 ).x
146197 else
147- glyphRectangles[inputIndex + 1 ].second.position(0.0 , 0.0 ).x
198+ glyphRectangles[selectionEnd + 1 ].second.position(0.0 , 0.0 ).x
148199 } else xOffset
149200 // Calculate scroll value when the caret is outside the text box
150201 val layoutMaxX = layout.contentBounds.position(1.0 , 0.0 ).x
151202 val rightPadding = textWidth(" m" ) * 2
152- if (caretX > layoutMaxX - rightPadding) {
153- scroll = layoutMaxX - rightPadding - caretX
154- }
203+ scroll = (layoutMaxX - rightPadding - caretX).coerceAtMost(2.0 )
155204 caretX + = scroll
156205 }
157206
207+ if (selectionStart != selectionEnd) {
208+ val a = min(selectionStart, selectionEnd) + 1
209+ val b = max(selectionStart, selectionEnd) + 1
210+ val selectionBounds = glyphRectangles.subList(a, b).map { it.second }.bounds
211+ val y = yOffset - font.descenderLength
212+ drawer.isolated {
213+ stroke = null
214+ fill = ColorRGBa .BLACK
215+ translate(scroll, 0.0 )
216+ rectangle(selectionBounds.x, y, selectionBounds.width, - textHeight)
217+ }
218+ }
219+
158220 cursor = Cursor (xOffset + scroll, yOffset)
159221 text(value)
160222 }
0 commit comments