1 /++ 2 A homemade text layout and editing engine, designed for the needs of minigui's custom widgets to be good enough for me to use. May or may not work for you. 3 4 5 You use it by creating a [TextLayouter] and populating it with some data. Then you connect it to a user interface which calls [TextLayouter.getDrawableText] to know what and where to display the content and manipulates the content through the [Selection] object. Your text has styles applied to it through a [TextStyle] interface, which is deliberately minimal for the layouter - you are expected to cast it back to your implementation as-needed to get your other data out. 6 7 See the docs on each of those objects for more details. 8 9 Bugs: 10 BiDi and right-to-left text in general is not yet implemented. I'm pretty sure I can do it, but I need unicode tables that aren't available to arsd yet. 11 12 Doesn't do text kerning since the other implementations I've looked at on-screen don't do it either so it seems unnecessary. I might revisit this. 13 14 Also doesn't handle shaped text, which breaks click point detection on Windows for certain script families. 15 16 The edit implementation is a simple string. It performs surprisingly well, but I'll probably go back to it and change to a gap buffer later. 17 18 Relaying out and saving state is only partially incremental at this time. 19 20 The main interfaces are written with eventually fixing these in mind, but I might have to extend the [MeasurableFont] and [TextStyle] interfaces, and it might need some helper objects injected too. So possible they will be small breaking changes to support these, but I'm pretty sure it won't require any major rewrites of the code nor of user code when these are added, just adding methods to interfaces. 21 22 History: 23 Written in December 2022. Released in arsd 11.0. 24 +/ 25 module arsd.textlayouter; 26 27 import arsd.simpledisplay; 28 29 /+ 30 FIXME: caret style might need to be separate from anything drawn. 31 FIXME: when adding things, inform new sizes for scrollbar updates in real time 32 FIXME: scroll when selecting and dragging oob. generally capture on mouse down and release on mouse up. 33 FIXME: page up, page down. 34 35 FIXME: there is a spot right between some glyphs when changing fonts where it selected none. 36 37 38 Need to know style at insertion point (which is the one before the caret codepoint unless it is at start of line, in which case it is the one at it) 39 40 41 The style interface might actually want like toHtml and toRtf. at least on the minigui side, not strictly necessary here. 42 +/ 43 44 45 /+ 46 subclass w/ style 47 lazy layout queuing 48 49 style info could possibly be a linked list but it prolly don't have to be anything too special 50 51 track changes 52 +/ 53 54 /+ 55 Word wrap needs to maintain indentation in some cases 56 57 The left and right margins of exclusion area 58 59 Exclusion are in the center? 60 61 line-spacing 62 63 if you click on the gap above a bounding box of a segment it doesn't find that segement despite being in the same line. need to check not just by segment bounding box but by line bounding box. 64 65 FIXME: in sdpy, font is not reset upon returning from a child painter 66 FIXME: in minigui the scrollbars can steal focus from the thing the are controlling 67 FIXME: scw needs a per-button-click scroll amount since 1 may not be sufficient every time (tho 1 should be a possibility somehow) 68 +/ 69 70 /+ 71 REPLACED CONTENT 72 73 magic char followed by a dchar 74 the dchar represents the replaced content array index 75 replaced content needs to tell the layouter: ascent, descent, width. 76 all replaced content gets its own special segment. 77 replaced content must be registered and const? or at the very least not modify things the layouter cares about. but better if nothing changes for undo sake. 78 79 it has a style but it only cares about the alignment from it. 80 +/ 81 82 /+ 83 HTML 84 generally take all the text nodes and make them have unique text style instances 85 the text style can then refer back to the dom for click handling, css forwarding etc. 86 87 but html has blocks... 88 89 BLOCK ELEMENTS 90 91 margin+padding behavior 92 bounding box of nested things for background images and borders 93 94 an inline-block gets this stuff but does not go on its own line. 95 96 INLINE TABLES 97 +/ 98 99 // FIXME: add center, left, right, justify and valign top, bottom, middle, baseline 100 // valign top = ascent = 0 of line. bottom = descent = bottom of line. middle = ascent+descent/2 = middle of line. baseline = matched baselines 101 102 // draw underline and strike through line segments - the offets may be in the font and underline might not want to slice the bottom fo p etc 103 // drawble textm ight give the offsets into the slice after all, and/or give non-trabable character things 104 105 106 // You can do the caret by any time it gets drawn, you set the flag that it is on, then you can xor it to turn it off and keep track of that at top level. 107 108 109 /++ 110 Represents the style of a span of text. 111 112 You should never mutate one of these, instead construct a new one. 113 114 Please note that methods may be added to this interface without being a full breaking change. 115 +/ 116 interface TextStyle { 117 /++ 118 Must never return `null`. 119 +/ 120 MeasurableFont font(); 121 122 // FIXME: I might also want a duplicate function for saving state. 123 124 // verticalAlign? 125 126 // i should keep a refcount here, then i can do a COW if i wanted to. 127 128 // you might use different style things to represent different html elements or something too for click responses. 129 130 /++ 131 You can mix this in to your implementation class to get default implementations of new methods I add. 132 133 You will almost certainly want to override the things anyway, but this can help you keep things compiling. 134 135 Please note that there is no default for font. 136 +/ 137 static mixin template Defaults() { 138 /++ 139 The default returns a [TerminalFontRepresentation]. This is almost certainly NOT what you want, 140 so implement your own `font()` member anyway. 141 +/ 142 MeasurableFont font() { 143 return TerminalFontRepresentation.instance; 144 } 145 146 } 147 } 148 149 /++ 150 This is a demo implementation of [MeasurableFont]. The expectation is more often that you'd use a [arsd.simpledisplay.OperatingSystemFont], which also implements this interface, but if you wanted to do your own thing this basic demo might help. 151 +/ 152 class TerminalFontRepresentation : MeasurableFont { 153 static TerminalFontRepresentation instance() { 154 static TerminalFontRepresentation i; 155 if(i is null) 156 i = new TerminalFontRepresentation(); 157 return i; 158 } 159 160 bool isMonospace() { return true; } 161 int averageWidth() { return 1; } 162 int height() { return 1; } 163 /// since it is a grid this is a bit bizarre to translate. 164 int ascent() { return 1; } 165 int descent() { return 0; } 166 167 int stringWidth(scope const(char)[] s, SimpleWindow window = null) { 168 int count; 169 foreach(dchar ch; s) 170 count++; 171 return count; 172 } 173 } 174 175 /++ 176 A selection has four pieces: 177 178 1) A position 179 2) An anchor 180 3) A focus 181 4) A user coordinate 182 183 The user coordinate should only ever be changed in direct response to actual user action and indicates 184 where they ideally want the focus to be. 185 186 If they move in the horizontal direction, the x user coordinate should change. The y should not, even if the actual focus moved around (e.g. moving to a previous line while left arrowing). 187 188 If they move in a vertical direction, the y user coordinate should change. The x should not even if the actual focus moved around (e.g. going to the end of a shorter line while up arrowing). 189 190 The position, anchor, and focus are stored in opaque units. The user coordinate is EITHER grid coordinates (line, glyph) or screen coordinates (pixels). 191 192 Most methods on the selection move the position. This is not visible to the user, it is just an internal marker. 193 194 setAnchor() sets the anchor to the current position. 195 setFocus() sets the focus to the current position. 196 197 The anchor is the part of the selection that doesn't move as you drag. The focus is the part of the selection that holds the caret and would move as you dragged around. (Open a program like Notepad and click and drag around. Your first click set the anchor, then as you drag, the focus moves around. The selection is everything between the anchor and the focus.) 198 199 The selection, while being fairly opaque, lets you do a great many things. Consider, for example, vim's 5dd command - delete five lines from the current position. You can do this by taking a selection, going to the beginning of the current line. Then dropping anchor. Then go down five lines and go to end of line. Then extend through the EOL character. Now delete the selection. Finally, restore the anchor and focus from the user coordinate, so their cursor on screen remains in the same approximate position. 200 201 The code can look something like this: 202 203 --- 204 selection 205 .moveHome 206 .setAnchor 207 .moveDown(5) 208 .moveEnd 209 .moveForward(&isEol) 210 .setFocus 211 .deleteContent 212 .moveToUserCoordinate 213 .setAnchor; 214 --- 215 216 If you can think about how you'd do it on the standard keyboard, you can do it with this api. Everything between a setAnchor and setFocus would be like holding shift while doing the other things. 217 218 void selectBetween(Selection other); 219 220 Please note that this is just a handle to another object. Treat it as a reference type. 221 +/ 222 public struct Selection { 223 /++ 224 You cannot construct these yourself. Instead, use [TextLayouter.selection] to get it. 225 +/ 226 @disable this(); 227 private this(TextLayouter layouter, int selectionId) { 228 this.layouter = layouter; 229 this.selectionId = selectionId; 230 } 231 private TextLayouter layouter; 232 private int selectionId; 233 234 private ref SelectionImpl impl() { 235 return layouter._selections[selectionId]; 236 } 237 238 /+ Inspection +/ 239 240 /++ 241 Returns `true` if the selection is currently empty. An empty selection still has a position - where the cursor is drawn - but has no text inside it. 242 243 See_Also: 244 [getContent], [getContentString] 245 +/ 246 bool isEmpty() { 247 return impl.focus == impl.anchor; 248 } 249 250 /++ 251 Function to get the content of the selection. It is fed to you as a series of zero or more chunks of text and style information. 252 253 Please note that some text blocks may be empty, indicating only style has changed. 254 255 See_Also: 256 [getContentString], [isEmpty] 257 +/ 258 void getContent(scope void delegate(scope const(char)[] text, TextStyle style) dg) { 259 dg(layouter.text[impl.start .. impl.end], null); // FIXME: style 260 } 261 262 /++ 263 Convenience function to get the content of the selection as a simple string. 264 265 See_Also: 266 [getContent], [isEmpty] 267 +/ 268 string getContentString() { 269 string s; 270 getContent((txt, style) { 271 s ~= txt; 272 }); 273 return s; 274 } 275 276 // need this so you can scroll found text into view and similar 277 Rectangle focusBoundingBox() { 278 return layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.focus), impl.focus); 279 } 280 281 /+ Setting the explicit positions to the current internal position +/ 282 283 /++ 284 These functions set the actual selection from the current internal position. 285 286 A selection has two major pieces, the anchor and the focus, and a third bookkeeping coordinate, called the user coordinate. 287 288 It is best to think about these by thinking about the user interface. When you click and drag in a text document, the point where 289 you clicked is the anchor position. As you drag, it moves the focus position. The selection is all the text between the anchor and 290 focus. The cursor (also known as the caret) is drawn at the focus point. 291 292 Meanwhile, the user coordinate is the point where the user last explicitly moved the focus. Try clicking near the end of a long line, 293 then moving up past a short line, to another long line. Your cursor should remain near the column of the original click, even though 294 the focus moved left while passing through the short line. The user coordinate is how this is achieved - explicit user action on the 295 horizontal axis (like pressing the left or right arrows) sets the X coordinate with [setUserXCoordinate], and explicit user action on the vertical axis sets the Y coordinate (like the up or down arrows) with [setUserYCoordinate], leaving X alone even if the focus moved horizontally due to a shorter or longer line. They're only moved together if the user action worked on both axes together (like a mouse click) with the [setUserCoordinate] function. Note that `setUserCoordinate` remembers the column even if there is no glyph there, making it ideal for mouse interaction, whereas the `setUserXCoordinate` and `setUserYCoordinate` set it to the position of the glyph on the focus, making them more suitable for keyboard interaction. 296 297 Before you set one of these values, you move the internal position with the `move` family of functions ([moveTo], [moveLeft], etc.). 298 299 Setting the anchor also always sets the focus. 300 301 For example, to select the whole document: 302 303 --- 304 with(selection) { 305 moveToStartOfDocument(); // changes internal position without affecting the actual selection 306 setAnchor(); // set the anchor, actually changing the selection. 307 // Note that setting the anchor also always sets the focus, so the selection is empty at this time. 308 moveToEndOfDocument(); // move the internal position to the end 309 setFocus(); // and now set the focus, which extends the selection from the anchor, meaning the whole document is selected now 310 } 311 --- 312 313 I didn't set the user coordinate there since the user's action didn't specify a row or column. 314 +/ 315 Selection setAnchor() { 316 impl.anchor = impl.position; 317 impl.focus = impl.position; 318 return this; 319 } 320 321 /// ditto 322 Selection setFocus() { 323 impl.focus = impl.position; 324 return this; 325 } 326 327 /// ditto 328 Selection setUserCoordinate(Point p) { 329 impl.virtualFocusPosition = p; 330 return this; 331 } 332 333 /// ditto 334 Selection setUserXCoordinate() { 335 impl.virtualFocusPosition.x = layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.position), impl.position).left; 336 return this; 337 } 338 339 /// ditto 340 Selection setUserYCoordinate() { 341 impl.virtualFocusPosition.y = layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.position), impl.position).top; 342 return this; 343 } 344 345 346 /+ Moving the internal position +/ 347 348 /++ 349 350 +/ 351 Selection moveTo(Point p, bool setUserCoordinate = true) { 352 impl.position = layouter.offsetOfClick(p); 353 if(setUserCoordinate) 354 impl.virtualFocusPosition = p; 355 return this; 356 } 357 358 /++ 359 360 +/ 361 Selection moveToStartOfDocument() { 362 impl.position = 0; 363 return this; 364 } 365 366 /// ditto 367 Selection moveToEndOfDocument() { 368 impl.position = cast(int) layouter.text.length - 1; // never include the 0 terminator 369 return this; 370 } 371 372 /++ 373 374 +/ 375 Selection moveToStartOfLine(bool byRender = true, bool includeLeadingWhitespace = true) { 376 // FIXME: chekc for word wrap by checking segment.displayLineNumber 377 // FIXME: includeLeadingWhitespace 378 while(impl.position > 0 && layouter.text[impl.position - 1] != '\n') 379 impl.position--; 380 381 return this; 382 } 383 384 /// ditto 385 Selection moveToEndOfLine(bool byRender = true) { 386 // FIXME: chekc for word wrap by checking segment.displayLineNumber 387 while(impl.position + 1 < layouter.text.length && layouter.text[impl.position] != '\n') // never include the 0 terminator 388 impl.position++; 389 return this; 390 } 391 392 /++ 393 If the position is abutting an end of line marker, it moves past it, to include it. 394 If not, it does nothing. 395 396 The intention is so you can delete a whole line by doing: 397 398 --- 399 with(selection) { 400 moveToStartOfLine(); 401 setAnchor(); 402 // this moves to the end of the visible line, but if you stopped here, you'd be left with an empty line 403 moveToEndOfLine(); 404 // this moves past the line marker, meaning you don't just delete the line's content, it deletes the entire line 405 moveToIncludeAdjacentEndOfLineMarker(); 406 setFocus(); 407 replaceContent(""); 408 } 409 --- 410 +/ 411 Selection moveToIncludeAdjacentEndOfLineMarker() { 412 // FIXME: i need to decide what i want to do about \r too. Prolly should remove it at the boundaries. 413 if(impl.position + 1 < layouter.text.length && layouter.text[impl.position] == '\n') { // never include the 0 terminator 414 impl.position++; 415 } 416 return this; 417 } 418 419 // note there's move up / down / left / right 420 // in addition to move forward / backward glyph/line 421 // the directions always match what's on screen. 422 // the others always match the logical order in the string. 423 /++ 424 425 +/ 426 Selection moveUp(int count = 1, bool byRender = true) { 427 verticalMoveImpl(-1, count, byRender); 428 return this; 429 } 430 431 /// ditto 432 Selection moveDown(int count = 1, bool byRender = true) { 433 verticalMoveImpl(1, count, byRender); 434 return this; 435 } 436 437 /// ditto 438 Selection moveLeft(int count = 1, bool byRender = true) { 439 horizontalMoveImpl(-1, count, byRender); 440 return this; 441 } 442 443 /// ditto 444 Selection moveRight(int count = 1, bool byRender = true) { 445 horizontalMoveImpl(1, count, byRender); 446 return this; 447 } 448 449 void find(scope const(char)[] text) { 450 } 451 452 /+ + 453 Searches by regex. 454 455 This is a template because the regex engine can be a heavy dependency, so it is only 456 included if you need it. The RegEx object is expected to match the Phobos std.regex.RegEx 457 api, so while you could, in theory, replace it, it is probably easier to just use the Phobos one. 458 +/ 459 void find(RegEx)(RegEx re) { 460 461 } 462 463 /+ Manipulating the data in the selection +/ 464 465 /++ 466 Replaces the content of the selection. If you replace it with an empty `newText`, it will delete the content. 467 468 If newText == "\b", it will delete the selection if it is non-empty, and otherwise delete the thing before the cursor. 469 470 If you want to do normal editor backspace key, you might want to check `if(!selection.isEmpty()) selection.moveLeft();` 471 before calling `selection.deleteContent()`. Similarly, for the delete key, you might use `moveRight` instead, since this 472 function will do nothing for an empty selection by itself. 473 474 FIXME: what if i want to replace it with some multiply styled text? Could probably call it in sequence actually. 475 +/ 476 Selection replaceContent(scope const(char)[] newText, TextLayouter.StyleHandle style = TextLayouter.StyleHandle.init) { 477 layouter.wasMutated_ = true; 478 479 if(style == TextLayouter.StyleHandle.init) 480 style = layouter.getInsertionStyleAt(impl.focus); 481 482 int removeBegin, removeEnd; 483 if(this.isEmpty()) { 484 if(newText.length == 1 && newText[0] == '\b') { 485 auto place = impl.focus; 486 if(place > 0) { 487 int amount = 1; 488 while((layouter.text[place - amount] & 0b11000000) == 0b10000000) // all non-start bytes of a utf-8 sequence have this convenient property 489 amount++; // assumes this will never go over the edge cuz of it being valid utf 8 internally 490 491 removeBegin = place - amount; 492 removeEnd = place; 493 494 if(removeBegin < 0) 495 removeBegin = 0; 496 if(removeEnd < 0) 497 removeEnd = 0; 498 } 499 500 newText = null; 501 } else { 502 removeBegin = impl.terminus; 503 removeEnd = impl.terminus; 504 } 505 } else { 506 removeBegin = impl.start; 507 removeEnd = impl.end; 508 if(newText.length == 1 && newText[0] == '\b') { 509 newText = null; 510 } 511 } 512 513 auto place = impl.terminus; 514 515 auto changeInLength = cast(int) newText.length - (removeEnd - removeBegin); 516 517 // FIXME: the horror 518 auto trash = layouter.text[0 .. removeBegin]; 519 trash ~= newText; 520 trash ~= layouter.text[removeEnd .. $]; 521 layouter.text = trash; 522 523 impl.position = removeBegin + cast(int) newText.length; 524 this.setAnchor(); 525 526 /+ 527 For styles: 528 if one part resides in the deleted zone, it should be truncated to the edge of the deleted zone 529 if they are entirely in the deleted zone - their new length is zero - they should simply be deleted 530 if they are entirely before the deleted zone, it can stay the same 531 if they are entirely after the deleted zone, they should get += changeInLength 532 533 FIXME: if the deleted zone lies entirely inside one of the styles, that style's length should be extended to include the new text if it has no style, or otherwise split into a few style blocks 534 535 However, the algorithm for default style in the new zone is a bit different: if at index 0 or right after a \n, it uses the next style. otherwise it uses the previous one. 536 +/ 537 538 //writeln(removeBegin, " ", removeEnd); 539 //foreach(st; layouter.styles) writeln("B: ", st.offset, "..", st.offset + st.length, " ", st.styleInformationIndex); 540 541 // first I'm going to update all of them so it is in a consistent state 542 foreach(ref st; layouter.styles) { 543 auto begin = st.offset; 544 auto end = st.offset + st.length; 545 546 void adjust(ref int what) { 547 if(what < removeBegin) { 548 // no change needed 549 } else if(what >= removeBegin && what < removeEnd) { 550 what = removeBegin; 551 } else if(what) { 552 what += changeInLength; 553 } 554 } 555 556 adjust(begin); 557 adjust(end); 558 559 assert(end >= begin); // empty styles are not permitted by the implementation 560 st.offset = begin; 561 st.length = end - begin; 562 } 563 564 // then go back and inject the new style, if needed 565 if(changeInLength > 0) { 566 changeStyle(removeBegin, removeBegin + cast(int) newText.length, style); 567 } 568 569 removeEmptyStyles(); 570 571 // or do i want to use init to just keep using the same style as is already there? 572 // FIXME 573 //if(style !is StyleHandle.init) { 574 // styles ~= StyleBlock(cast(int) before.length, cast(int) changeInLength, style.index); 575 //} 576 577 578 auto endInvalidate = removeBegin + newText.length; 579 if(removeEnd > endInvalidate) 580 endInvalidate = removeEnd; 581 layouter.invalidateLayout(removeBegin, endInvalidate, changeInLength); 582 583 // there's a new style from removeBegin to removeBegin + newText.length 584 585 // FIXME other selections in the zone need to be adjusted too 586 // if they are in the deleted zone, it should be moved to the end of the new zone (removeBegin + newText.length) 587 // if they are before the deleted zone, they can stay the same 588 // if they are after the deleted zone, they should be adjusted by changeInLength 589 foreach(idx, ref selection; layouter._selections[0 .. layouter.selectionsInUse]) { 590 591 // don't adjust ourselves here, we already did it above 592 // and besides don't want mutation in here 593 if(idx == selectionId) 594 continue; 595 596 void adjust(ref int what) { 597 if(what < removeBegin) { 598 // no change needed 599 } else if(what >= removeBegin && what < removeEnd) { 600 what = removeBegin; 601 } else if(what) { 602 what += changeInLength; 603 } 604 } 605 606 adjust(selection.anchor); 607 adjust(selection.terminus); 608 } 609 // you might need to set the user coordinate after this! 610 611 return this; 612 } 613 614 private void removeEmptyStyles() { 615 /+ the code doesn't like empty style blocks, so gonna go back and remove those +/ 616 for(int i = 0; i < cast(int) layouter.styles.length; i++) { 617 if(layouter.styles[i].length == 0) { 618 for(auto i2 = i; i2 + 1 < layouter.styles.length; i2++) 619 layouter.styles[i2] = layouter.styles[i2 + 1]; 620 layouter.styles = layouter.styles[0 .. $-1]; 621 layouter.styles.assumeSafeAppend(); 622 i--; 623 } 624 } 625 } 626 627 /++ 628 Changes the style of the given selection. Gives existing styles in the selection to your delegate 629 and you return a new style to assign to that block. 630 +/ 631 public void changeStyle(TextLayouter.StyleHandle delegate(TextStyle existing) newStyle) { 632 // FIXME there might be different sub-styles so we should actually look them up and send each one 633 auto ns = newStyle(null); 634 changeStyle(impl.start, impl.end, ns); 635 removeEmptyStyles(); 636 637 layouter.invalidateLayout(impl.start, impl.end, 0); 638 } 639 640 /+ Impl helpers +/ 641 642 private void changeStyle(int newStyleBegin, int newStyleEnd, TextLayouter.StyleHandle style) { 643 // FIXME: binary search 644 for(size_t i = 0; i < layouter.styles.length; i++) { 645 auto s = &layouter.styles[i]; 646 const oldStyleBegin = s.offset; 647 const oldStyleEnd = s.offset + s.length; 648 649 if(newStyleBegin >= oldStyleBegin && newStyleBegin < oldStyleEnd) { 650 // the cases: 651 652 // it is an exact match in size, we can simply overwrite it 653 if(newStyleBegin == oldStyleBegin && newStyleEnd == oldStyleEnd) { 654 s.styleInformationIndex = style.index; 655 break; // all done 656 } 657 // we're the same as the existing style, so it is just a matter of extending it to include us 658 else if(s.styleInformationIndex == style.index) { 659 if(newStyleEnd > oldStyleEnd) { 660 s.length = newStyleEnd - oldStyleBegin; 661 662 // then need to fix up all the subsequent blocks, adding the offset, reducing the length 663 int remainingFixes = newStyleEnd - oldStyleEnd; 664 foreach(st; layouter.styles[i + 1 .. $]) { 665 auto thisFixup = remainingFixes; 666 if(st.length < thisFixup) 667 thisFixup = st.length; 668 // this can result in 0 length, the loop after this will delete that. 669 st.offset += thisFixup; 670 st.length -= thisFixup; 671 672 remainingFixes -= thisFixup; 673 674 assert(remainingFixes >= 0); 675 676 if(remainingFixes == 0) 677 break; 678 } 679 } 680 // otherwise it is all already there and nothing need be done at all 681 break; 682 } 683 // for the rest of the cases, the style does not match and is not a size match, 684 // so a new block is going to have to be inserted 685 // /////////// 686 // we're entirely contained inside, so keep the left, insert ourselves, and re-create right. 687 else if(newStyleEnd > oldStyleBegin && newStyleEnd < oldStyleEnd) { 688 // keep the old style on the left... 689 s.length = newStyleBegin - oldStyleBegin; 690 691 auto toInsert1 = TextLayouter.StyleBlock(newStyleBegin, newStyleEnd - newStyleBegin, style.index); 692 auto toInsert2 = TextLayouter.StyleBlock(newStyleEnd, oldStyleEnd - newStyleEnd, s.styleInformationIndex); 693 694 layouter.styles = layouter.styles[0 .. i + 1] ~ toInsert1 ~ toInsert2 ~ layouter.styles[i + 1 .. $]; 695 696 // writeln(*s); writeln(toInsert1); writeln(toInsert2); 697 698 break; // no need to continue processing as the other offsets are unaffected 699 } 700 // we need to keep the left end of the original thing, but then insert ourselves on afterward 701 else if(newStyleBegin >= oldStyleBegin) { 702 // want to just shorten the original thing, then adjust the values 703 // so next time through the loop can work on that existing block 704 705 s.length = newStyleBegin - oldStyleBegin; 706 707 // extend the following style to start here, so there's no gap in the next loop iteration 708 if(i + i < layouter.styles.length) { 709 auto originalOffset = layouter.styles[i+1].offset; 710 assert(originalOffset >= newStyleBegin); 711 layouter.styles[i+1].offset = newStyleBegin; 712 layouter.styles[i+1].length += originalOffset - newStyleBegin; 713 714 // i will NOT change the style info index yet, since the next iteration will do that 715 continue; 716 } else { 717 // at the end of the loop we can just append the new thing and break out of here 718 layouter.styles ~= TextLayouter.StyleBlock(newStyleBegin, newStyleEnd - newStyleBegin, style.index); 719 break; 720 } 721 } 722 else { 723 // this should be impossible as i think i covered all the cases above 724 // as we iterate through 725 // writeln(oldStyleBegin, "..", oldStyleEnd, " -- ", newStyleBegin, "..", newStyleEnd); 726 assert(0); 727 } 728 } 729 } 730 731 // foreach(st; layouter.styles) writeln("A: ", st.offset, "..", st.offset + st.length, " ", st.styleInformationIndex); 732 } 733 734 // returns the edge of the new cursor position 735 private void horizontalMoveImpl(int direction, int count, bool byRender) { 736 assert(direction != 0); 737 738 auto place = impl.focus + direction; 739 740 foreach(c; 0 .. count) { 741 while(place >= 0 && place < layouter.text.length && (layouter.text[place] & 0b11000000) == 0b10000000) // all non-start bytes of a utf-8 sequence have this convenient property 742 place += direction; 743 } 744 745 // FIXME if(byRender), if we're on a rtl line, swap the things. but if it is mixed it won't even do anything and stay in logical order 746 747 if(place < 0) 748 place = 0; 749 if(place >= layouter.text.length) 750 place = cast(int) layouter.text.length - 1; 751 752 impl.position = place; 753 754 } 755 756 // returns the baseline of the new cursor 757 private void verticalMoveImpl(int direction, int count, bool byRender) { 758 assert(direction != 0); 759 // this needs to find the closest glyph on the virtual x on the previous (rendered) line 760 761 int segmentIndex = layouter.findContainingSegment(impl.terminus); 762 763 // we know this is going to lead to a different segment since 764 // the layout breaks up that way, so we can just go straight backward 765 766 auto segment = layouter.segments[segmentIndex]; 767 768 auto idealX = impl.virtualFocusPosition.x; 769 770 auto targetLineNumber = segment.displayLineNumber + (direction * count); 771 if(targetLineNumber < 0) 772 targetLineNumber = 0; 773 774 // FIXME if(!byRender) 775 776 777 // FIXME: when you are going down, a line that begins with tab doesn't highlight the right char. 778 779 int bestHit = -1; 780 int bestHitDistance = int.max; 781 782 // writeln(targetLineNumber, " ", segmentIndex, " ", layouter.segments.length); 783 784 segmentLoop: while(segmentIndex >= 0 && segmentIndex < layouter.segments.length) { 785 segment = layouter.segments[segmentIndex]; 786 if(segment.displayLineNumber == targetLineNumber) { 787 // we're in the right line... but not necessarily the right segment 788 // writeln("line found"); 789 if(idealX >= segment.boundingBox.left && idealX < segment.boundingBox.right) { 790 // need to find the exact thing in here 791 792 auto hit = segment.textBeginOffset; 793 auto ul = segment.upperLeft; 794 795 bool found; 796 auto txt = layouter.text[segment.textBeginOffset .. segment.textEndOffset]; 797 auto codepoint = 0; 798 foreach(idx, dchar d; txt) { 799 auto width = layouter.segmentsWidths[segmentIndex][codepoint]; 800 801 hit = segment.textBeginOffset + cast(int) idx; 802 803 auto distanceToLeft = ul.x - idealX; 804 if(distanceToLeft < 0) distanceToLeft = -distanceToLeft; 805 if(distanceToLeft < bestHitDistance) { 806 bestHit = hit; 807 bestHitDistance = distanceToLeft; 808 } else { 809 // getting further away = no help 810 break; 811 } 812 813 /* 814 // FIXME: I probably want something slightly different 815 if(ul.x >= idealX) { 816 found = true; 817 break; 818 } 819 */ 820 821 ul.x += width; 822 codepoint++; 823 } 824 825 /* 826 if(!found) 827 hit = segment.textEndOffset - 1; 828 829 impl.position = hit; 830 bestHit = -1; 831 */ 832 833 impl.position = bestHit; 834 bestHit = -1; 835 836 // selections[selectionId].virtualFocusPosition = Point(selections[selectionId].virtualFocusPosition.x, segment.boundingBox.bottom); 837 838 break segmentLoop; 839 } else { 840 // FIXME: assuming ltr here 841 auto distance = idealX - segment.boundingBox.right; 842 if(distance < 0) 843 distance = -distance; 844 if(bestHit == -1 || distance < bestHitDistance) { 845 bestHit = segment.textEndOffset - 1; 846 bestHitDistance = distance; 847 } 848 } 849 } else if(bestHit != -1) { 850 impl.position = bestHit; 851 bestHit = -1; 852 break segmentLoop; 853 } 854 855 segmentIndex += direction; 856 } 857 858 if(bestHit != -1) 859 impl.position = bestHit; 860 861 if(impl.position == layouter.text.length) 862 impl.position -- ; // never select the eof marker 863 } 864 } 865 866 private struct SelectionImpl { 867 // you want multiple selections at most points 868 int id; 869 int anchor; 870 int terminus; 871 872 int position; 873 874 alias focus = terminus; 875 876 /+ 877 As you move through lines of different lengths, your actual x will change, 878 but the user will want to stay in the same relative spot, consider passing: 879 880 long thing 881 short 882 long thing 883 884 from the 'i'. When you go down, you'd be back by the t, but go down again, you should 885 go back to the i. This variable helps achieve this. 886 +/ 887 Point virtualFocusPosition; 888 889 int start() { 890 return anchor <= terminus ? anchor : terminus; 891 } 892 int end() { 893 return anchor <= terminus ? terminus : anchor; 894 } 895 bool empty() { 896 return anchor == terminus; 897 } 898 bool containsOffset(int textOffset) { 899 return textOffset >= start && textOffset < end; 900 } 901 bool isIncludedInRange(int textStart, int textEnd) { 902 // if either end are in there, we're obviously in the range 903 if((start >= textStart && start < textEnd) || (end >= textStart && end < textEnd)) 904 return true; 905 // or if the selection is entirely inside the given range... 906 if(start >= textStart && end < textEnd) 907 return true; 908 // or if the given range is at all inside the selection 909 if((textStart >= start && textStart < end) || (textEnd >= start && textEnd < end)) 910 return true; 911 return false; 912 } 913 } 914 915 /++ 916 Bugs: 917 Only tested on Latin scripts at this time. Other things should be possible but might need work. Let me know if you need it and I'll see what I can do. 918 +/ 919 class TextLayouter { 920 921 922 // actually running this invariant gives quadratic performance in the layouter (cuz of isWordwrapPoint lol) 923 // so gonna only version it in for special occasions 924 version(none) 925 invariant() { 926 // There is one and exactly one segment for every char in the string. 927 // The segments are stored in sequence from index 0 to the end of the string. 928 // styleInformationIndex is always in bounds of the styles array. 929 // There is one and exactly one style block for every char in the string. 930 // Style blocks are stored in sequence from index 0 to the end of the string. 931 932 assert(text.length > 0 && text[$-1] == 0); 933 assert(styles.length >= 1); 934 int last = 0; 935 foreach(style; styles) { 936 assert(style.offset == last); // all styles must be in order and contiguous 937 assert(style.length > 0); // and non-empty 938 assert(style.styleInformationIndex != -1); // and not default constructed (this should be resolved before adding) 939 assert(style.styleInformationIndex >= 0 && style.styleInformationIndex < stylePalette.length); // all must be in-bounds 940 last = style.offset + style.length; 941 } 942 assert(last == text.length); // and all chars in the array must be covered by a style block 943 } 944 945 /++ 946 Gets the object representing the given selection. 947 948 Normally, id = 0 is the user's selection, then id's 60, 61, 62, and 63 are private to the application. 949 +/ 950 Selection selection(int id = 0) { 951 assert(id >= 0 && id < _selections.length); 952 return Selection(this, id); 953 } 954 955 /++ 956 The rendered size of the text. 957 +/ 958 public int width() { 959 relayoutIfNecessary(); 960 return _width; 961 } 962 963 /// ditto 964 public int height() { 965 relayoutIfNecessary(); 966 return _height; 967 } 968 969 static struct State { 970 // for the delta compression, the text is the main field to worry about 971 // and what it really needs to know is just based on what, then what is added and what is removed. 972 // i think everything else i'd just copy in (or reference the same array) anyway since they're so 973 // much smaller anyway. 974 // 975 // and if the text is small might as well just copy/reference it too tbh. 976 private { 977 char[] text; 978 TextStyle[] stylePalette; 979 StyleBlock[] styles; 980 SelectionImpl[] selections; 981 } 982 } 983 984 // for manual undo stuff 985 // and all state should be able to do do it incrementally too; each modification to those should be compared. 986 /++ 987 The editor's internal state can be saved and restored as an opaque blob. You might use this to make undo checkpoints and similar. 988 989 Its implementation may use delta compression from a previous saved state, it will try to do this transparently for you to save memory. 990 +/ 991 const(State)* saveState() { 992 return new State(text.dup, stylePalette.dup, styles.dup, _selections.dup); 993 } 994 /// ditto 995 void restoreState(const(State)* state) { 996 auto changeInLength = cast(int) this.text.length - cast(int) state.text.length; 997 this.text = state.text.dup; 998 // FIXME: bad cast 999 this.stylePalette = (cast(TextStyle[]) state.stylePalette).dup; 1000 this.styles = state.styles.dup; 1001 this._selections = state.selections.dup; 1002 1003 invalidateLayout(0, text.length, changeInLength); 1004 } 1005 1006 // FIXME: I might want to make the original line number exposed somewhere too like in the segment draw information 1007 1008 // FIXME: all the actual content - styles, text, and selection stuff - needs to be able to communicate its changes 1009 // incrementally for the network use case. the segments tho not that important. 1010 1011 // FIXME: for the password thing all the glyph positions need to be known to this system, so it can't just draw it 1012 // that way (unless it knows it is using a monospace font... but we can trick it by giving it a fake font that gives all those metrics) 1013 // so actually that is the magic lol 1014 1015 private static struct StyleBlock { 1016 int offset; 1017 int length; 1018 1019 int styleInformationIndex; 1020 } 1021 1022 void resetSelection(int selectionId) { 1023 1024 } 1025 1026 // FIXME: is it moving teh anchor or the focus? 1027 void extendSelection(int selectionId, bool fromBeginning, bool direction, int delegate(scope const char[] c) handler) { 1028 // iterates through the selection, giving you the chars, until you return 0 1029 // can use this to do things like delete words in front of cursor 1030 } 1031 1032 void duplicateSelection(int receivingSelectionId, int sourceSelectionId) { 1033 1034 } 1035 1036 private int findContainingSegment(int textOffset) { 1037 1038 relayoutIfNecessary(); 1039 1040 // FIXME: binary search 1041 1042 // FIXME: when the index is here, use it 1043 foreach(idx, segment; segments) { 1044 // this assumes the segments are in order of text offset 1045 if(textOffset >= segment.textBeginOffset && textOffset < segment.textEndOffset) 1046 return cast(int) idx; 1047 } 1048 assert(0); 1049 } 1050 1051 // need page up+down, home, edit, arrows, etc. 1052 1053 /++ 1054 Finds the given text, setting the given selection to it, if found. 1055 1056 Starts from the given selection and moves in the direction to find next. 1057 1058 Returns true if found. 1059 +/ 1060 FindResult find(int selectionId, in const(char)[] text, bool direction, bool wraparound) { 1061 return FindResult.NotFound; 1062 } 1063 /// ditto 1064 enum FindResult : int { 1065 NotFound = 0, 1066 Found = 1, 1067 WrappedAround = 2 1068 } 1069 1070 private bool wasMutated_ = false; 1071 /++ 1072 The layouter maintains a flag to tell if the content has been changed. 1073 +/ 1074 public bool wasMutated() { 1075 return wasMutated_; 1076 } 1077 1078 /// ditto 1079 public void clearWasMutatedFlag() { 1080 wasMutated_ = false; 1081 } 1082 1083 /++ 1084 Represents a possible registered style for a segment of text. 1085 +/ 1086 public static struct StyleHandle { 1087 private this(int idx) { this.index = idx; } 1088 private int index = -1; 1089 } 1090 1091 /++ 1092 Registers a text style you can use in text segments. 1093 +/ 1094 // FIXME: i might have to construct it internally myself so i can return it const. 1095 public StyleHandle registerStyle(TextStyle style) { 1096 stylePalette ~= style; 1097 return StyleHandle(cast(int) stylePalette.length - 1); 1098 } 1099 1100 1101 /++ 1102 Appends text at the end, without disturbing user selection. 1103 +/ 1104 public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init) { 1105 wasMutated_ = true; 1106 auto before = this.text; 1107 this.text.length += text.length; 1108 this.text[before.length-1 .. before.length-1 + text.length] = text[]; 1109 this.text[$-1] = 0; // gotta maintain the zero terminator i use 1110 // or do i want to use init to just keep using the same style as is already there? 1111 if(style is StyleHandle.init) { 1112 // default is to extend the existing style 1113 styles[$-1].length += text.length; 1114 } else { 1115 // otherwise, insert a new block for it 1116 styles[$-1].length -= 1; // it no longer covers the zero terminator 1117 1118 // but this does, hence the +1 1119 styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index); 1120 } 1121 1122 invalidateLayout(cast(int) before.length - 1 /* zero terminator */, this.text.length, cast(int) text.length); 1123 } 1124 1125 /++ 1126 Calls your delegate for each segment of the text, guaranteeing you will be called exactly once for each non-nil char in the string and each slice will have exactly one style. A segment may be as small as a single char. 1127 1128 FIXME: have a getTextInSelection 1129 1130 FIXME: have some kind of index stuff so you can select some text found in here (think regex search) 1131 +/ 1132 void getText(scope void delegate(scope const(char)[] segment, TextStyle style) handler) { 1133 handler(text[0 .. $-1], null); // cut off the null terminator 1134 } 1135 1136 /++ 1137 Gets the current text value as a plain-text string. 1138 +/ 1139 string getTextString() { 1140 string s; 1141 getText((segment, style) { 1142 s ~= segment; 1143 }); 1144 return s; 1145 } 1146 1147 public static struct DrawingInformation { 1148 Rectangle boundingBox; 1149 Point initialBaseline; 1150 ulong selections; // 0 if not selected. bitmask of selection ids otherwise 1151 1152 int direction; // you start at initialBaseline then draw ltr or rtl or up or down. 1153 // might also store glyph id, which could be encoded texture # + position, stuff like that. if each segment were 1154 // a glyph at least which is sometimes needed but prolly not gonna stress abut that in my use cases, i'd rather batch. 1155 } 1156 1157 public static struct CaretInformation { 1158 int id; 1159 Rectangle boundingBox; 1160 } 1161 1162 // assumes the idx is indeed in the segment 1163 private Rectangle boundingBoxOfGlyph(size_t segmentIndex, int idx) { 1164 // I can't relayoutIfNecessary here because that might invalidate the segmentIndex!! 1165 // relayoutIfNecessary(); 1166 auto segment = segments[segmentIndex]; 1167 1168 int codepointCounter = 0; 1169 auto bb = segment.boundingBox; 1170 foreach(thing, dchar cp; text[segment.textBeginOffset .. segment.textEndOffset]) { 1171 auto w = segmentsWidths[segmentIndex][codepointCounter]; 1172 1173 if(thing + segment.textBeginOffset == idx) { 1174 bb.right = bb.left + w; 1175 return bb; 1176 } 1177 1178 bb.left += w; 1179 1180 codepointCounter++; 1181 } 1182 1183 bb.right = bb.left + 1; 1184 1185 return bb; 1186 } 1187 1188 void getTextAtPosition(Point p) { 1189 relayoutIfNecessary(); 1190 // return the text in that segment, the style info attached, and if that specific point is part of a selection (can be used to tell if it should be a drag operation) 1191 // then might want dropTextAt(Point p) 1192 } 1193 1194 /++ 1195 Gets the text that you need to draw, guaranteeing each call to your delegate will: 1196 1197 * Have a contiguous slice into text 1198 * Have exactly one style (which may be null, meaning use all your default values. Be sure you draw with the same font you passed as the default font to TextLayouter.) 1199 * Be a linear block of text that fits in a single rectangular segment 1200 * A segment will be as large a block of text as the implementation can do, but it may be as short as a single char. 1201 * The segment may be a special escape sequence. FIXME explain how to check this. 1202 1203 Return `false` from your delegate to stop iterating through the text. 1204 1205 Please note that the `caretPosition` can be `Rectangle.init`, indicating it is not present in this segment. If it is not that, it will be the bounding box of the glyph. 1206 1207 You can use the `startFrom` parameter to skip ahead. The intended use case for this is to start from a scrolling position in the box; the first segment given will include this point. FIXME: maybe it should just go ahead and do a bounding box. Note that the segments may extend outside the point; it is just meant that it will include that and try to trim the rest. 1208 1209 The segment may include all forms of whitespace, including newlines, tab characters, etc. Generally, a tab character will be in its own segment and \n will appear at the end of a segment. You will probably want to `stripRight` each segment depending on your drawing functions. 1210 +/ 1211 void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) { 1212 relayoutIfNecessary(); 1213 getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) { 1214 if(segment.textBeginOffset == -1) 1215 return true; 1216 1217 TextStyle style; 1218 assert(segment.styleInformationIndex < stylePalette.length); 1219 1220 style = stylePalette[segment.styleInformationIndex]; 1221 1222 ubyte[64] possibleSelections; 1223 int possibleSelectionsCount; 1224 1225 CaretInformation[64] caretInformation; 1226 int cic; 1227 1228 // bounding box reduction 1229 foreach(si, selection; _selections[0 .. selectionsInUse]) { 1230 if(selection.isIncludedInRange(segment.textBeginOffset, segment.textEndOffset)) { 1231 if(!selection.empty()) { 1232 possibleSelections[possibleSelectionsCount++] = cast(ubyte) si; 1233 } 1234 if(selection.focus >= segment.textBeginOffset && selection.focus < segment.textEndOffset) { 1235 1236 // make sure the caret box represents that it would be if we actually 1237 // did the insertion, so adjust the bounding box to account for a possibly 1238 // different font 1239 1240 auto insertionStyle = stylePalette[getInsertionStyleAt(selection.focus).index]; 1241 auto glyphStyle = style; 1242 1243 auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus); 1244 1245 bb.top += glyphStyle.font.ascent; 1246 bb.bottom -= glyphStyle.font.descent; 1247 1248 bb.top -= insertionStyle.font.ascent; 1249 bb.bottom += insertionStyle.font.descent; 1250 1251 caretInformation[cic++] = CaretInformation(cast(int) si, bb); 1252 } 1253 } 1254 } 1255 1256 // the rest of this might need splitting based on selections 1257 1258 DrawingInformation di; 1259 di.boundingBox = Rectangle(segment.upperLeft, Size(segment.width, segment.height)); 1260 di.selections = 0; 1261 1262 // di.initialBaseline = Point(x, y); // FIXME 1263 // FIXME if the selection focus is in this box, we should set the caretPosition to the bounding box of the associated glyph 1264 // di.caretPosition = Rectangle(x, y, w, h); // FIXME 1265 1266 auto end = segment.textEndOffset; 1267 if(end == text.length) 1268 end--; // don't send the terminating 0 to the user as that's an internal detail 1269 1270 auto txt = text[segment.textBeginOffset .. end]; 1271 1272 if(possibleSelectionsCount == 0) { 1273 // no selections present, no need to iterate 1274 // FIXME: but i might have to take some gap chars and such out anyway. 1275 return dg(txt, style, di, caretInformation[0 .. cic]); 1276 } else { 1277 ulong lastSel = 0; 1278 size_t lastSelPos = 0; 1279 size_t lastSelCodepoint = 0; 1280 bool exit = false; 1281 1282 void sendSegment(size_t start, size_t end, size_t codepointStart, size_t codepointEnd) { 1283 di.selections = lastSel; 1284 1285 Rectangle bbOriginal = di.boundingBox; 1286 1287 int segmentWidth; 1288 1289 foreach(width; segmentsWidths[segmentIndex][codepointStart .. codepointEnd]) { 1290 segmentWidth += width; 1291 } 1292 1293 auto diFragment = di; 1294 diFragment.boundingBox.right = diFragment.boundingBox.left + segmentWidth; 1295 1296 // FIXME: adjust the rest of di for this 1297 // FIXME: the caretInformation arguably should be truncated for those not in this particular sub-segment 1298 exit = !dg(txt[start .. end], style, diFragment, caretInformation[0 .. cic]); 1299 1300 di.initialBaseline.x += segmentWidth; 1301 di.boundingBox.left += segmentWidth; 1302 1303 lastSelPos = end; 1304 lastSelCodepoint = codepointEnd; 1305 } 1306 1307 size_t codepoint = 0; 1308 1309 foreach(ci, dchar ch; txt) { 1310 auto sel = selectionsAt(cast(int) ci + segment.textBeginOffset); 1311 if(sel != lastSel) { 1312 // send this segment 1313 1314 sendSegment(lastSelPos, ci, lastSelCodepoint, codepoint); 1315 lastSel = sel; 1316 if(exit) return false; 1317 } 1318 1319 codepoint++; 1320 } 1321 1322 sendSegment(lastSelPos, txt.length, lastSelCodepoint, codepoint); 1323 if(exit) return false; 1324 } 1325 1326 return true; 1327 }, box); 1328 } 1329 1330 // returns any segments that may lie inside the bounding box. if the box's size is 0, it is unbounded and goes through all segments 1331 // may return more than is necessary; it uses the box as a hint to speed the search, not as the strict bounds it returns. 1332 void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) { 1333 relayoutIfNecessary(); 1334 1335 if(box.right == box.left) 1336 box.right = int.max; 1337 if(box.bottom == box.top) 1338 box.bottom = int.max; 1339 1340 if(segments.length < 64 || box.top < 64) { 1341 foreach(idx, ref segment; segments) { 1342 if(dg(idx, segment) == false) 1343 break; 1344 } 1345 } else { 1346 int maximum = cast(int) segments.length; 1347 int searchPoint = maximum / 2; 1348 1349 keepSearching: 1350 //writeln(searchPoint); 1351 if(segments[searchPoint].upperLeft.y > box.top) { 1352 // we're too far ahead to find the box 1353 maximum = searchPoint; 1354 auto newSearchPoint = maximum / 2; 1355 if(newSearchPoint == searchPoint) { 1356 searchPoint = newSearchPoint; 1357 goto useIt; 1358 } 1359 searchPoint = newSearchPoint; 1360 goto keepSearching; 1361 } else if(segments[searchPoint].boundingBox.bottom < box.top) { 1362 // the box is a way down from here still 1363 auto newSearchPoint = (maximum - searchPoint) / 2 + searchPoint; 1364 if(newSearchPoint == searchPoint) { 1365 searchPoint = newSearchPoint; 1366 goto useIt; 1367 } 1368 searchPoint = newSearchPoint; 1369 goto keepSearching; 1370 } 1371 1372 useIt: 1373 1374 auto line = segments[searchPoint].displayLineNumber; 1375 if(line) { 1376 // go to the line right before this to ensure we have everything in here 1377 while(searchPoint != 0 && segments[searchPoint].displayLineNumber == line) 1378 searchPoint--; 1379 } 1380 1381 foreach(idx, ref segment; segments[searchPoint .. $]) { 1382 if(dg(idx + searchPoint, segment) == false) 1383 break; 1384 } 1385 } 1386 } 1387 1388 private { 1389 // user code can add new styles to the palette 1390 TextStyle[] stylePalette; 1391 1392 // if editable by user, these will change 1393 char[] text; 1394 StyleBlock[] styles; 1395 1396 // the layout function calculates these 1397 Segment[] segments; 1398 short[][] segmentsWidths; 1399 } 1400 1401 /++ 1402 1403 +/ 1404 this(TextStyle defaultStyle) { 1405 this.stylePalette ~= defaultStyle; 1406 this.text = [0]; // i never want to let the editor go over, so this pseudochar makes that a bit easier 1407 this.styles ~= StyleBlock(0, 1, 0); // default style should never be deleted too at the end of the file 1408 this.invalidateLayout(0, 1, 0); 1409 } 1410 1411 // maybe unstable 1412 TextStyle defaultStyle() { 1413 auto ts = this.stylePalette[0]; 1414 invalidateLayout(0, text.length, 0); // assume they are going to mutate it 1415 return ts; 1416 } 1417 1418 bool editable; 1419 int wordWrapLength = 0; 1420 int delegate(int x) tabStop = null; 1421 int delegate(Rectangle line) leftOffset = null; 1422 int delegate(Rectangle line) rightOffset = null; 1423 int lineSpacing = 0; 1424 1425 /+ 1426 the function it could call is drawStringSegment with a certain slice of it, an offset (guaranteed to be rectangular) and then you do the styles. it does need to know the font tho. 1427 1428 it takes a flag: UpperLeft or Baseline. this tells its coordinates for the string segment when you draw. 1429 1430 The style can just be a void* or something, not really the problem of the layouter; it only cares about font metrics 1431 1432 The layout thing needs to know: 1433 1) is it word wrapped 1434 2) a delegate for offset left for the given line height 1435 2) a delegate for offset right for the given line height 1436 1437 GetSelection() returns the segments that are currently selected 1438 Caret position, if there is one 1439 1440 Each TextLayouter can represent a block element in HTML terms. Centering and such done outside. 1441 Selections going across blocks is an exercise for the outside code (it'd select start to all of one, all of middle, all to end of last). 1442 1443 1444 EDITING: 1445 just like addText which it does replacing the selection if there is one or inserting/overstriking at the caret 1446 1447 everything has an optional void* style which it does as offset-based overlays 1448 1449 user responsibility to getSelection if they want to add something to the style 1450 +/ 1451 1452 private static struct Segment { 1453 // 32 bytes rn, i can reasonably save 6 with shorts 1454 // do i even need the segmentsWidths cache or can i reasonably recalculate it lazily? 1455 1456 int textBeginOffset; 1457 int textEndOffset; // can make a short length i think 1458 1459 int styleInformationIndex; 1460 1461 // calculated values after iterating through the segment 1462 int width; // short 1463 int height; // short 1464 1465 Point upperLeft; 1466 1467 int displayLineNumber; // I might change this to be a fractional thing, like 24 bits line number, 8 bits fractional number (from word wrap) tho realistically i suspect an index of original lines would be easier to maintain (could only have one value per like 100 real lines cuz it just narrows down the linear search 1468 1469 /* 1470 Point baseline() { 1471 1472 } 1473 */ 1474 1475 Rectangle boundingBox() { 1476 return Rectangle(upperLeft, Size(width, height)); 1477 } 1478 } 1479 1480 private int _width; 1481 private int _height; 1482 1483 private SelectionImpl[64] _selections; 1484 private int selectionsInUse = 1; 1485 1486 /++ 1487 Selections have two parts: an anchor (typically set to where the user clicked the mouse down) 1488 and a focus (typically where the user released the mouse button). As you click and drag, you 1489 want to change the focus while keeping the anchor the same. 1490 1491 The caret is drawn at the focus. If the anchor and focus are the same point, the selection 1492 is empty. 1493 1494 Please note that the selection focus is different than a keyboard focus. (I'd personally prefer 1495 to call it a terminus, but I'm trying to use the same terminology as the web standards, even if 1496 I don't like it.) 1497 1498 After calling this, you don't need to call relayout(), but you might want to redraw to show the 1499 user the result of this action. 1500 +/ 1501 1502 // FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data) 1503 1504 /+ 1505 Returns the nearest offset in the text for the given point. 1506 1507 it should return if it was inside the segment bounding box tho 1508 +/ 1509 int offsetOfClick(Point p) { 1510 int idx = cast(int) text.length - 1; 1511 1512 relayoutIfNecessary(); 1513 1514 if(p.y > _height) 1515 return idx; 1516 1517 getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) { 1518 idx = segment.textBeginOffset; 1519 // FIXME: this all assumes ltr 1520 1521 auto boundingBox = Rectangle(segment.upperLeft, Size(segment.width, segment.height)); 1522 if(boundingBox.contains(p)) { 1523 int x = segment.upperLeft.x; 1524 int codePointIndex = 0; 1525 1526 int bestHit = int.max; 1527 int bestHitDistance = int.max; 1528 if(bestHitDistance < 0) bestHitDistance = -bestHitDistance; 1529 foreach(i, dchar ch; text[segment.textBeginOffset .. segment.textEndOffset]) { 1530 const width = segmentsWidths[segmentIndex][codePointIndex]; 1531 idx = segment.textBeginOffset + cast(int) i; // can't just idx++ since it needs utf-8 stride 1532 1533 auto distanceToLeft = p.x - x; 1534 if(distanceToLeft < 0) distanceToLeft = -distanceToLeft; 1535 1536 //auto distanceToRight = p.x - (x + width); 1537 //if(distanceToRight < 0) distanceToRight = -distanceToRight; 1538 1539 //bool improved = false; 1540 1541 if(distanceToLeft < bestHitDistance) { 1542 bestHit = idx; 1543 bestHitDistance = distanceToLeft; 1544 // improved = true; 1545 } 1546 /* 1547 if(distanceToRight < bestHitDistance) { 1548 bestHit = idx + 1; 1549 bestHitDistance = distanceToRight; 1550 improved = true; 1551 } 1552 */ 1553 1554 //if(!improved) { 1555 // we're moving further away, no point continuing 1556 // (please note that RTL transitions = different segment) 1557 //break; 1558 //} 1559 1560 x += width; 1561 codePointIndex++; 1562 } 1563 1564 if(bestHit != int.max) 1565 idx = bestHit; 1566 1567 return false; 1568 } else if(p.x < boundingBox.left && p.y >= boundingBox.top && p.y < boundingBox.bottom) { 1569 // to the left of a line 1570 // assumes ltr 1571 idx = segment.textBeginOffset; 1572 return false; 1573 /+ 1574 } else if(p.x >= boundingBox.right && p.y >= boundingBox.top && p.y < boundingBox.bottom) { 1575 // to the right of a line 1576 idx = segment.textEndOffset; 1577 return false; 1578 +/ 1579 } else if(p.y < segment.upperLeft.y) { 1580 // should go to the end of the previous line 1581 auto thisLine = segment.displayLineNumber; 1582 idx = 0; 1583 while(segmentIndex > 0) { 1584 segmentIndex--; 1585 1586 if(segments[segmentIndex].displayLineNumber < thisLine) { 1587 idx = segments[segmentIndex].textEndOffset - 1; 1588 break; 1589 } 1590 } 1591 return false; 1592 } else { 1593 // for single line if nothing else matched we'd best put it at the end; will be reset for the next iteration 1594 // if there is one. and if not, this is where we want it - at the end of the text 1595 idx = cast(int) text.length - 1; 1596 } 1597 1598 return true; 1599 }, Rectangle(p, Size(0, 0))); 1600 return idx; 1601 } 1602 1603 private StyleHandle getInsertionStyleAt(int offset) { 1604 assert(offset >= 0 && offset < text.length); 1605 /+ 1606 If we are at the first part of a logical line, use the next local style (the one in bounds at the offset). 1607 1608 Otherwise, use the previous one (the one in bounds). 1609 +/ 1610 1611 if(offset == 0 || text[offset - 1] == '\n') { 1612 // no adjust needed, we use the style here 1613 } else { 1614 offset--; // use the previous one 1615 } 1616 1617 return getStyleAt(offset); 1618 } 1619 1620 private StyleHandle getStyleAt(int offset) { 1621 // FIXME: binary search 1622 foreach(style; styles) { 1623 if(offset >= style.offset && offset < (style.offset + style.length)) 1624 return StyleHandle(style.styleInformationIndex); 1625 } 1626 assert(0); 1627 } 1628 1629 ulong selectionsAt(int offset) { 1630 ulong result; 1631 ulong bit = 1; 1632 foreach(selection; _selections[0 .. selectionsInUse]) { 1633 if(selection.containsOffset(offset)) 1634 result |= bit; 1635 bit <<= 1; 1636 } 1637 return result; 1638 } 1639 1640 private int wordWrapWidth_; 1641 1642 /++ 1643 Set to 0 to disable word wrapping. 1644 +/ 1645 public void wordWrapWidth(int width) { 1646 if(width != wordWrapWidth_) { 1647 wordWrapWidth_ = width; 1648 invalidateLayout(0, text.length, 0); 1649 } 1650 } 1651 1652 private int justificationWidth_; 1653 1654 /++ 1655 1656 +/ 1657 public void justificationWidth(int width) { 1658 if(width != justificationWidth_) { 1659 justificationWidth_ = width; 1660 invalidateLayout(0, text.length, 0); 1661 } 1662 } 1663 1664 protected bool isWordwrapPoint(dchar c) { 1665 if(c == ' ') 1666 return true; 1667 return false; 1668 } 1669 1670 private bool invalidateLayout_; 1671 private int invalidStart = int.max; 1672 private int invalidEnd = 0; 1673 private int invalidatedChangeInTextLength = 0; 1674 /++ 1675 This should be called (internally, end users don't need to see it) any time the text or style has changed. 1676 +/ 1677 protected void invalidateLayout(size_t start, size_t end, int changeInTextLength) { 1678 invalidateLayout_ = true; 1679 1680 if(start < invalidStart) 1681 invalidStart = cast(int) start; 1682 if(end > invalidEnd) 1683 invalidEnd = cast(int) end; 1684 1685 invalidatedChangeInTextLength += changeInTextLength; 1686 } 1687 1688 /++ 1689 This should be called (internally, end users don't need to see it) any time you're going to return something to the user that is dependent on the layout. 1690 +/ 1691 protected void relayoutIfNecessary() { 1692 if(invalidateLayout_) { 1693 relayoutImplementation(); 1694 invalidateLayout_ = false; 1695 invalidStart = int.max; 1696 invalidEnd = 0; 1697 invalidatedChangeInTextLength = 0; 1698 } 1699 } 1700 1701 /++ 1702 Params: 1703 wordWrapLength = the length, in display pixels, of the layout's bounding box as far as word wrap is concerned. If 0, word wrapping is disabled. 1704 1705 FIXME: wordWrapChars and if you word wrap, should it indent it too? more realistically i pass the string to the helper and it has to findWordBoundary and then it can prolly return the left offset too, based on the previous line offset perhaps. 1706 1707 substituteGlyph? actually that can prolly be a fake password font. 1708 1709 1710 int maximumHeight. if non-zero, the leftover text is returned so you can pass it to another layout instance (e.g. for columns or pagination) 1711 +/ 1712 protected void relayoutImplementation() { 1713 1714 1715 // an optimization here is to avoid redoing stuff outside the invalidated zone. 1716 // basically it would keep going until a segment after the invalidated end area was in the state before and after. 1717 1718 debug(text_layouter_bench) { 1719 // writeln("relayouting"); 1720 import core.time; 1721 auto start = MonoTime.currTime; 1722 scope(exit) { 1723 writeln(MonoTime.currTime - start); 1724 } 1725 } 1726 1727 auto originalSegments = segments; 1728 auto originalWidth = _width; 1729 auto originalHeight = _height; 1730 auto originalSegmentsWidths = segmentsWidths; 1731 1732 _width = 0; 1733 _height = 0; 1734 1735 assert(invalidStart != int.max); 1736 assert(invalidStart >= 0); 1737 assert(invalidStart < text.length); 1738 1739 if(invalidEnd > text.length) 1740 invalidEnd = cast(int) text.length; 1741 1742 int firstInvalidSegment = 0; 1743 1744 Point currentCorner = Point(0, 0); 1745 int displayLineNumber = 0; 1746 int lineSegmentIndexStart = 0; 1747 1748 if(invalidStart != 0) { 1749 // while i could binary search for the invalid thing, 1750 // i also need to rebuild _width and _height anyway so 1751 // just gonna loop through and hope for the best. 1752 bool found = false; 1753 1754 // I can't just use the segment bounding box per se since that isn't the whole line 1755 // and the finishLine adjustment for mixed fonts/sizes will throw things off. so we 1756 // want to start at the very corner of the line 1757 int lastLineY; 1758 int thisLineY; 1759 foreach(idx, segment; segments) { 1760 // FIXME: i might actually need to go back to the logical line 1761 if(displayLineNumber != segment.displayLineNumber) { 1762 lastLineY = thisLineY; 1763 displayLineNumber = segment.displayLineNumber; 1764 lineSegmentIndexStart = cast(int) idx; 1765 } 1766 auto b = segment.boundingBox.bottom; 1767 if(b > thisLineY) 1768 thisLineY = b; 1769 1770 if(invalidStart >= segment.textBeginOffset && invalidStart < segment.textEndOffset) { 1771 // we'll redo the whole line with the invalidated region since it might have other coordinate things 1772 1773 segment = segments[lineSegmentIndexStart]; 1774 1775 firstInvalidSegment = lineSegmentIndexStart;// cast(int) idx; 1776 invalidStart = segment.textBeginOffset; 1777 displayLineNumber = segment.displayLineNumber; 1778 currentCorner = segment.upperLeft; 1779 currentCorner.y = lastLineY; 1780 1781 found = true; 1782 break; 1783 } 1784 1785 // FIXME: since we rewind to the line segment start above this might not be correct anymore. 1786 auto bb = segment.boundingBox; 1787 if(bb.right > _width) 1788 _width = bb.right; 1789 if(bb.bottom > _height) 1790 _height = bb.bottom; 1791 } 1792 assert(found); 1793 } 1794 1795 // writeln(invalidStart, " starts segment ", firstInvalidSegment, " and line ", displayLineNumber, " seg ", lineSegmentIndexStart); 1796 1797 segments = segments[0 .. firstInvalidSegment]; 1798 segments.assumeSafeAppend(); 1799 1800 segmentsWidths = segmentsWidths[0 .. firstInvalidSegment]; 1801 segmentsWidths.assumeSafeAppend(); 1802 1803 version(try_kerning_hack) { 1804 size_t previousIndex = 0; 1805 int lastWidth; 1806 int lastWidthDistance; 1807 } 1808 1809 Segment segment; 1810 1811 Segment previousOldSavedSegment; 1812 short[] previousOldSavedWidths; 1813 TextStyle currentStyle = null; 1814 int currentStyleIndex = 0; 1815 MeasurableFont font; 1816 ubyte[128] glyphWidths; 1817 void loadNewFont(MeasurableFont what) { 1818 font = what; 1819 1820 // caching the ascii widths locally can give a boost to ~ 20% of the speed of this function 1821 foreach(char c; 32 .. 128) { 1822 auto w = font.stringWidth((&c)[0 .. 1]); 1823 glyphWidths[c] = cast(ubyte) w; // FIXME: what if it doesn't fit? 1824 } 1825 } 1826 1827 auto styles = this.styles; 1828 1829 foreach(style; this.styles) { 1830 if(invalidStart >= style.offset && invalidStart < (style.offset + style.length)) { 1831 currentStyle = stylePalette[style.styleInformationIndex]; 1832 if(currentStyle !is null) 1833 loadNewFont(currentStyle.font); 1834 currentStyleIndex = style.styleInformationIndex; 1835 1836 styles = styles[1 .. $]; 1837 break; 1838 } else if(style.offset > invalidStart) { 1839 break; 1840 } 1841 styles = styles[1 .. $]; 1842 } 1843 1844 int offsetToNextStyle = int.max; 1845 if(styles.length) { 1846 offsetToNextStyle = styles[0].offset; 1847 } 1848 1849 1850 assert(offsetToNextStyle >= 0); 1851 1852 short[] widths; 1853 1854 size_t segmentBegan = invalidStart; 1855 void finishSegment(size_t idx) { 1856 if(idx == segmentBegan) 1857 return; 1858 segmentBegan = idx; 1859 segment.textEndOffset = cast(int) idx; 1860 segment.displayLineNumber = displayLineNumber; 1861 1862 if(segments.length < originalSegments.length) { 1863 previousOldSavedSegment = originalSegments[segments.length]; 1864 previousOldSavedWidths = originalSegmentsWidths[segmentsWidths.length]; 1865 } else { 1866 previousOldSavedSegment = Segment.init; 1867 previousOldSavedWidths = null; 1868 } 1869 1870 segments ~= segment; 1871 segmentsWidths ~= widths; 1872 1873 segment = Segment.init; 1874 segment.upperLeft = currentCorner; 1875 segment.styleInformationIndex = currentStyleIndex; 1876 segment.textBeginOffset = cast(int) idx; 1877 widths = null; 1878 } 1879 1880 // FIXME: when we start in an invalidated thing this is not necessarily right, it should be calculated above 1881 int biggestDescent = font.descent; 1882 int lineHeight = font.height; 1883 1884 bool finishLine(size_t idx, MeasurableFont outerFont) { 1885 if(segment.textBeginOffset == idx) 1886 return false; // no need to keep nothing. 1887 1888 if(currentCorner.x > this._width) 1889 this._width = currentCorner.x; 1890 1891 auto thisLineY = currentCorner.y; 1892 1893 auto thisLineHeight = lineHeight; 1894 currentCorner.y += lineHeight; 1895 currentCorner.x = 0; 1896 1897 finishSegment(idx); // i use currentCorner in there! so this must be after that 1898 displayLineNumber++; 1899 1900 lineHeight = outerFont.height; 1901 biggestDescent = outerFont.descent; 1902 1903 // go back and adjust all the segments on this line to have the right height and do vertical alignment with the baseline 1904 foreach(ref seg; segments[lineSegmentIndexStart .. $]) { 1905 MeasurableFont font; 1906 if(seg.styleInformationIndex < stylePalette.length) { 1907 auto si = stylePalette[seg.styleInformationIndex]; 1908 if(si) 1909 font = si.font; 1910 } 1911 1912 auto baseline = thisLineHeight - biggestDescent; 1913 1914 seg.upperLeft.y += baseline - font.ascent; 1915 seg.height = thisLineHeight - (baseline - font.ascent); 1916 } 1917 1918 // now need to check if we can finish relayout early 1919 1920 // if we're beyond the invalidated section and have original data to compare against... 1921 previousOldSavedSegment.textBeginOffset += invalidatedChangeInTextLength; 1922 previousOldSavedSegment.textEndOffset += invalidatedChangeInTextLength; 1923 1924 /+ 1925 // FIXME: would be nice to make this work somehow - when you input a new line it needs to just adjust the y stuff 1926 // part of the problem is that it needs to inject a new segment for the newline and then the whole old array is 1927 // broken. 1928 int deltaY; 1929 int deltaLineNumber; 1930 1931 if(idx >= invalidEnd && segments[$-1] != previousOldSavedSegment) { 1932 deltaY = thisLineHeight; 1933 deltaLineNumber = 1; 1934 previousOldSavedSegment.upperLeft.y += deltaY; 1935 previousOldSavedSegment.displayLineNumber += deltaLineNumber; 1936 writeln("trying deltaY = ", deltaY); 1937 writeln(previousOldSavedSegment); 1938 writeln(segments[$-1]); 1939 } 1940 +/ 1941 1942 // FIXME: if the only thing that's changed is a y coordinate, adjust that too 1943 // finishEarly(); 1944 if(idx >= invalidEnd && segments[$-1] == previousOldSavedSegment) { 1945 if(segmentsWidths[$-1] == previousOldSavedWidths) { 1946 // we've hit a point where nothing has changed, it is time to stop processing 1947 1948 foreach(ref seg; originalSegments[segments.length .. $]) { 1949 seg.textBeginOffset += invalidatedChangeInTextLength; 1950 seg.textEndOffset += invalidatedChangeInTextLength; 1951 1952 /+ 1953 seg.upperLeft.y += deltaY; 1954 seg.displayLineNumber += deltaLineNumber; 1955 +/ 1956 1957 auto bb = seg.boundingBox; 1958 if(bb.right > _width) 1959 _width = bb.right; 1960 if(bb.bottom > _height) 1961 _height = bb.bottom; 1962 } 1963 1964 // these refer to the same array or should anyway so hopefully this doesn't do anything. 1965 // FIXME: confirm this isn't sucky 1966 segments ~= originalSegments[segments.length .. $]; 1967 segmentsWidths ~= originalSegmentsWidths[segmentsWidths.length .. $]; 1968 1969 return true; 1970 } else { 1971 // writeln("not matched"); 1972 // writeln(previousOldSavedWidths != segmentsWidths[$-1]); 1973 } 1974 } 1975 1976 lineSegmentIndexStart = cast(int) segments.length; 1977 1978 return false; 1979 } 1980 1981 void finishEarly() { 1982 // lol i did all the work before triggering this 1983 } 1984 1985 segment.upperLeft = currentCorner; 1986 segment.styleInformationIndex = currentStyleIndex; 1987 segment.textBeginOffset = invalidStart; 1988 1989 bool endSegment; 1990 bool endLine; 1991 1992 bool tryWordWrapOnNext; 1993 1994 // writeln("Prior to loop: ", MonoTime.currTime - start, " ", invalidStart); 1995 1996 // FIXME: i should prolly go by grapheme 1997 foreach(idxRaw, dchar ch; text[invalidStart .. $]) { 1998 auto idx = idxRaw + invalidStart; 1999 2000 version(try_kerning_hack) 2001 lastWidthDistance++; 2002 auto oldFont = font; 2003 if(offsetToNextStyle == idx) { 2004 auto oldStyle = currentStyle; 2005 if(styles.length) { 2006 StyleBlock currentStyleBlock = styles[0]; 2007 offsetToNextStyle += currentStyleBlock.length; 2008 styles = styles[1 .. $]; 2009 2010 currentStyle = stylePalette[currentStyleBlock.styleInformationIndex]; 2011 currentStyleIndex = currentStyleBlock.styleInformationIndex; 2012 } else { 2013 currentStyle = null; 2014 offsetToNextStyle = int.max; 2015 } 2016 if(oldStyle !is currentStyle) { 2017 if(!endLine) 2018 endSegment = true; 2019 2020 loadNewFont(currentStyle.font); 2021 } 2022 } 2023 2024 if(tryWordWrapOnNext) { 2025 int nextWordwrapPoint = cast(int) idx; 2026 while(nextWordwrapPoint < text.length && !isWordwrapPoint(text[nextWordwrapPoint])) { 2027 if(text[nextWordwrapPoint] == '\n') 2028 break; 2029 nextWordwrapPoint++; 2030 } 2031 2032 if(currentCorner.x + font.stringWidth(text[idx .. nextWordwrapPoint]) >= wordWrapWidth_) 2033 endLine = true; 2034 2035 tryWordWrapOnNext = false; 2036 } 2037 2038 if(endSegment && !endLine) { 2039 finishSegment(idx); 2040 endSegment = false; 2041 } 2042 2043 bool justChangedLine; 2044 if(endLine) { 2045 auto flr = finishLine(idx, oldFont); 2046 if(flr) 2047 return finishEarly(); 2048 endLine = false; 2049 endSegment = false; 2050 justChangedLine = true; 2051 } 2052 2053 if(font !is oldFont) { 2054 // FIXME: adjust height 2055 if(justChangedLine || font.height > lineHeight) 2056 lineHeight = font.height; 2057 if(justChangedLine || font.descent > biggestDescent) 2058 biggestDescent = font.descent; 2059 } 2060 2061 2062 2063 int thisWidth = 0; 2064 2065 switch(ch) { 2066 case 0: 2067 goto advance; 2068 case '\r': 2069 goto advance; 2070 case '\n': 2071 /+ 2072 finishSegment(idx); 2073 segment.textBeginOffset = cast(int) idx; 2074 2075 thisWidth = 0; 2076 +/ 2077 2078 endLine = true; 2079 goto advance; 2080 2081 // FIXME: a tab at the end of a line causes the next line to indent 2082 case '\t': 2083 finishSegment(idx); 2084 2085 // a tab should be its own segment with no text 2086 // per se 2087 2088 thisWidth = 48; 2089 2090 segment.width += thisWidth; 2091 currentCorner.x += thisWidth; 2092 2093 endSegment = true; 2094 goto advance; 2095 2096 //goto advance; 2097 default: 2098 // FIXME: i don't think the draw thing uses kerning but if it does this is wrong. 2099 2100 // figure out this length (it uses previous idx to get some kerning info used) 2101 version(try_kerning_hack) { 2102 if(lastWidthDistance == 1) { 2103 auto width = font.stringWidth(text[previousIndex .. idx + stride(text[idx])]); 2104 thisWidth = width - lastWidth; 2105 // writeln(text[previousIndex .. idx + stride(text[idx])], " ", width, "-", lastWidth); 2106 } else { 2107 auto width = font.stringWidth(text[idx .. idx + stride(text[idx])]); 2108 thisWidth = width; 2109 } 2110 } else { 2111 if(text[idx] < 128) 2112 thisWidth = glyphWidths[text[idx]]; 2113 else 2114 thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]); 2115 } 2116 2117 segment.width += thisWidth; 2118 currentCorner.x += thisWidth; 2119 2120 version(try_kerning_hack) { 2121 lastWidth = thisWidth; 2122 previousIndex = idx; 2123 lastWidthDistance = 0; 2124 } 2125 } 2126 2127 if(wordWrapWidth_ > 0 && isWordwrapPoint(ch)) 2128 tryWordWrapOnNext = true; 2129 2130 // if im iterating and hit something that would change the line height, will have to go back and change everything perhaps. or at least work with offsets from the baseline throughout... 2131 2132 // might also just want a special string sequence that can inject things in the middle of text like inline images. it'd have to tell the height and advance. 2133 2134 // this would be to test if the kerning adjustments do anything. seems like the fonts 2135 // don't care tbh but still. 2136 // thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]); 2137 2138 advance: 2139 if(segment.textBeginOffset != -1) { 2140 widths ~= cast(short) thisWidth; 2141 } 2142 } 2143 2144 finishLine(text.length, font); 2145 2146 _height = currentCorner.y; 2147 2148 assert(segments.length); 2149 2150 //return widths; 2151 2152 // writefln("%(%s\n%)", segments[0 .. 10]); 2153 } 2154 2155 private { 2156 int stride(char c) { 2157 if(c < 0x80) { 2158 return 1; 2159 } else if(c == 0xff) { 2160 return 1; 2161 } else { 2162 import core.bitop : bsr; 2163 return 7 - bsr((~uint(c)) & 0xFF); 2164 } 2165 } 2166 } 2167 } 2168 2169 class StyledTextLayouter(StyleClass) : TextLayouter { 2170 2171 }