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