1 /** 2 Copyright: Copyright (c) 2017-2018 Andrey Penechko. 3 License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0). 4 Authors: Andrey Penechko. 5 */ 6 module voxelman.gui.textedit.lineedit; 7 8 import core.time : MonoTime; 9 import std.format : formattedWrite; 10 import voxelman.container.gapbuffer; 11 import voxelman.log; 12 import voxelman.gui; 13 import voxelman.math; 14 import voxelman.graphics; 15 import voxelman.container.chunkedbuffer; 16 import voxelman.gui.textedit.cursor; 17 import voxelman.gui.textedit.textmodel; 18 import voxelman.gui.textedit.linebuffer; 19 import voxelman.gui.textedit.texteditor; 20 import voxelman.gui.textedit.texteditorview; 21 22 @Component("gui.LineEditData", Replication.none) 23 struct LineEditData 24 { 25 TextViewSettings settings; 26 void delegate(string) enterHandler; 27 28 GapBuffer!char textData; 29 Selection selection; 30 MonoTime blinkStart; 31 } 32 33 struct LineEditLogic 34 { 35 LineEditData* data; 36 GuiContext ctx; 37 38 alias data this; 39 40 void resetBlinkTimer() 41 { 42 blinkStart = MonoTime.currTime; 43 } 44 45 void moveSelectionCursor(MoveCommand com, bool extendSelection) 46 { 47 selection.end = moveCursor(selection.end, com); 48 if (!extendSelection) selection.start = selection.end; 49 } 50 51 void copySelection() 52 { 53 auto sel = getSelectionForCopyCut(); 54 copyText(sel.start.byteOffset, sel.end.byteOffset); 55 } 56 57 void copyText(size_t from, size_t to) 58 { 59 auto copiedText = textData[from..to].toChunkedRange.byItem; 60 ctx.clipboard = copiedText; 61 } 62 63 size_t length() { return textData.length; } 64 alias opDollar = length; 65 66 ChunkedRange!char opSlice(size_t from, size_t to) 67 { 68 return textData[from..to].toChunkedRange; 69 } 70 71 Selection getSelectionForCopyCut() 72 { 73 return selection.normalized; 74 } 75 76 private void cutSelection() 77 { 78 auto sel = getSelectionForCopyCut(); 79 copyText(sel.start.byteOffset, sel.end.byteOffset); 80 removeAndUpdateSelection(sel); 81 } 82 83 void replaceSelection(const(char)[] str) 84 { 85 removeAndUpdateSelection(selection); 86 selection = emptySelection(insertText(selection.end, str)); 87 } 88 89 /// Returns pos after inserted text 90 Cursor insertText(Cursor cur, const(char)[] str) 91 { 92 if (str.length == 0) return cur; 93 94 //auto pieceUndo = 95 textData.putAt(cur.byteOffset, str); 96 //int insertedLines = lines.onPaste(cur, str); 97 auto afterInsertedText = Cursor(cur.byteOffset + str.length, cur.line); 98 //auto undoItem = UndoItem(pieceUndo, cur, afterInsertedText, UndoCommand.undoInsert, selection); 99 //putUndo(undoItem); 100 return afterInsertedText; 101 } 102 103 void removeAndUpdateSelection(Selection sel) 104 { 105 Selection normSel = sel.normalized; 106 removeText(normSel); 107 // position cursor at the start of removed text 108 selection = emptySelection(normSel.start); 109 } 110 111 void removeText(Selection sel) 112 { 113 removeText(sel.start, sel.end); 114 } 115 116 void removeText(Cursor from, Cursor to) 117 { 118 if (from == to) return; 119 assert(from.byteOffset < to.byteOffset, "from.byteOffset < to.byteOffset"); 120 //auto pieceUndo = 121 infof("remove %s %s", from.byteOffset, to.byteOffset-from.byteOffset); 122 textData.remove(from.byteOffset, to.byteOffset-from.byteOffset); 123 //auto undoItem = UndoItem(pieceUndo, from, to, UndoCommand.undoRemove, selection); 124 //putUndo(undoItem); 125 //lines.onRemove(from, to); 126 } 127 128 void onCommand(EditorCommand com) 129 { 130 switch(com.type) 131 { 132 case EditorCommandType.insert_eol: 133 if (enterHandler) enterHandler(cast(string)textData.getContinuousSlice(0, textData.length)); 134 break; 135 case EditorCommandType.insert_tab: 136 replaceSelection("\t"); 137 break; 138 case EditorCommandType.delete_left_char: 139 if (selection.empty) 140 { 141 auto leftCur = moveCursor(selection.end, MoveCommand.move_left_char); 142 removeAndUpdateSelection(Selection(leftCur, selection.end)); 143 } 144 else removeAndUpdateSelection(selection); 145 break; 146 case EditorCommandType.delete_right_char: 147 if (selection.empty) 148 { 149 auto rightCur = moveCursor(selection.end, MoveCommand.move_right_char); 150 removeAndUpdateSelection(Selection(selection.end, rightCur)); 151 } 152 else removeAndUpdateSelection(selection); 153 break; 154 case EditorCommandType.delete_right_word: break; 155 case EditorCommandType.delete_right_line: break; 156 case EditorCommandType.cut: cutSelection(); break; 157 case EditorCommandType.copy: copySelection(); break; 158 case EditorCommandType.paste: replaceSelection(ctx.clipboard); break; 159 case EditorCommandType.select_all: 160 selection = Selection(Cursor(0,0), Cursor(length, 0)); 161 break; 162 case EditorCommandType.undo: 163 //undo(); 164 break; 165 case EditorCommandType.redo: 166 //redo(); 167 break; 168 case EditorCommandType.cur_move_first: .. case EditorCommandType.cur_move_last: 169 selection.end = moveCursor(selection.end, com.toMoveCommand); 170 if (!com.extendSelection) selection.start = selection.end; 171 break; 172 default: break; 173 } 174 } 175 import std.utf : stride, strideBack; 176 177 uint strideAt(size_t offset) 178 { 179 auto str = textData[offset..$]; 180 return stride(str); 181 } 182 183 size_t nextOffset(size_t offset) 184 { 185 return offset + strideAt(offset); 186 } 187 188 size_t prevOffset(size_t offset) 189 { 190 return offset - strideBack(textData[0u..offset]); 191 } 192 193 Cursor moveCursor(Cursor cur, MoveCommand com) 194 { 195 switch(com) with(MoveCommand) { 196 case move_right_char: 197 if (cur.byteOffset < textData.length) { 198 auto newOffset = nextOffset(cur.byteOffset); 199 cur.byteOffset = newOffset; 200 } 201 break; 202 case move_left_char: 203 if (cur.byteOffset > 0) { 204 auto newOffset = prevOffset(cur.byteOffset); 205 cur.byteOffset = newOffset; 206 } 207 break; 208 case move_to_bol: cur.byteOffset = 0; break; 209 case move_to_eol: cur.byteOffset = textData.length; break; 210 default: break; 211 } 212 return cur; 213 } 214 215 void clear() 216 { 217 textData.clear(); 218 selection = Selection(); 219 } 220 221 Cursor calcCursorPos(ivec2 absPointerPos, ivec2 absPos) 222 { 223 ivec2 viewportPointerPos = absPointerPos - absPos; 224 return calcCursorPos(viewportPointerPos); 225 } 226 227 Cursor calcCursorPos(ivec2 canvasPointerPos) 228 { 229 //if (data.editor.textData.length == 0) return Cursor(); 230 ivec2 glyphSize = settings.scaledGlyphSize; 231 232 auto range = glyphWidthRange(&settings, textData[]); 233 foreach(int x, int width; range) 234 { 235 int glyphCenter = x + width/2; 236 if (canvasPointerPos.x < glyphCenter) break; 237 } 238 auto cursorBytes = range.byteOffset; 239 return Cursor(cursorBytes); 240 } 241 } 242 243 struct LineEdit 244 { 245 static: 246 247 enum vmargin = 2; 248 249 WidgetProxy create(WidgetProxy parent, void delegate(string) enterHandler = null) 250 { 251 TextViewSettings settings; 252 settings.font = parent.ctx.style.font; 253 settings.color = parent.ctx.style.color; 254 return parent.createChild( 255 LineEditData(settings, enterHandler), 256 WidgetEvents( 257 &enterWidget, &drawWidget, 258 &pointerPressed, &pointerReleased, &pointerMoved, 259 &keyPressed, &charTyped), 260 WidgetIsFocusable() 261 ).minSize(0, settings.font.metrics.height + vmargin*2); 262 } 263 264 void enterWidget(WidgetProxy widget, ref PointerEnterEvent event) 265 { 266 widget.ctx.cursorIcon = CursorIcon.ibeam; 267 } 268 269 void drawWidget(WidgetProxy widget, ref DrawEvent event) 270 { 271 if (event.bubbling) return; 272 273 auto transform = widget.getOrCreate!WidgetTransform; 274 auto data = widget.get!LineEditData; 275 Selection sel = data.selection; 276 ivec2 glyphSize = data.settings.scaledGlyphSize; 277 278 ivec2 textPos = transform.absPos + ivec2(0, vmargin); 279 280 auto mesherParams = event.renderQueue.defaultText(); 281 mesherParams.scissors = irect(transform.absPos, transform.size); 282 mesherParams.color = data.settings.color; 283 mesherParams.scale = data.settings.fontScale; 284 mesherParams.font = cast(FontRef)data.settings.font; 285 mesherParams.depth = event.depth+1; 286 mesherParams.origin = textPos; 287 mesherParams.monospaced = data.settings.monospaced; 288 289 mesherParams.meshText(data.textData[]); 290 291 // draw selection 292 { 293 Selection normSel = sel.normalized; 294 enum selCol = Color4ub(180, 230, 255, 128); 295 int selStartX = textWidth(&data.settings, data.textData[0..normSel.start.byteOffset]); 296 int selEndX = textWidth(&data.settings, data.textData[0..normSel.end.byteOffset]); 297 vec2 size = vec2(selEndX - selStartX, glyphSize.y); 298 vec2 pos = vec2(textPos) + vec2(selStartX, 0); 299 event.renderQueue.drawRectFill(pos, size, event.depth, selCol); 300 } 301 302 // draw cursor 303 if (widget.ctx.focusedWidget == widget) 304 { 305 auto sinceBlinkStart = MonoTime.currTime - data.blinkStart; 306 if (sinceBlinkStart.total!"msecs" % 1000 < 500) 307 { 308 int cursorX = textWidth(&data.settings, data.textData[0..sel.end.byteOffset]); 309 vec2 pos = vec2(textPos) + vec2(cursorX, 0); 310 vec2 size = vec2(1, glyphSize.y); 311 event.renderQueue.drawRectFill(pos, size, event.depth+1, data.settings.color); 312 } 313 } 314 315 event.depth += 2; 316 } 317 318 void keyPressed(WidgetProxy widget, ref KeyPressEvent event) 319 { 320 auto data = widget.get!LineEditData; 321 auto logic = LineEditLogic(data, widget.ctx); 322 323 logic.resetBlinkTimer(); 324 325 auto command = keyPressToCommand(event); 326 logic.onCommand(command); 327 } 328 329 void charTyped(WidgetProxy widget, ref CharEnterEvent event) 330 { 331 import std.utf : encode; 332 import std.typecons : Yes; 333 auto data = widget.get!LineEditData; 334 auto logic = LineEditLogic(data, widget.ctx); 335 char[4] buf; 336 auto numBytes = encode!(Yes.useReplacementDchar)(buf, event.character); 337 logic.replaceSelection(buf[0..numBytes]); 338 logic.resetBlinkTimer(); 339 } 340 341 void clear(WidgetProxy widget) 342 { 343 auto data = widget.get!LineEditData; 344 auto logic = LineEditLogic(data, widget.ctx); 345 logic.clear; 346 } 347 348 void pointerPressed(WidgetProxy widget, ref PointerPressEvent event) 349 { 350 if (event.button == PointerButton.PB_LEFT) 351 { 352 auto transform = widget.getOrCreate!WidgetTransform; 353 auto data = widget.get!LineEditData; 354 auto logic = LineEditLogic(data, widget.ctx); 355 Cursor cursorPos = logic.calcCursorPos(event.pointerPosition, transform.absPos); 356 357 data.selection.end = cursorPos; 358 359 bool extendSelection = event.shift; 360 if (!extendSelection) 361 data.selection.start = cursorPos; 362 } 363 event.handled = true; 364 } 365 366 void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event) 367 { 368 event.handled = true; 369 } 370 371 void setEnterHandler(WidgetProxy widget, void delegate(string) enterHandler) 372 { 373 auto data = widget.get!LineEditData; 374 data.enterHandler = enterHandler; 375 } 376 377 void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event) 378 { 379 if (widget.ctx.state.pressedWidget == widget) 380 { 381 auto transform = widget.getOrCreate!WidgetTransform; 382 auto data = widget.get!LineEditData; 383 auto logic = LineEditLogic(data, widget.ctx); 384 data.selection.end = logic.calcCursorPos(event.newPointerPos, transform.absPos); 385 } 386 event.handled = true; 387 } 388 }