Move paragraph extra into demos
Change-Id: I5006e412084ad8e82eae5671577228f9241dbc1d Reviewed-on: https://skia-review.googlesource.com/c/skia/+/406176 Reviewed-by: Kevin Lubick <kjlubick@google.com> Commit-Queue: Mike Reed <reed@google.com>
This commit is contained in:
parent
be0eedcf6d
commit
a5b17fd8d0
141
demos.skia.org/demos/textedit/index.html
Normal file
141
demos.skia.org/demos/textedit/index.html
Normal file
@ -0,0 +1,141 @@
|
||||
<!DOCTYPE html>
|
||||
<title>TextEdit demo in CanvasKit</title>
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script type="text/javascript" src="https://particles.skia.org/dist/canvaskit.js"></script>
|
||||
<script type="text/javascript" src="textapi_utils.js"></script>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
border: 1px dashed grey;
|
||||
}
|
||||
</style>
|
||||
|
||||
<body>
|
||||
<h1>TextEdit in CanvasKit</h1>
|
||||
|
||||
<canvas id=para2 width=600 height=600 tabindex='-1'></canvas>
|
||||
</body>
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
let CanvasKit;
|
||||
onload = async () => {
|
||||
CanvasKit = await CanvasKitInit({ locateFile: (file) => 'https://particles.skia.org/dist/'+file });
|
||||
ParagraphAPI2();
|
||||
};
|
||||
|
||||
function ParagraphAPI2() {
|
||||
const surface = CanvasKit.MakeCanvasSurface('para2');
|
||||
if (!surface) {
|
||||
console.error('Could not make surface');
|
||||
return;
|
||||
}
|
||||
|
||||
const mouse = MakeMouse();
|
||||
const cursor = MakeCursor(CanvasKit);
|
||||
const canvas = surface.getCanvas();
|
||||
|
||||
const text0 = "In a hole in the ground there lived a hobbit. Not a nasty, dirty, " +
|
||||
"wet hole full of worms and oozy smells. This was a hobbit-hole and " +
|
||||
"that means good food, a warm hearth, and all the comforts of home.";
|
||||
const LOC_X = 20,
|
||||
LOC_Y = 20;
|
||||
|
||||
const bgPaint = new CanvasKit.Paint();
|
||||
bgPaint.setColor([0.965, 0.965, 0.965, 1]);
|
||||
|
||||
const editor = MakeEditor(text0, {typeface:null, size:24}, cursor, 400);
|
||||
|
||||
editor.applyStyleToRange({size:100}, 0, 1);
|
||||
editor.applyStyleToRange({italic:true}, 38, 38+6);
|
||||
editor.applyStyleToRange({color:[1,0,0,1]}, 5, 5+4);
|
||||
|
||||
editor.setXY(LOC_X, LOC_Y);
|
||||
|
||||
function drawFrame(canvas) {
|
||||
const lines = editor.getLines();
|
||||
|
||||
canvas.clear(CanvasKit.WHITE);
|
||||
|
||||
if (mouse.isActive()) {
|
||||
const pos = mouse.getPos(-LOC_X, -LOC_Y);
|
||||
const a = lines_pos_to_index(lines, pos[0], pos[1]);
|
||||
const b = lines_pos_to_index(lines, pos[2], pos[3]);
|
||||
if (a === b) {
|
||||
editor.setIndex(a);
|
||||
} else {
|
||||
editor.setIndices(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
canvas.drawRect(editor.bounds(), bgPaint);
|
||||
editor.draw(canvas);
|
||||
|
||||
surface.requestAnimationFrame(drawFrame);
|
||||
}
|
||||
surface.requestAnimationFrame(drawFrame);
|
||||
|
||||
function interact(e) {
|
||||
const type = e.type;
|
||||
if (type === 'pointerup') {
|
||||
mouse.setUp(e.offsetX, e.offsetY);
|
||||
} else if (type === 'pointermove') {
|
||||
mouse.setMove(e.offsetX, e.offsetY);
|
||||
} else if (type === 'pointerdown') {
|
||||
mouse.setDown(e.offsetX, e.offsetY);
|
||||
}
|
||||
};
|
||||
|
||||
function keyhandler(e) {
|
||||
switch (e.key) {
|
||||
case 'ArrowLeft': editor.moveDX(-1); return;
|
||||
case 'ArrowRight': editor.moveDX(1); return;
|
||||
case 'ArrowUp':
|
||||
e.preventDefault();
|
||||
editor.moveDY(-1);
|
||||
return;
|
||||
case 'ArrowDown':
|
||||
e.preventDefault();
|
||||
editor.moveDY(1);
|
||||
return;
|
||||
case 'Backspace':
|
||||
editor.deleteSelection(-1);
|
||||
return;
|
||||
case 'Delete':
|
||||
editor.deleteSelection(1);
|
||||
return;
|
||||
case 'Shift':
|
||||
return;
|
||||
case 'Tab': // todo: figure out how to handle...
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
if (e.ctrlKey) {
|
||||
switch (e.key) {
|
||||
case 'r': editor.applyStyleToSelection({color:[1,0,0,1]}); return;
|
||||
case 'g': editor.applyStyleToSelection({color:[0,0.6,0,1]}); return;
|
||||
case 'u': editor.applyStyleToSelection({color:[0,0,1,1]}); return;
|
||||
case 'k': editor.applyStyleToSelection({color:[0,0,0,1]}); return;
|
||||
|
||||
case 'i': editor.applyStyleToSelection({italic:'toggle'}); return;
|
||||
case 'b': editor.applyStyleToSelection({bold:'toggle'}); return;
|
||||
|
||||
case ']': editor.applyStyleToSelection({size_add:1}); return;
|
||||
case '[': editor.applyStyleToSelection({size_add:-1}); return;
|
||||
}
|
||||
}
|
||||
if (!e.ctrlKey && !e.metaKey) {
|
||||
e.preventDefault(); // at least needed for 'space'
|
||||
editor.insert(e.key);
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById('para2').addEventListener('pointermove', interact);
|
||||
document.getElementById('para2').addEventListener('pointerdown', interact);
|
||||
document.getElementById('para2').addEventListener('pointerup', interact);
|
||||
document.getElementById('para2').addEventListener('keydown', keyhandler);
|
||||
return surface;
|
||||
}
|
||||
|
||||
</script>
|
608
demos.skia.org/demos/textedit/textapi_utils.js
Normal file
608
demos.skia.org/demos/textedit/textapi_utils.js
Normal file
@ -0,0 +1,608 @@
|
||||
|
||||
function ASSERT(pred) {
|
||||
console.assert(pred, 'assert failed');
|
||||
}
|
||||
|
||||
function LOG(...args) {
|
||||
// comment out for non-debugging
|
||||
// console.log(args);
|
||||
}
|
||||
|
||||
function MakeCursor(CanvasKit) {
|
||||
const linePaint = new CanvasKit.Paint();
|
||||
linePaint.setColor([0,0,1,1]);
|
||||
linePaint.setStyle(CanvasKit.PaintStyle.Stroke);
|
||||
linePaint.setStrokeWidth(2);
|
||||
linePaint.setAntiAlias(true);
|
||||
|
||||
const pathPaint = new CanvasKit.Paint();
|
||||
pathPaint.setColor([0,0,1,0.25]);
|
||||
linePaint.setAntiAlias(true);
|
||||
|
||||
return {
|
||||
_line_paint: linePaint, // wrap in weak-ref so we can delete it?
|
||||
_path_paint: pathPaint,
|
||||
_x: 0,
|
||||
_top: 0,
|
||||
_bottom: 0,
|
||||
_path: null, // only use x,top,bottom if path is null
|
||||
_draws_per_sec: 2,
|
||||
|
||||
// pass 0 for no-draw, pass inf. for always on
|
||||
setBlinkRate: function(blinks_per_sec) {
|
||||
this._draws_per_sec = blinks_per_sec;
|
||||
},
|
||||
place: function(x, top, bottom) {
|
||||
this._x = x;
|
||||
this._top = top;
|
||||
this._bottom = bottom;
|
||||
|
||||
this.setPath(null);
|
||||
},
|
||||
setPath: function(path) {
|
||||
if (this._path) {
|
||||
this._path.delete();
|
||||
}
|
||||
this._path = path;
|
||||
},
|
||||
draw_before: function(canvas) {
|
||||
if (this._path) {
|
||||
canvas.drawPath(this._path, this._path_paint);
|
||||
}
|
||||
},
|
||||
draw_after: function(canvas) {
|
||||
if (this._path) {
|
||||
return;
|
||||
}
|
||||
if (Math.floor(Date.now() * this._draws_per_sec / 1000) & 1) {
|
||||
canvas.drawLine(this._x, this._top, this._x, this._bottom, this._line_paint);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function MakeMouse() {
|
||||
return {
|
||||
_start_x: 0, _start_y: 0,
|
||||
_curr_x: 0, _curr_y: 0,
|
||||
_active: false,
|
||||
|
||||
isActive: function() {
|
||||
return this._active;
|
||||
},
|
||||
setDown: function(x, y) {
|
||||
this._start_x = this._curr_x = x;
|
||||
this._start_y = this._curr_y = y;
|
||||
this._active = true;
|
||||
},
|
||||
setMove: function(x, y) {
|
||||
this._curr_x = x;
|
||||
this._curr_y = y;
|
||||
},
|
||||
setUp: function(x, y) {
|
||||
this._curr_x = x;
|
||||
this._curr_y = y;
|
||||
this._active = false;
|
||||
},
|
||||
getPos: function(dx, dy) {
|
||||
return [ this._start_x + dx, this._start_y + dy, this._curr_x + dx, this._curr_y + dy ];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function runs_x_to_index(runs, x) {
|
||||
for (const r of runs) {
|
||||
for (let i = 1; i < r.offsets.length; i += 1) {
|
||||
if (x < r.positions[i*2]) {
|
||||
const mid = (r.positions[i*2-2] + r.positions[i*2]) * 0.5;
|
||||
if (x <= mid) {
|
||||
return r.offsets[i-1];
|
||||
} else {
|
||||
return r.offsets[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const r = runs[runs.length-1];
|
||||
return r.offsets[r.offsets.length-1];
|
||||
}
|
||||
|
||||
function lines_pos_to_index(lines, x, y) {
|
||||
if (y < lines[0].top) {
|
||||
return 0;
|
||||
}
|
||||
const l = lines.find((l) => y <= l.bottom);
|
||||
return l ? runs_x_to_index(l.runs, x)
|
||||
: lines[lines.length - 1].textRange.last;
|
||||
}
|
||||
|
||||
function runs_index_to_run(runs, index) {
|
||||
const r = runs.find((r) => index <= r.offsets[r.offsets.length-1]);
|
||||
// return last if no run is found
|
||||
return r ? r : runs[runs.length-1];
|
||||
}
|
||||
|
||||
function runs_index_to_x(runs, index) {
|
||||
const r = runs_index_to_run(runs, index);
|
||||
const i = r.offsets.findIndex((offset) => index === offset);
|
||||
return i >= 0 ? r.positions[i*2]
|
||||
: r.positions[r.positions.length-2]; // last x
|
||||
}
|
||||
|
||||
function lines_index_to_line_index(lines, index) {
|
||||
const l = lines.findIndex((l) => index <= l.textRange.last);
|
||||
return l >= 0 ? l : lines.length-1;
|
||||
}
|
||||
|
||||
function lines_index_to_line(lines, index) {
|
||||
return lines[lines_index_to_line_index(lines, index)];
|
||||
}
|
||||
|
||||
function lines_indices_to_path(lines, a, b, width) {
|
||||
if (a == b) {
|
||||
return null;
|
||||
}
|
||||
if (a > b) { [a, b] = [b, a]; }
|
||||
|
||||
const path = new CanvasKit.Path();
|
||||
const la = lines_index_to_line(lines, a);
|
||||
const lb = lines_index_to_line(lines, b);
|
||||
const ax = runs_index_to_x(la.runs, a);
|
||||
const bx = runs_index_to_x(lb.runs, b);
|
||||
if (la == lb) {
|
||||
path.addRect([ax, la.top, bx, la.bottom]);
|
||||
} else {
|
||||
path.addRect([ax, la.top, width, la.bottom]);
|
||||
path.addRect([0, lb.top, bx, lb.bottom]);
|
||||
if (la.bottom < lb.top) {
|
||||
path.addRect([0, la.bottom, width, lb.top]); // extra lines inbetween
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
function string_del(str, start, end) {
|
||||
return str.slice(0, start) + str.slice(end, str.length);
|
||||
}
|
||||
|
||||
function make_default_paint() {
|
||||
const p = new CanvasKit.Paint();
|
||||
p.setAntiAlias(true);
|
||||
return p;
|
||||
}
|
||||
|
||||
function make_default_font(tf) {
|
||||
const font = new CanvasKit.Font(tf);
|
||||
font.setSubpixel(true);
|
||||
return font;
|
||||
}
|
||||
|
||||
function MakeStyle(length) {
|
||||
return {
|
||||
_length: length,
|
||||
typeface: null,
|
||||
size: null,
|
||||
color: null,
|
||||
bold: null,
|
||||
italic: null,
|
||||
|
||||
_check_toggle: function(src, dst) {
|
||||
if (src == 'toggle') {
|
||||
return !dst;
|
||||
} else {
|
||||
return src;
|
||||
}
|
||||
},
|
||||
|
||||
// returns true if we changed something affecting layout
|
||||
mergeFrom: function(src) {
|
||||
let layoutChanged = false;
|
||||
|
||||
if (src.typeface && this.typeface !== src.typeface) {
|
||||
this.typeface = src.typeface;
|
||||
layoutChanged = true;
|
||||
}
|
||||
if (src.size && this.size !== src.size) {
|
||||
this.size = src.size;
|
||||
layoutChanged = true;
|
||||
}
|
||||
if (src.color) { this.color = src.color; }
|
||||
|
||||
if (src.bold) {
|
||||
this.bold = this._check_toggle(src.bold, this.bold);
|
||||
}
|
||||
if (src.italic) {
|
||||
this.italic = this._check_toggle(src.italic, this.italic);
|
||||
}
|
||||
|
||||
if (src.size_add) {
|
||||
this.size += src.size_add;
|
||||
layoutChanged = true;
|
||||
}
|
||||
|
||||
return layoutChanged;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function MakeEditor(text, style, cursor, width) {
|
||||
const ed = {
|
||||
_text: text,
|
||||
_lines: null,
|
||||
_cursor: cursor,
|
||||
_width: width,
|
||||
_index: { start: 0, end: 0 },
|
||||
_styles: null,
|
||||
// drawing
|
||||
_X: 0,
|
||||
_Y: 0,
|
||||
_paint: make_default_paint(),
|
||||
_font: make_default_font(style.typeface),
|
||||
|
||||
getLines: function() { return this._lines; },
|
||||
|
||||
width: function() {
|
||||
return this._width;
|
||||
},
|
||||
height: function() {
|
||||
return this._lines[this._lines.length-1].bottom;
|
||||
},
|
||||
bounds: function() {
|
||||
return [this._X, this._Y, this._X + this.width(), this._Y + this.height()];
|
||||
},
|
||||
setXY: function(x, y) {
|
||||
this._X = x;
|
||||
this._Y = y;
|
||||
},
|
||||
|
||||
_rebuild_selection: function() {
|
||||
const a = this._index.start;
|
||||
const b = this._index.end;
|
||||
ASSERT(a >= 0 && a <= b && b <= this._text.length);
|
||||
if (a === b) {
|
||||
const l = lines_index_to_line(this._lines, a);
|
||||
const x = runs_index_to_x(l.runs, a);
|
||||
this._cursor.place(x, l.top, l.bottom);
|
||||
} else {
|
||||
this._cursor.setPath(lines_indices_to_path(this._lines, a, b, this._width));
|
||||
}
|
||||
},
|
||||
setIndex: function(i) {
|
||||
this._index.start = this._index.end = i;
|
||||
this._rebuild_selection();
|
||||
},
|
||||
setIndices: function(a, b) {
|
||||
if (a > b) { [a, b] = [b, a]; }
|
||||
this._index.start = a;
|
||||
this._index.end = b;
|
||||
this._rebuild_selection();
|
||||
},
|
||||
moveDX: function(dx) {
|
||||
let index;
|
||||
if (this._index.start == this._index.end) {
|
||||
// just adjust and pin
|
||||
index = Math.max(Math.min(this._index.start + dx, this._text.length), 0);
|
||||
} else {
|
||||
// 'deselect' the region, and turn it into just a single index
|
||||
index = dx < 0 ? this._index.start : this._index.end;
|
||||
}
|
||||
this.setIndex(index);
|
||||
},
|
||||
moveDY: function(dy) {
|
||||
let index = (dy < 0) ? this._index.start : this._index.end;
|
||||
const i = lines_index_to_line_index(this._lines, index);
|
||||
if (dy < 0 && i === 0) {
|
||||
index = 0;
|
||||
} else if (dy > 0 && i == this._lines.length - 1) {
|
||||
index = this._text.length;
|
||||
} else {
|
||||
const x = runs_index_to_x(this._lines[i].runs, index);
|
||||
// todo: statefully track "original" x when an up/down sequence started,
|
||||
// so we can avoid drift.
|
||||
index = runs_x_to_index(this._lines[i+dy].runs, x);
|
||||
}
|
||||
this.setIndex(index);
|
||||
},
|
||||
|
||||
_validateStyles: function() {
|
||||
const len = this._styles.reduce((sum, style) => sum + style._length, 0);
|
||||
ASSERT(len === this._text.length);
|
||||
},
|
||||
_validateBlocks: function(blocks) {
|
||||
const len = blocks.reduce((sum, block) => sum + block.length, 0);
|
||||
ASSERT(len === this._text.length);
|
||||
},
|
||||
|
||||
_buildLines: function() {
|
||||
this._validateStyles();
|
||||
|
||||
const blocks = [];
|
||||
let block = null;
|
||||
for (const s of this._styles) {
|
||||
if (!block || (block.typeface === s.typeface && block.size === s.size)) {
|
||||
if (!block) {
|
||||
block = { length: 0, typeface: s.typeface, size: s.size };
|
||||
}
|
||||
block.length += s._length;
|
||||
} else {
|
||||
blocks.push(block);
|
||||
block = { length: s._length, typeface: s.typeface, size: s.size };
|
||||
}
|
||||
}
|
||||
blocks.push(block);
|
||||
this._validateBlocks(blocks);
|
||||
|
||||
this._lines = CanvasKit.ParagraphBuilder.ShapeText(this._text, blocks, this._width);
|
||||
this._rebuild_selection();
|
||||
|
||||
// add textRange to each run, to aid in drawing
|
||||
this._runs = [];
|
||||
for (const l of this._lines) {
|
||||
for (const r of l.runs) {
|
||||
r.textRange = { start: r.offsets[0], end: r.offsets[r.offsets.length-1] };
|
||||
this._runs.push(r);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// note: this does not rebuild lines/runs, or update the cursor,
|
||||
// but it does edit the text and styles
|
||||
// returns true if it deleted anything
|
||||
_deleteRange: function(start, end) {
|
||||
ASSERT(start >= 0 && end <= this._text.length);
|
||||
ASSERT(start <= end);
|
||||
if (start === end) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._delete_style_range(start, end);
|
||||
// Do this after shrink styles (we use text.length in an assert)
|
||||
this._text = string_del(this._text, start, end);
|
||||
},
|
||||
deleteSelection: function(direction) {
|
||||
let start = this._index.start;
|
||||
if (start == this._index.end) {
|
||||
if (direction < 0) {
|
||||
if (start == 0) {
|
||||
return; // nothing to do
|
||||
}
|
||||
this._deleteRange(start - 1, start);
|
||||
start -= 1;
|
||||
} else {
|
||||
if (start >= this._text.length) {
|
||||
return; // nothing to do
|
||||
}
|
||||
this._deleteRange(start, start + 1);
|
||||
}
|
||||
} else {
|
||||
this._deleteRange(start, this._index.end);
|
||||
}
|
||||
this._index.start = this._index.end = start;
|
||||
this._buildLines();
|
||||
},
|
||||
insert: function(charcode) {
|
||||
if (this._index.start != this._index.end) {
|
||||
this.deleteSelection();
|
||||
}
|
||||
const index = this._index.start;
|
||||
|
||||
// do this before edit the text (we use text.length in an assert)
|
||||
const [i, prev_len] = this.find_style_index_and_prev_length(index);
|
||||
this._styles[i]._length += 1;
|
||||
|
||||
// now grow the text
|
||||
this._text = this._text.slice(0, index) + charcode + this._text.slice(index);
|
||||
|
||||
this._index.start = this._index.end = index + 1;
|
||||
this._buildLines();
|
||||
},
|
||||
|
||||
draw: function(canvas) {
|
||||
canvas.save();
|
||||
canvas.translate(this._X, this._Y);
|
||||
|
||||
this._cursor.draw_before(canvas);
|
||||
|
||||
const runs = this._runs;
|
||||
const styles = this._styles;
|
||||
const f = this._font;
|
||||
const p = this._paint;
|
||||
|
||||
let s = styles[0];
|
||||
let sindex = 0;
|
||||
let s_start = 0;
|
||||
let s_end = s._length;
|
||||
|
||||
let r = runs[0];
|
||||
let rindex = 0;
|
||||
|
||||
let start = 0;
|
||||
let end = 0;
|
||||
while (start < this._text.length) {
|
||||
while (r.textRange.end <= start) {
|
||||
r = runs[++rindex];
|
||||
if (!r) {
|
||||
// ran out of runs, so the remaining text must just be WS
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!r) break;
|
||||
while (s_end <= start) {
|
||||
s = styles[++sindex];
|
||||
s_start = s_end;
|
||||
s_end += s._length;
|
||||
}
|
||||
end = Math.min(r.textRange.end, s_end);
|
||||
|
||||
LOG('New range: ', start, end,
|
||||
'from run', r.textRange.start, r.textRange.end,
|
||||
'style', s_start, s_end);
|
||||
|
||||
// check that we have anything to draw
|
||||
if (r.textRange.start >= end) {
|
||||
start = end;
|
||||
continue; // could be a span of WS with no glyphs
|
||||
}
|
||||
|
||||
// f.setTypeface(r.typeface); // r.typeface is always null (for now)
|
||||
f.setSize(r.size);
|
||||
f.setEmbolden(s.bold);
|
||||
f.setSkewX(s.italic ? -0.2 : 0);
|
||||
p.setColor(s.color ? s.color : [0,0,0,1]);
|
||||
|
||||
let gly = r.glyphs;
|
||||
let pos = r.positions;
|
||||
if (start > r.textRange.start || end < r.textRange.end) {
|
||||
// search for the subset of glyphs to draw
|
||||
let glyph_start, glyph_end;
|
||||
for (let i = 0; i < r.offsets.length; ++i) {
|
||||
if (r.offsets[i] >= start) {
|
||||
glyph_start = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
for (let i = glyph_start+1; i < r.offsets.length; ++i) {
|
||||
if (r.offsets[i] >= end) {
|
||||
glyph_end = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
LOG(' glyph subrange', glyph_start, glyph_end);
|
||||
gly = gly.slice(glyph_start, glyph_end);
|
||||
pos = pos.slice(glyph_start*2, glyph_end*2);
|
||||
} else {
|
||||
LOG(' use entire glyph run');
|
||||
}
|
||||
canvas.drawGlyphs(gly, pos, 0, 0, f, p);
|
||||
|
||||
start = end;
|
||||
}
|
||||
|
||||
this._cursor.draw_after(canvas);
|
||||
canvas.restore();
|
||||
},
|
||||
|
||||
// Styling
|
||||
|
||||
// returns [index, prev total length before this style]
|
||||
find_style_index_and_prev_length: function(index) {
|
||||
let len = 0;
|
||||
for (let i = 0; i < this._styles.length; ++i) {
|
||||
const l = this._styles[i]._length;
|
||||
len += l;
|
||||
// < favors the latter style if index is between two styles
|
||||
if (index < len) {
|
||||
return [i, len - l];
|
||||
}
|
||||
}
|
||||
ASSERT(len === this._text.length);
|
||||
return [this._styles.length-1, len];
|
||||
},
|
||||
_delete_style_range: function(start, end) {
|
||||
// shrink/remove styles
|
||||
//
|
||||
// [.....][....][....][.....] styles
|
||||
// [..................] start...end
|
||||
//
|
||||
// - trim the first style
|
||||
// - remove the middle styles
|
||||
// - trim the last style
|
||||
|
||||
let N = end - start;
|
||||
let [i, prev_len] = this.find_style_index_and_prev_length(start);
|
||||
let s = this._styles[i];
|
||||
if (start > prev_len) {
|
||||
// we overlap the first style (but not entirely
|
||||
const skip = start - prev_len;
|
||||
ASSERT(skip < s._length);
|
||||
const shrink = Math.min(N, s._length - skip);
|
||||
ASSERT(shrink > 0);
|
||||
s._length -= shrink;
|
||||
N -= shrink;
|
||||
if (N === 0) {
|
||||
return;
|
||||
}
|
||||
i += 1;
|
||||
ASSERT(i < this._styles.length);
|
||||
}
|
||||
while (N > 0) {
|
||||
s = this._styles[i];
|
||||
if (N >= s._length) {
|
||||
N -= s._length;
|
||||
this._styles.splice(i, 1);
|
||||
} else {
|
||||
s._length -= N;
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
applyStyleToRange: function(style, start, end) {
|
||||
if (start > end) { [start, end] = [end, start]; }
|
||||
ASSERT(start >= 0 && end <= this._text.length);
|
||||
if (start === end) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOG('trying to apply', style, start, end);
|
||||
let i;
|
||||
for (i = 0; i < this._styles.length; ++i) {
|
||||
if (start <= this._styles[i]._length) {
|
||||
break;
|
||||
}
|
||||
start -= this._styles[i]._length;
|
||||
end -= this._styles[i]._length;
|
||||
}
|
||||
|
||||
let s = this._styles[i];
|
||||
// do we need to fission off a clean subset for the head of s?
|
||||
if (start > 0) {
|
||||
const ns = Object.assign({}, s);
|
||||
s._length = start;
|
||||
ns._length -= start;
|
||||
LOG('initial splice', i, start, s._length, ns._length);
|
||||
i += 1;
|
||||
this._styles.splice(i, 0, ns);
|
||||
end -= start;
|
||||
// we don't use start any more
|
||||
}
|
||||
// merge into any/all whole styles we overlap
|
||||
let layoutChanged = false;
|
||||
while (end >= this._styles[i]._length) {
|
||||
LOG('whole run merging for style index', i)
|
||||
layoutChanged |= this._styles[i].mergeFrom(style);
|
||||
end -= this._styles[i]._length;
|
||||
i += 1;
|
||||
if (end == 0) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
// do we partially cover the last run
|
||||
if (end > 0) {
|
||||
s = this._styles[i];
|
||||
const ns = Object.assign({}, s); // the new first half
|
||||
ns._length = end;
|
||||
s._length -= end; // trim the (unchanged) tail
|
||||
LOG('merging tail', i, ns._length, s._length);
|
||||
layoutChanged |= ns.mergeFrom(style);
|
||||
this._styles.splice(i, 0, ns);
|
||||
}
|
||||
|
||||
this._validateStyles();
|
||||
LOG('after applying styles', this._styles);
|
||||
|
||||
if (layoutChanged) {
|
||||
this._buildLines();
|
||||
}
|
||||
},
|
||||
applyStyleToSelection: function(style) {
|
||||
this.applyStyleToRange(style, this._index.start, this._index.end);
|
||||
},
|
||||
};
|
||||
|
||||
const s = MakeStyle(ed._text.length);
|
||||
s.mergeFrom(style);
|
||||
ed._styles = [ s ];
|
||||
ed._buildLines();
|
||||
return ed;
|
||||
}
|
Loading…
Reference in New Issue
Block a user