1 module rpui.widgets.text_input.edit_component; 2 3 import std.algorithm.comparison; 4 import std.algorithm.searching; 5 import std.string; 6 import std.math; 7 import std.conv; 8 9 import rpui.input; 10 import rpui.primitives; 11 import rpui.math; 12 import rpui.widgets.text_input.widget; 13 import rpui.widgets.text_input.select_component; 14 import rpui.widgets.text_input.carriage; 15 import rpui.widgets.text_input.transforms_system; 16 import rpui.events; 17 import rpui.theme; 18 19 struct EditComponent { 20 const commonSplitChars = " ,.;:?'!|/\\~*+-=(){}<>[]#%&^@$№`\""d; 21 const japanesePunctuation = "\u3000{}()[]【】、,…‥。〽「」『』〝〟〜:!?"d; 22 const splitChars = commonSplitChars ~ japanesePunctuation; 23 24 utf32string text; 25 Carriage carriage; 26 float scrollDelta = 0.0f; 27 vec2 absoulteTextPosition; 28 29 SelectRegion selectRegion; 30 TextInput textInput; 31 TextInputTransformsSystem transformsSystem; 32 private utf32string lastText; 33 34 void attach(TextInput textInput, TextInputTransformsSystem transformsSystem) { 35 this.textInput = textInput; 36 this.transformsSystem = transformsSystem; 37 } 38 39 void reset() { 40 selectRegion.stopSelection(); 41 carriage.reset(); 42 } 43 44 void onKeyPressed(in KeyPressedEvent event) { 45 carriage.timer = 0; 46 carriage.visible = true; 47 48 if (isKeyPressed(KeyCode.Shift) && !selectRegion.startedSelection) 49 selectRegion.startSelection(carriage.pos); 50 51 switch (event.key) { 52 case KeyCode.Left: 53 if (isKeyPressed(KeyCode.Ctrl)) { 54 carriage.setCarriagePos(carriage.navigateCarriage(-1)); 55 } else { 56 carriage.moveCarriage(-1); 57 } 58 59 break; 60 61 case KeyCode.Right: 62 if (isKeyPressed(KeyCode.Ctrl)) { 63 carriage.setCarriagePos(carriage.navigateCarriage(1)); 64 } else { 65 carriage.moveCarriage(1); 66 } 67 68 break; 69 70 case KeyCode.Home: 71 carriage.setCarriagePos(0); 72 break; 73 74 case KeyCode.End: 75 carriage.setCarriagePos(cast(int) text.length); 76 break; 77 78 case KeyCode.Delete: 79 if (selectRegion.textIsSelected()) { 80 removeSelectedRegion(); 81 } else { 82 if (isKeyPressed(KeyCode.Ctrl)) { 83 const end = carriage.navigateCarriage(1); 84 removeRegion(carriage.pos, end); 85 } else { 86 removeRegion(carriage.pos, carriage.pos+1); 87 } 88 } 89 break; 90 91 case KeyCode.BackSpace: 92 if (selectRegion.textIsSelected()) { 93 removeSelectedRegion(); 94 } else { 95 if (isKeyPressed(KeyCode.Ctrl)) { 96 const start = carriage.navigateCarriage(-1); 97 removeRegion(start, carriage.pos); 98 } else { 99 removeRegion(carriage.pos-1, carriage.pos); 100 } 101 } 102 break; 103 104 default: 105 // Nothing 106 } 107 } 108 109 void removeSelectedRegion() { 110 removeRegion( 111 selectRegion.start, 112 selectRegion.end 113 ); 114 } 115 116 void removeRegion(in int start, in int end) { 117 if (start < 0 || end > text.length) 118 return; 119 120 const leftPart = text[0 .. start]; 121 const rightPart = text[end .. text.length]; 122 123 text = leftPart ~ rightPart; 124 carriage.setCarriagePos(start); 125 } 126 127 private bool isISOControlCharacter(in utf32char ch) { 128 // Control characters 129 // https://www.utf8-chartable.de/unicode-utf8-table.pl?utf8=0x 130 return (ch >= 0x00 && ch <= 0x1F) || (ch >= 0x7F && ch <= 0x9F); 131 } 132 133 bool onTextEntered(in TextEnteredEvent event) { 134 return enterText(to!utf32string(event.key)); 135 } 136 137 bool enterText(in utf32string charToPut) { 138 carriage.timer = 0; 139 carriage.visible = true; 140 141 foreach (ch; charToPut) { 142 if (isISOControlCharacter(ch)) 143 return false; 144 } 145 146 // Splitting text to two parts by carriage position 147 148 utf32string leftPart; 149 utf32string rightPart; 150 auto newCarriagePos = carriage.pos; 151 152 if (!selectRegion.textIsSelected()) { 153 leftPart = text[0 .. carriage.pos]; 154 rightPart = text[carriage.pos .. $]; 155 newCarriagePos += charToPut.length; 156 } 157 else { 158 leftPart = text[0 .. selectRegion.start]; 159 rightPart = text[selectRegion.end .. $]; 160 newCarriagePos = selectRegion.start + cast(int) charToPut.length; 161 } 162 163 const newText = leftPart ~ charToPut ~ rightPart; 164 165 text = newText; 166 carriage.pos = newCarriagePos; 167 168 selectRegion.stopSelection(); 169 return true; 170 } 171 172 void onMouseDown(in MouseDownEvent event) { 173 if (textInput.autoSelectOnFocus) 174 return; 175 176 carriage.setCarriagePosFromMousePos(event.x, event.y); 177 178 if (!isKeyPressed(KeyCode.Shift)) 179 selectRegion.startSelection(carriage.pos); 180 } 181 182 void onMouseMove(in MouseMoveEvent event) { 183 if (event.button != MouseButton.mouseLeft) 184 return; 185 186 // if (!textInput.isNumberMode()) 187 if (textInput.isFocused) 188 carriage.setCarriagePosFromMousePos(event.x, event.y); 189 } 190 191 void onDblClick(in DblClickEvent event) { 192 const left = carriage.navigateCarriage(-1); 193 const right = carriage.navigateCarriage(1); 194 195 selectRegion.start = left; 196 selectRegion.end = right; 197 carriage.pos = right; 198 } 199 200 void onTripleClick(in TripleClickEvent event) { 201 selectAll(); 202 } 203 204 float getTextWidth() { 205 return textInput.measure.textWidth; 206 } 207 208 float getTextRegionSize(in int start, in int end) 209 // in(start <= end) 210 { 211 if (start == end) 212 return 0.0f; 213 214 return transformsSystem.getRegionTextWidth(start, end); 215 } 216 217 vec2 getTextRegionOffset(in int charPos) { 218 const regionSize = getTextRegionSize(0, charPos); 219 const offset = vec2( 220 regionSize + scrollDelta, 221 textInput.measure.textTopMargin 222 ); 223 224 float alignOffset = 0; 225 226 if (textInput.textAlign == Align.left) { 227 alignOffset = textInput.measure.textLeftMargin; 228 } 229 else if (textInput.textAlign == Align.right) { 230 alignOffset = -textInput.measure.textRightMargin; 231 } 232 233 return vec2(alignOffset - textInput.measure.carriageBoundary, 0) + 234 textInput.measure.textRelativePosition + offset; 235 } 236 237 void selectAll() { 238 selectRegion.start = 0; 239 selectRegion.end = cast(int) text.length; 240 selectRegion.startedSelection = true; 241 carriage.pos = selectRegion.end; 242 } 243 244 void unselect() { 245 selectRegion.start = 0; 246 selectRegion.end = 0; 247 selectRegion.startedSelection = false; 248 } 249 250 void onFocus() { 251 lastText = textInput.text; 252 } 253 254 void onBlur() { 255 switch (textInput.inputType) { 256 case TextInput.InputType.integer: 257 if (!isNumeric(textInput.text)) { 258 textInput.text = lastText; 259 } else { 260 const value = textInput.text.to!float; 261 textInput.text = round(value).to!utf32string; 262 } 263 264 break; 265 266 case TextInput.InputType.number: 267 if (!isNumeric(textInput.text)) 268 textInput.text = lastText; 269 270 break; 271 272 case TextInput.InputType.text: 273 return; 274 275 default: 276 return; 277 } 278 } 279 }