Skip to content

Commit 6ebe9e5

Browse files
committed
[orx-panel] Add selection to TextInput
Also select all, delete selection, jump between words.
1 parent a198005 commit 6ebe9e5

1 file changed

Lines changed: 109 additions & 47 deletions

File tree

  • orx-jvm/orx-panel/src/main/kotlin/org/openrndr/panel/elements

orx-jvm/orx-panel/src/main/kotlin/org/openrndr/panel/elements/TextInput.kt

Lines changed: 109 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package org.openrndr.panel.elements
33
import org.openrndr.*
44
import org.openrndr.color.ColorRGBa
55
import org.openrndr.draw.Drawer
6+
import org.openrndr.draw.isolated
67
import org.openrndr.events.Event
78
import org.openrndr.extra.textwriter.Cursor
89
import org.openrndr.extra.textwriter.writer
@@ -13,6 +14,7 @@ import org.openrndr.panel.style.borderColor
1314
import org.openrndr.panel.style.color
1415
import org.openrndr.panel.style.effectiveBackground
1516
import org.openrndr.shape.Rectangle
17+
import org.openrndr.shape.bounds
1618
import kotlin.math.max
1719
import kotlin.math.min
1820
import 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

Comments
 (0)