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.texteditorview; 7 8 import core.time : MonoTime; 9 import std.stdio : writefln, writeln; 10 import std.array; 11 12 import datadriven.entityman : EntityManager; 13 import voxelman.container.buffer; 14 import voxelman.graphics; 15 import voxelman.log; 16 import voxelman.gui; 17 import voxelman.math; 18 import voxelman.platform; 19 import voxelman.text.linebuffer; 20 import voxelman.text.scale; 21 22 import voxelman.gui.textedit.texteditor; 23 import voxelman.gui.textedit.cursor; 24 import voxelman.gui.textedit.linebuffer; 25 import voxelman.gui.textedit.textmodel; 26 27 void registerComponents(ref EntityManager widgets) 28 { 29 widgets.registerComponent!TextEditorViewportData; 30 widgets.registerComponent!TextEditorLineNumbersData; 31 } 32 33 @Component("gui.TextEditorViewportData", Replication.none) 34 struct TextEditorViewportData 35 { 36 TextModel editor; 37 TextViewSettingsRef settings; 38 MonoTime blinkStart; 39 40 ivec2 textPos; // on text canvas, in pixels 41 42 int autoscrollY; 43 int firstVisibleLine; 44 int lastVisibleLine; 45 bool autoscroll; 46 // If true prevents scrolling up 47 // If false, scrolling up will disable autoscroll 48 bool hardAutoscroll; 49 50 void resetBlinkTimer() 51 { 52 blinkStart = MonoTime.currTime; 53 } 54 55 void scroll(ivec2 delta) 56 { 57 if (autoscroll && hardAutoscroll) return; 58 59 textPos += ivec2(0, delta.y * settings.scrollSpeedLines * settings.scaledGlyphH); 60 if (textPos.y < autoscrollY) 61 { 62 autoscroll = false; 63 } 64 } 65 66 void update(GuiContext ctx, ivec2 size) 67 { 68 ivec2 textSizeInGlyphs = ivec2(0, editor.numLines); 69 ivec2 textSizeInPixels = textSizeInGlyphs * settings.scaledGlyphSize; 70 71 int maxVisibleLines = divCeil(size.y, settings.scaledGlyphH); 72 autoscrollY = (editor.numLines - maxVisibleLines) * settings.scaledGlyphH; 73 autoscrollY = clamp(autoscrollY, 0, textSizeInPixels.y); 74 if (autoscroll) 75 { 76 textPos.y = autoscrollY; 77 } 78 79 textPos = vector_clamp(textPos, ivec2(0, 0), textSizeInPixels); 80 81 //if (editor.textSizeInGlyphs.y == 0) return; 82 83 firstVisibleLine = clamp(textPos.y / settings.scaledGlyphH, 0, editor.lastLine); 84 85 int viewportEndPos = textPos.y + size.y; 86 lastVisibleLine = clamp(viewportEndPos / settings.scaledGlyphH, 0, editor.lastLine); 87 88 ctx.debugText.putfln("textPos %s", textPos); 89 ctx.debugText.putfln("size %s", size); 90 ctx.debugText.putfln("viewportEndPos %s", viewportEndPos); 91 ctx.debugText.putfln("firstVisibleLine %s", firstVisibleLine); 92 ctx.debugText.putfln("lastVisibleLine %s", lastVisibleLine); 93 } 94 } 95 96 struct TextEditorViewportLogic 97 { 98 static: 99 WidgetProxy create( 100 WidgetProxy parent, 101 TextModel editor, 102 TextViewSettingsRef settings) 103 { 104 return parent.createChild( 105 TextEditorViewportData(editor, settings, MonoTime.currTime), 106 WidgetEvents( 107 &onScroll, &enterWidget, &drawWidget, 108 &pointerPressed, &pointerReleased, &pointerMoved, 109 &keyPressed, &charTyped 110 ), 111 WidgetIsFocusable() 112 ); 113 } 114 115 void onScroll(WidgetProxy widget, ref ScrollEvent event) 116 { 117 auto data = widget.get!TextEditorViewportData; 118 data.scroll(ivec2(event.delta)); 119 } 120 121 void enterWidget(WidgetProxy widget, ref PointerEnterEvent event) 122 { 123 widget.ctx.cursorIcon = CursorIcon.ibeam; 124 } 125 126 void drawWidget(WidgetProxy widget, ref DrawEvent event) 127 { 128 if (event.bubbling) return; 129 130 auto transform = widget.getOrCreate!WidgetTransform; 131 auto data = widget.get!TextEditorViewportData; 132 auto editor = data.editor; 133 auto settings = data.settings; 134 data.update(widget.ctx, transform.size); 135 Selection sel = data.editor.selection; 136 137 auto from = editor.lineInfo(data.firstVisibleLine).startOffset; 138 auto to = editor.lineInfo(data.lastVisibleLine).endOffset; 139 auto renderedText = editor[from..to].byItem; 140 141 MonoTime startTime = MonoTime.currTime; 142 143 auto mesherParams = event.renderQueue.defaultText(); 144 mesherParams.scissors = irect(transform.absPos, transform.size); 145 mesherParams.scale = settings.fontScale; 146 mesherParams.color = settings.color; 147 mesherParams.font = cast(FontRef)settings.font; 148 mesherParams.depth = event.depth+1; 149 mesherParams.origin = transform.absPos; 150 mesherParams.monospaced = settings.monospaced; 151 152 mesherParams.meshText(renderedText); 153 154 ivec2 glyphSize = settings.scaledGlyphSize; 155 156 // draw selection 157 { 158 enum selCol = rgb(180, 230, 255); 159 160 Selection normSel = sel.normalized; 161 int firstSelectedLine = normSel.start.line; 162 int lastSelectedLine = normSel.end.line; 163 int firstVisibleSelectedLine = max(firstSelectedLine, data.firstVisibleLine); 164 int lastVisibleSelectedLine = min(lastSelectedLine, data.lastVisibleLine); 165 166 foreach(line; firstVisibleSelectedLine..lastVisibleSelectedLine+1) 167 { 168 size_t lineStart = 0; 169 size_t lineEnd; 170 171 auto lineInfo = editor.lineInfo(line); 172 173 if (line == firstSelectedLine) lineStart = normSel.start.byteOffset; 174 else lineStart = lineInfo.startOffset; 175 176 if (line == lastSelectedLine) lineEnd = normSel.end.byteOffset; 177 else lineEnd = lineInfo.endOffset; 178 179 int viewportTopOffset = line - data.firstVisibleLine; 180 181 182 int selStartX = textWidth(settings, editor[lineInfo.startOffset..lineStart].byItem); 183 int selEndX = textWidth(settings, editor[lineInfo.startOffset..lineEnd].byItem); 184 185 if (line != lastSelectedLine) selEndX += glyphSize.x; // make newline visible 186 187 vec2 size = vec2(selEndX - selStartX, glyphSize.y); 188 vec2 pos = vec2(transform.absPos) + vec2(selStartX, viewportTopOffset*glyphSize.y); 189 190 event.renderQueue.drawRectFill(pos, size, event.depth, selCol); 191 } 192 } 193 194 // draw cursor 195 if (widget.ctx.focusedWidget == widget) 196 { 197 auto sinceBlinkStart = MonoTime.currTime - data.blinkStart; 198 if (sinceBlinkStart.total!"msecs" % 1000 < 500) 199 { 200 int viewportTopOffset = sel.end.line - data.firstVisibleLine; 201 auto lineInfo = editor.lineInfo(sel.end.line); 202 int cursorX = textWidth(settings, editor[lineInfo.startOffset..sel.end.byteOffset].byItem); 203 vec2 pos = vec2(transform.absPos) + vec2(cursorX, viewportTopOffset*glyphSize.y); 204 vec2 size = vec2(1, glyphSize.y); 205 event.renderQueue.drawRectFill(pos, size, event.depth+1, settings.color); 206 } 207 } 208 209 event.depth += 2; 210 211 widget.ctx.debugText.putfln("Append glyphs: %sus", (MonoTime.currTime - startTime).total!"usecs"); 212 widget.ctx.debugText.putfln("Lines: %s", editor.numLines); 213 //widget.ctx.debugText.putfln("Size: %s", data.editor.textSizeInGlyphs); 214 widget.ctx.debugText.putfln("sel start: %s", sel.start); 215 widget.ctx.debugText.putfln("sel end: %s", sel.end); 216 } 217 218 void keyPressed(WidgetProxy widget, ref KeyPressEvent event) 219 { 220 auto data = widget.get!TextEditorViewportData; 221 data.resetBlinkTimer(); 222 auto command = keyPressToCommand(event); 223 data.editor.onCommand(command); 224 225 if (event.keyCode == KeyCode.KEY_M && event.control) 226 data.settings.monospaced = !data.settings.monospaced; 227 } 228 229 void charTyped(WidgetProxy widget, ref CharEnterEvent event) 230 { 231 import std.utf : encode; 232 import std.typecons : Yes; 233 auto data = widget.get!TextEditorViewportData; 234 char[4] buf; 235 auto numBytes = encode!(Yes.useReplacementDchar)(buf, event.character); 236 data.editor.onCommand(EditorCommand(EditorCommandType.input, 0, buf[0..numBytes])); 237 //data.editor.replaceSelection(buf[0..numBytes]); 238 data.resetBlinkTimer(); 239 } 240 241 void pointerPressed(WidgetProxy widget, ref PointerPressEvent event) 242 { 243 if (event.button == PointerButton.PB_LEFT) 244 { 245 auto transform = widget.getOrCreate!WidgetTransform; 246 auto data = widget.get!TextEditorViewportData; 247 248 Cursor cursorPos = calcCursorPos(event.pointerPosition, transform.absPos, data); 249 250 data.editor.selection.end = cursorPos; 251 252 bool extendSelection = event.shift; 253 if (!extendSelection) 254 data.editor.selection.start = cursorPos; 255 } 256 event.handled = true; 257 } 258 259 void pointerReleased(WidgetProxy widget, ref PointerReleaseEvent event) 260 { 261 event.handled = true; 262 } 263 264 void pointerMoved(WidgetProxy widget, ref PointerMoveEvent event) 265 { 266 if (widget.ctx.state.pressedWidget == widget) 267 { 268 auto transform = widget.getOrCreate!WidgetTransform; 269 auto data = widget.get!TextEditorViewportData; 270 data.editor.selection.end = calcCursorPos(event.newPointerPos, transform.absPos, data); 271 } 272 event.handled = true; 273 } 274 275 Cursor calcCursorPos(ivec2 absPointerPos, ivec2 absPos, TextEditorViewportData* data) 276 { 277 ivec2 viewportPointerPos = absPointerPos - absPos; 278 ivec2 canvasPointerPos = viewportPointerPos + data.textPos; 279 return calcCursorPos(canvasPointerPos, data); 280 } 281 282 Cursor calcCursorPos(ivec2 canvasPointerPos, TextEditorViewportData* data) 283 { 284 //if (data.editor.textData.length == 0) return Cursor(); 285 ivec2 glyphSize = data.settings.scaledGlyphSize; 286 287 int cursorLine = clamp(canvasPointerPos.y / glyphSize.y, 0, data.editor.lastLine); 288 289 auto lineInfo = data.editor.lineInfo(cursorLine); 290 auto from = lineInfo.startOffset; 291 auto to = lineInfo.endOffset; 292 auto text = data.editor[from..to].byItem; // text without newline 293 294 auto range = glyphWidthRange(data.settings, text); 295 foreach(int x, int width; range) 296 { 297 int glyphCenter = x + width/2; 298 if (canvasPointerPos.x < glyphCenter) break; 299 } 300 auto cursorBytes = lineInfo.startOffset + range.byteOffset; 301 return Cursor(cursorBytes, cursorLine); 302 } 303 } 304 305 int textWidth(T)(TextViewSettingsRef settings, T text) 306 { 307 auto range = glyphWidthRange(settings, text); 308 foreach(b, c; range) {} 309 return range.x; 310 } 311 312 313 auto glyphWidthRange(R)(TextViewSettingsRef settings, R text) 314 { 315 return GlyphWidthRange!R(settings, text); 316 } 317 318 struct GlyphWidthRange(R) 319 { 320 import std.utf : decodeFront; 321 import std.typecons : Yes; 322 TextViewSettingsRef settings; 323 R input; 324 int x; 325 size_t byteOffset; 326 327 int opApply(scope int delegate(int x, int width) del) 328 { 329 int glyphW = settings.scaledGlyphW; 330 x = 0; 331 byteOffset = 0; 332 333 auto initialBytes = input.length; 334 if (settings.monospaced) 335 { 336 int column; 337 338 while(!input.empty) 339 { 340 dchar codePoint = decodeFront!(Yes.useReplacementDchar)(input); 341 int width; 342 if (codePoint == '\t') 343 { 344 int tabGlyphs = tabWidth(settings.tabSize, column); 345 width = tabGlyphs * glyphW; 346 column += tabGlyphs; 347 } 348 else 349 { 350 width = glyphW; 351 ++column; 352 } 353 354 if (auto ret = del(x, width)) return ret; 355 356 byteOffset = initialBytes - input.length; 357 x += width; 358 } 359 } 360 else 361 { 362 while(!input.empty) 363 { 364 dchar codePoint = decodeFront!(Yes.useReplacementDchar)(input); 365 int width; 366 if (codePoint == '\t') 367 { 368 int tabPixels = tabWidth(settings.tabSize * glyphW, x); 369 width = tabPixels; 370 } 371 else 372 { 373 const Glyph* glyph = settings.font.getGlyph(codePoint); 374 width = glyph.metrics.advanceX; 375 } 376 377 if (auto ret = del(x, width)) return ret; 378 379 byteOffset = initialBytes - input.length; 380 x += width; 381 } 382 } 383 byteOffset = initialBytes - input.length; 384 return 0; 385 } 386 } 387 388 EditorCommand keyPressToCommand(ref KeyPressEvent event) 389 { 390 alias ComType = EditorCommandType; 391 392 EditorCommand moveCommand(MoveCommand com, bool extendSelection) 393 { 394 return EditorCommand(cast(EditorCommandType)(com + EditorCommandType.cur_move_first), extendSelection); 395 } 396 397 switch(event.keyCode) with(KeyCode) 398 { 399 case KEY_LEFT: return moveCommand(MoveCommand.move_left_char, event.shift); 400 case KEY_RIGHT: return moveCommand(MoveCommand.move_right_char, event.shift); 401 case KEY_UP: return moveCommand(MoveCommand.move_up_line, event.shift); 402 case KEY_DOWN: return moveCommand(MoveCommand.move_down_line, event.shift); 403 case KEY_HOME: return moveCommand(MoveCommand.move_to_bol, event.shift); 404 case KEY_END: return moveCommand(MoveCommand.move_to_eol, event.shift); 405 case KEY_TAB: return EditorCommand(ComType.insert_tab); 406 case KEY_ENTER: case KEY_KP_ENTER: return EditorCommand(ComType.insert_eol); 407 case KEY_BACKSPACE: 408 if (event.control) 409 { 410 if (event.shift) return EditorCommand(ComType.delete_left_line); 411 else return EditorCommand(ComType.delete_left_word); 412 } 413 else return EditorCommand(ComType.delete_left_char); 414 case KEY_DELETE: 415 if (event.control) 416 { 417 if (event.shift) return EditorCommand(ComType.delete_right_line); 418 else return EditorCommand(ComType.delete_right_word); 419 } 420 else return EditorCommand(ComType.delete_right_char); 421 case KEY_A: if (event.control) return EditorCommand(ComType.select_all); break; 422 case KEY_C: if (event.control) return EditorCommand(ComType.copy); break; 423 case KEY_X: if (event.control) return EditorCommand(ComType.cut); break; 424 case KEY_V: if (event.control) return EditorCommand(ComType.paste); break; 425 case KEY_Z: 426 if (event.control) 427 { 428 if (event.shift) return EditorCommand(ComType.redo); 429 else return EditorCommand(ComType.undo); 430 } 431 break; 432 default: break; 433 } 434 return EditorCommand(ComType.none); 435 } 436 437 @Component("gui.TextEditorLineNumbersData", Replication.none) 438 struct TextEditorLineNumbersData 439 { 440 // component connections 441 TextModel editor; 442 TextViewSettingsRef settings; 443 WidgetId viewport; 444 445 // vars 446 enum leftSpacing = 1; 447 enum rightSpacing = 2; 448 449 int widthInGlyphs() 450 { 451 return numDigitsInNumber(editor.numLines) + leftSpacing + rightSpacing; 452 } 453 454 int widthInPixels(int _widthInGlyphs) 455 { 456 return cast(int)(_widthInGlyphs * settings.scaledGlyphW); 457 } 458 } 459 460 struct TextEditorLineNumbersLogic 461 { 462 static: 463 WidgetProxy create( 464 WidgetProxy parent, 465 TextModel editor, 466 TextViewSettingsRef settings) 467 { 468 return parent.createChild( 469 TextEditorLineNumbersData(editor, settings), 470 WidgetEvents(&drawWidget, &measure)); 471 } 472 473 void setViewport(WidgetProxy widget, WidgetProxy viewport) 474 { 475 auto data = widget.get!TextEditorLineNumbersData; 476 data.viewport = viewport; 477 } 478 479 void drawWidget(WidgetProxy widget, ref DrawEvent event) 480 { 481 if (event.bubbling) return; 482 483 auto transform = widget.getOrCreate!WidgetTransform; 484 auto data = widget.get!TextEditorLineNumbersData; 485 auto viewportData = widget.ctx.get!TextEditorViewportData(data.viewport); 486 487 int widthInGlyphs = data.widthInGlyphs; 488 489 auto mesherParams = event.renderQueue.defaultText(); 490 mesherParams.scale = data.settings.fontScale; 491 mesherParams.origin = transform.absPos; 492 mesherParams.monospaced = true; 493 494 foreach (line; viewportData.firstVisibleLine..viewportData.lastVisibleLine+1) 495 { 496 size_t lineNumber = line+1; 497 size_t digits = numDigitsInNumber(lineNumber); 498 size_t spacing = widthInGlyphs - (digits + data.rightSpacing); 499 foreach (_; 0..spacing) 500 mesherParams.meshText(" "); 501 mesherParams.meshTextf("%s\n", lineNumber); 502 } 503 504 widget.ctx.debugText.putfln("Line num w: %s", widthInGlyphs); 505 } 506 507 void measure(WidgetProxy widget, ref MeasureEvent event) 508 { 509 auto transform = widget.getOrCreate!WidgetTransform; 510 auto data = widget.get!TextEditorLineNumbersData; 511 int widthInGlyphs = data.widthInGlyphs; 512 int widthInPixels = data.widthInPixels(widthInGlyphs); 513 transform.measuredSize = ivec2(widthInPixels, 0); 514 } 515 } 516 /* 517 struct TextEditorMinimap 518 { 519 // component connections 520 TextEditorCRef document; 521 TextViewSettingsRef settings; 522 TextEditorViewportConstRef viewport; 523 524 enum glyphSize = ivec2(1, 2); 525 526 // vars 527 ivec2 position; 528 ivec2 size; 529 530 int glyphsX; 531 532 // private 533 private Bitmap image; 534 535 void update(ref LineBuffer debugText) 536 { 537 538 } 539 540 void render(RenderQueue renderQueue, ref LineBuffer debugText) 541 { 542 //lvec2 textSizeInPixels = document.textSizeInGlyphs * settings.scaledGlyphSize; 543 //long scrollPercent = viewport.textPos.y / textSizeInPixels.y; 544 545 //long minimapAreaWidth = viewport.size.x / settings.scaledGlyphW; 546 547 //vec2 minimapPos = position + vec2(viewport.size.x - minimapAreaWidth, 0); 548 //vec2 size = vec2(minimapAreaWidth, viewport.size.y); 549 renderQueue.drawRectFill(vec2(position), vec2(size), 0, Color4ub(200, 200, 200, 255)); 550 } 551 } 552 */