• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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