1 2function ASSERT(pred) { 3 console.assert(pred, 'assert failed'); 4} 5 6function LOG(...args) { 7 // comment out for non-debugging 8// console.log(args); 9} 10 11function MakeCursor(CanvasKit) { 12 const linePaint = new CanvasKit.Paint(); 13 linePaint.setColor([0,0,1,1]); 14 linePaint.setStyle(CanvasKit.PaintStyle.Stroke); 15 linePaint.setStrokeWidth(2); 16 linePaint.setAntiAlias(true); 17 18 const pathPaint = new CanvasKit.Paint(); 19 pathPaint.setColor([0,0,1,0.25]); 20 linePaint.setAntiAlias(true); 21 22 return { 23 _line_paint: linePaint, // wrap in weak-ref so we can delete it? 24 _path_paint: pathPaint, 25 _x: 0, 26 _top: 0, 27 _bottom: 0, 28 _path: null, // only use x,top,bottom if path is null 29 _draws_per_sec: 2, 30 31 // pass 0 for no-draw, pass inf. for always on 32 setBlinkRate: function(blinks_per_sec) { 33 this._draws_per_sec = blinks_per_sec; 34 }, 35 place: function(x, top, bottom) { 36 this._x = x; 37 this._top = top; 38 this._bottom = bottom; 39 40 this._path = null; 41 }, 42 setPath: function(path) { 43 this._path = path; 44 }, 45 draw_before: function(canvas) { 46 if (this._path) { 47 canvas.drawPath(this._path, this._path_paint); 48 } 49 }, 50 draw_after: function(canvas) { 51 if (this._path) { 52 return; 53 } 54 if (Math.floor(Date.now() * this._draws_per_sec / 1000) & 1) { 55 canvas.drawLine(this._x, this._top, this._x, this._bottom, this._line_paint); 56 } 57 }, 58 }; 59} 60 61function MakeMouse() { 62 return { 63 _start_x: 0, _start_y: 0, 64 _curr_x: 0, _curr_y: 0, 65 _active: false, 66 67 isActive: function() { 68 return this._active; 69 }, 70 setDown: function(x, y) { 71 this._start_x = this._curr_x = x; 72 this._start_y = this._curr_y = y; 73 this._active = true; 74 }, 75 setMove: function(x, y) { 76 this._curr_x = x; 77 this._curr_y = y; 78 }, 79 setUp: function(x, y) { 80 this._curr_x = x; 81 this._curr_y = y; 82 this._active = false; 83 }, 84 getPos: function(dx, dy) { 85 return [ this._start_x + dx, this._start_y + dy, this._curr_x + dx, this._curr_y + dy ]; 86 }, 87 }; 88} 89 90function runs_x_to_index(runs, x) { 91 for (const r of runs) { 92 for (let i = 1; i < r.offsets.length; i += 1) { 93 if (x < r.positions[i*2]) { 94 const mid = (r.positions[i*2-2] + r.positions[i*2]) * 0.5; 95 if (x <= mid) { 96 return r.offsets[i-1]; 97 } else { 98 return r.offsets[i]; 99 } 100 } 101 } 102 } 103 const r = runs[runs.length-1]; 104 return r.offsets[r.offsets.length-1]; 105} 106 107function lines_pos_to_index(lines, x, y) { 108 if (y < lines[0].top) { 109 return 0; 110 } 111 for (const l of lines) { 112 if (y <= l.bottom) { 113 return runs_x_to_index(l.runs, x); 114 } 115 } 116 return lines[lines.length - 1].textRange.last; 117} 118 119function runs_index_to_run(runs, index) { 120 for (const r of runs) { 121 if (index <= r.offsets[r.offsets.length-1]) { 122 return r; 123 } 124 } 125 return runs[runs.length-1]; // last run 126} 127 128function runs_index_to_x(runs, index) { 129 const r = runs_index_to_run(runs, index); 130 for (const i in r.offsets) { 131 if (index == r.offsets[i]) { 132 return r.positions[i*2]; 133 } 134 } 135 return r.positions[r.positions.length-2]; // last x 136} 137 138function lines_index_to_line_index(lines, index) { 139 let i = 0; 140 for (const l of lines) { 141 if (index <= l.textRange.last) { 142 return i; 143 } 144 i += 1; 145 } 146 return lines.length-1; 147} 148 149function lines_index_to_line(lines, index) { 150 return lines[lines_index_to_line_index(lines, index)]; 151} 152 153function lines_index_to_x(lines, index) { 154 for (const l of lines) { 155 if (index <= l.textRange.last) { 156 return runs_index_to_x(l.runs, index); 157 } 158 } 159} 160 161function lines_indices_to_path(lines, a, b, width) { 162 if (a == b) { 163 return null; 164 } 165 if (a > b) { [a, b] = [b, a]; } 166 167 const path = new CanvasKit.Path(); 168 const la = lines_index_to_line(lines, a); 169 const lb = lines_index_to_line(lines, b); 170 const ax = runs_index_to_x(la.runs, a); 171 const bx = runs_index_to_x(lb.runs, b); 172 if (la == lb) { 173 path.addRect([ax, la.top, bx, la.bottom]); 174 } else { 175 path.addRect([ax, la.top, width, la.bottom]); 176 path.addRect([0, lb.top, bx, lb.bottom]); 177 if (la.bottom < lb.top) { 178 path.addRect([0, la.bottom, width, lb.top]); // extra lines inbetween 179 } 180 } 181 return path; 182} 183 184function string_del(str, start, end) { 185 return str.slice(0, start) + str.slice(end, str.length); 186} 187 188function make_default_paint() { 189 const p = new CanvasKit.Paint(); 190 p.setAntiAlias(true); 191 return p; 192} 193 194function make_default_font(tf) { 195 const font = new CanvasKit.Font(tf); 196 font.setSubpixel(true); 197 return font; 198} 199 200function MakeStyle(length) { 201 return { 202 _length: length, 203 typeface: null, 204 size: null, 205 color: null, 206 bold: null, 207 italic: null, 208 underline: null, 209 210 _check_toggle: function(src, dst) { 211 if (src == 'toggle') { 212 return !dst; 213 } else { 214 return src; 215 } 216 }, 217 218 // returns true if we changed something affecting layout 219 mergeFrom: function(src) { 220 let layoutChanged = false; 221 222 if (src.typeface && this.typeface !== src.typeface) { 223 this.typeface = src.typeface; 224 layoutChanged = true; 225 } 226 if (src.size && this.size !== src.size) { 227 this.size = src.size; 228 layoutChanged = true; 229 } 230 if (src.color) { this.color = src.color; } 231 232 if (src.bold) { 233 this.bold = this._check_toggle(src.bold, this.bold); 234 } 235 if (src.italic) { 236 this.italic = this._check_toggle(src.italic, this.italic); 237 } 238 if (src.underline) { 239 this.underline = this._check_toggle(src.underline, this.underline); 240 } 241 242 if (src.size_add) { 243 this.size += src.size_add; 244 layoutChanged = true; 245 } 246 247 return layoutChanged; 248 } 249 }; 250} 251 252function MakeEditor(text, style, cursor, width) { 253 const ed = { 254 _text: text, 255 _lines: null, 256 _cursor: cursor, 257 _width: width, 258 _index: { start: 0, end: 0 }, 259 _styles: null, 260 // drawing 261 _X: 0, 262 _Y: 0, 263 _paint: make_default_paint(), 264 _font: make_default_font(style.typeface), 265 266 getLines: function() { return this._lines; }, 267 268 width: function() { 269 return this._width; 270 }, 271 height: function() { 272 return this._lines[this._lines.length-1].bottom; 273 }, 274 bounds: function() { 275 return [this._X, this._Y, this._X + this.width(), this._Y + this.height()]; 276 }, 277 setXY: function(x, y) { 278 this._X = x; 279 this._Y = y; 280 }, 281 282 _rebuild_selection: function() { 283 const a = this._index.start; 284 const b = this._index.end; 285 ASSERT(a >= 0 && a <= b && b <= this._text.length); 286 if (a === b) { 287 const l = lines_index_to_line(this._lines, a); 288 const x = runs_index_to_x(l.runs, a); 289 this._cursor.place(x, l.top, l.bottom); 290 } else { 291 this._cursor.setPath(lines_indices_to_path(this._lines, a, b, this._width)); 292 } 293 }, 294 setIndex: function(i) { 295 this._index.start = this._index.end = i; 296 this._rebuild_selection(); 297 }, 298 setIndices: function(a, b) { 299 if (a > b) { [a, b] = [b, a]; } 300 this._index.start = a; 301 this._index.end = b; 302 this._rebuild_selection(); 303 }, 304 moveDX: function(dx) { 305 let index; 306 if (this._index.start == this._index.end) { 307 // just adjust and pin 308 index = Math.max(Math.min(this._index.start + dx, this._text.length), 0); 309 } else { 310 // 'deselect' the region, and turn it into just a single index 311 index = dx < 0 ? this._index.start : this._index.end; 312 } 313 this.setIndex(index); 314 }, 315 moveDY: function(dy) { 316 let index = (dy < 0) ? this._index.start : this._index.end; 317 const i = lines_index_to_line_index(this._lines, index); 318 if (dy < 0 && i == 0) { 319 index = 0; 320 } else if (dy > 0 && i == this._lines.length - 1) { 321 index = this._text.length; 322 } else { 323 const x = runs_index_to_x(this._lines[i].runs, index); 324 // todo: statefully track "original" x when an up/down sequence started, 325 // so we can avoid drift. 326 index = runs_x_to_index(this._lines[i+dy].runs, x); 327 } 328 this.setIndex(index); 329 }, 330 331 _validateStyles: function() { 332 let len = 0; 333 for (const s of this._styles) { 334 len += s._length; 335 } 336 ASSERT(len === this._text.length); 337 }, 338 _validateBlocks: function(blocks) { 339 let len = 0; 340 for (const b of blocks) { 341 len += b.length; 342 } 343 ASSERT(len === this._text.length); 344 }, 345 346 _buildLines: function() { 347 this._validateStyles(); 348 349 const build_sparse = true; 350 const blocks = []; 351 let block = null; 352 for (const s of this._styles) { 353 if (build_sparse) { 354 if (!block || (block.typeface === s.typeface && block.size === s.size)) { 355 if (!block) { 356 block = { length: 0, typeface: s.typeface, size: s.size }; 357 } 358 block.length += s._length; 359 } else { 360 blocks.push(block); 361 block = { length: s._length, typeface: s.typeface, size: s.size }; 362 } 363 } else { 364 // force a block on every style boundary for now 365 blocks.push({ length: s._length, typeface: s.typeface, size: s.size }); 366 } 367 } 368 if (build_sparse) { 369 blocks.push(block); 370 } 371 this._validateBlocks(blocks); 372 373 this._lines = CanvasKit.ParagraphBuilder.ShapeText(this._text, blocks, this._width); 374 this._rebuild_selection(); 375 376 // add textRange to each run, to aid in drawing 377 this._runs = []; 378 for (const l of this._lines) { 379 for (const r of l.runs) { 380 r.textRange = { start: r.offsets[0], end: r.offsets[r.offsets.length-1] }; 381 this._runs.push(r); 382 } 383 } 384 }, 385 386 // note: this does not rebuild lines/runs, or update the cursor, 387 // but it does edit the text and styles 388 // returns true if it deleted anything 389 _deleteRange: function(start, end) { 390 ASSERT(start >= 0 && end <= this._text.length); 391 ASSERT(start <= end); 392 if (start === end) { 393 return false; 394 } 395 396 this._delete_style_range(start, end); 397 // Do this after shrink styles (we use text.length in an assert) 398 this._text = string_del(this._text, start, end); 399 }, 400 deleteSelection: function() { 401 let start = this._index.start; 402 if (start == this._index.end) { 403 if (start == 0) { 404 return; // nothing to do 405 } 406 this._deleteRange(start - 1, start); 407 start -= 1; 408 } else { 409 this._deleteRange(start, this._index.end); 410 } 411 this._index.start = this._index.end = start; 412 this._buildLines(); 413 }, 414 insert: function(charcode) { 415 if (this._index.start != this._index.end) { 416 this.deleteSelection(); 417 } 418 const index = this._index.start; 419 420 // do this before edit the text (we use text.length in an assert) 421 const [i, prev_len] = this.find_style_index_and_prev_length(index); 422 this._styles[i]._length += 1; 423 424 // now grow the text 425 this._text = this._text.slice(0, index) + charcode + this._text.slice(index); 426 427 this._index.start = this._index.end = index + 1; 428 this._buildLines(); 429 }, 430 431 draw: function(canvas) { 432 canvas.save(); 433 canvas.translate(this._X, this._Y); 434 435 this._cursor.draw_before(canvas); 436 437 const runs = this._runs; 438 const styles = this._styles; 439 const f = this._font; 440 const p = this._paint; 441 442 let s = styles[0]; 443 let sindex = 0; 444 let s_start = 0; 445 let s_end = s._length; 446 447 let r = runs[0]; 448 let rindex = 0; 449 450 let start = 0; 451 let end = 0; 452 while (start < this._text.length) { 453 while (r.textRange.end <= start) { 454 r = runs[++rindex]; 455 if (!r) { 456 // ran out of runs, so the remaining text must just be WS 457 break; 458 } 459 } 460 if (!r) break; 461 while (s_end <= start) { 462 s = styles[++sindex]; 463 s_start = s_end; 464 s_end += s._length; 465 } 466 end = Math.min(r.textRange.end, s_end); 467 468 LOG('New range: ', start, end, 469 'from run', r.textRange.start, r.textRange.end, 470 'style', s_start, s_end); 471 472 // check that we have anything to draw 473 if (r.textRange.start >= end) { 474 start = end; 475 continue; // could be a span of WS with no glyphs 476 } 477 478// f.setTypeface(r.typeface); // r.typeface is always null (for now) 479 f.setSize(r.size); 480 f.setEmbolden(s.bold); 481 f.setSkewX(s.italic ? -0.2 : 0); 482 p.setColor(s.color ? s.color : [0,0,0,1]); 483 484 let gly = r.glyphs; 485 let pos = r.positions; 486 if (start > r.textRange.start || end < r.textRange.end) { 487 // search for the subset of glyphs to draw 488 let glyph_start, glyph_end; 489 for (let i = 0; i < r.offsets.length; ++i) { 490 if (r.offsets[i] >= start) { 491 glyph_start = i; 492 break; 493 } 494 } 495 for (let i = glyph_start+1; i < r.offsets.length; ++i) { 496 if (r.offsets[i] >= end) { 497 glyph_end = i; 498 break; 499 } 500 } 501 LOG(' glyph subrange', glyph_start, glyph_end); 502 gly = gly.slice(glyph_start, glyph_end); 503 // +2 at the end so we can see the trailing position (esp. for underlines) 504 pos = pos.slice(glyph_start*2, glyph_end*2 + 2); 505 } else { 506 LOG(' use entire glyph run'); 507 } 508 canvas.drawGlyphs(gly, pos, 0, 0, f, p); 509 510 if (s.underline) { 511 const gap = 2; 512 const Y = pos[1]; // first Y 513 const lastX = pos[gly.length*2]; 514 const sects = f.getGlyphIntercepts(gly, pos, Y+2, Y+4); 515 516 let x = pos[0]; 517 for (let i = 0; i < sects.length; i += 2) { 518 const end = sects[i] - gap; 519 if (x < end) { 520 canvas.drawRect([x, Y+2, end, Y+4], p); 521 } 522 x = sects[i+1] + gap; 523 } 524 if (x < lastX) { 525 canvas.drawRect([x, Y+2, lastX, Y+4], p); 526 } 527 } 528 529 start = end; 530 } 531 532 this._cursor.draw_after(canvas); 533 canvas.restore(); 534 }, 535 536 // Styling 537 538 // returns [index, prev total length before this style] 539 find_style_index_and_prev_length: function(index) { 540 let len = 0; 541 for (let i = 0; i < this._styles.length; ++i) { 542 const l = this._styles[i]._length; 543 len += l; 544 // < favors the latter style if index is between two styles 545 if (index < len) { 546 return [i, len - l]; 547 } 548 } 549 ASSERT(len === this._text.length); 550 return [this._styles.length-1, len]; 551 }, 552 _delete_style_range: function(start, end) { 553 // shrink/remove styles 554 // 555 // [.....][....][....][.....] styles 556 // [..................] start...end 557 // 558 // - trim the first style 559 // - remove the middle styles 560 // - trim the last style 561 562 let N = end - start; 563 let [i, prev_len] = this.find_style_index_and_prev_length(start); 564 let s = this._styles[i]; 565 if (start > prev_len) { 566 // we overlap the first style (but not entirely 567 const skip = start - prev_len; 568 ASSERT(skip < s._length); 569 const shrink = Math.min(N, s._length - skip); 570 ASSERT(shrink > 0); 571 s._length -= shrink; 572 N -= shrink; 573 if (N === 0) { 574 return; 575 } 576 i += 1; 577 ASSERT(i < this._styles.length); 578 } 579 while (N > 0) { 580 s = this._styles[i]; 581 if (N >= s._length) { 582 N -= s._length; 583 this._styles.splice(i, 1); 584 } else { 585 s._length -= N; 586 break; 587 } 588 } 589 }, 590 591 applyStyleToRange: function(style, start, end) { 592 if (start > end) { [start, end] = [end, start]; } 593 ASSERT(start >= 0 && end <= this._text.length); 594 if (start === end) { 595 return; 596 } 597 598 LOG('trying to apply', style, start, end); 599 let i; 600 for (i = 0; i < this._styles.length; ++i) { 601 if (start <= this._styles[i]._length) { 602 break; 603 } 604 start -= this._styles[i]._length; 605 end -= this._styles[i]._length; 606 } 607 608 let s = this._styles[i]; 609 // do we need to fission off a clean subset for the head of s? 610 if (start > 0) { 611 const ns = Object.assign({}, s); 612 s._length = start; 613 ns._length -= start; 614 LOG('initial splice', i, start, s._length, ns._length); 615 i += 1; 616 this._styles.splice(i, 0, ns); 617 end -= start; 618 // we don't use start any more 619 } 620 // merge into any/all whole styles we overlap 621 let layoutChanged = false; 622 while (end >= this._styles[i]._length) { 623 LOG('whole run merging for style index', i) 624 layoutChanged |= this._styles[i].mergeFrom(style); 625 end -= this._styles[i]._length; 626 i += 1; 627 if (end == 0) { 628 break; 629 } 630 } 631 // do we partially cover the last run 632 if (end > 0) { 633 s = this._styles[i]; 634 const ns = Object.assign({}, s); // the new first half 635 ns._length = end; 636 s._length -= end; // trim the (unchanged) tail 637 LOG('merging tail', i, ns._length, s._length); 638 layoutChanged |= ns.mergeFrom(style); 639 this._styles.splice(i, 0, ns); 640 } 641 642 this._validateStyles(); 643 LOG('after applying styles', this._styles); 644 645 if (layoutChanged) { 646 this._buildLines(); 647 } 648 }, 649 applyStyleToSelection: function(style) { 650 this.applyStyleToRange(style, this._index.start, this._index.end); 651 }, 652 }; 653 654 const s = MakeStyle(ed._text.length); 655 s.mergeFrom(style); 656 ed._styles = [ s ]; 657 ed._buildLines(); 658 return ed; 659} 660