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