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