diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml
index c4f1e81c5e4..3b34011e098 100644
--- a/.github/workflows/nodejs.yml
+++ b/.github/workflows/nodejs.yml
@@ -19,7 +19,7 @@ jobs:
strategy:
matrix:
- node-version: [16.x]
+ node-version: [22.x]
steps:
- uses: actions/checkout@v2
diff --git a/ace-internal.d.ts b/ace-internal.d.ts
index 7380d3c1833..85cefb7d05e 100644
--- a/ace-internal.d.ts
+++ b/ace-internal.d.ts
@@ -85,6 +85,16 @@ export namespace Ace {
offset: number,
height: number,
gutterOffset: number
+ fontMetrics: {
+ textWidth: (row: number, column: number) => number,
+ getRects: (start: Position, end: Position) => Rect[]
+ }
+ }
+ interface Rect {
+ left: number,
+ top: number,
+ width: number,
+ height: number,
}
interface HardWrapOptions {
@@ -1449,6 +1459,7 @@ declare module "./src/edit_session" {
_changedWidgets?: any,
$options: any,
$wrapMethod?: any,
+ $fontMetrics?: FontMetrics|null,
$enableVarChar?: any,
$wrap?: any,
$navigateWithinSoftTabs?: boolean,
diff --git a/ace.d.ts b/ace.d.ts
index e42bef67a26..592fe419efe 100644
--- a/ace.d.ts
+++ b/ace.d.ts
@@ -84,6 +84,16 @@ declare module "ace-code" {
offset: number;
height: number;
gutterOffset: number;
+ fontMetrics: {
+ textWidth: (row: number, column: number) => number;
+ getRects: (start: Position, end: Position) => Rect[];
+ };
+ }
+ interface Rect {
+ left: number;
+ top: number;
+ width: number;
+ height: number;
}
interface HardWrapOptions {
/** First row of the range to process */
diff --git a/demo/kitchen-sink/demo.js b/demo/kitchen-sink/demo.js
index b71c7a5adb5..1cd8cd04c8e 100644
--- a/demo/kitchen-sink/demo.js
+++ b/demo/kitchen-sink/demo.js
@@ -673,6 +673,16 @@ optionsPanelContainer.insertBefore(
"Open Dialog ",
["button", {onclick: openTestDialog.bind(null, false)}, "Scale"],
["button", {onclick: openTestDialog.bind(null, true)}, "Height"]
+ ],
+ ["div", {},
+ ["button", {onclick: function() {
+ editor.setOption("fontFamily", "cursive");
+ session.setValue( session.getValue() + "שלום עולם בעברית123" +"\n" + "ジャパン + 八洲\n" + "𒐫𒈙⸻ဪ", 1);
+ }}, "cursive"],
+ ["button", {onclick: function() {
+ editor.setOption("fontFamily", "Tahoma");
+ session.setValue( session.getValue() + "שלום עולם בעברית123" +"\n" + "ジャパン + 八洲", 1);
+ }}, "Tahoma"],
]
]),
optionsPanelContainer.children[1]
diff --git a/demo/kitchen-sink/inline_editor.js b/demo/kitchen-sink/inline_editor.js
index ec1b6d9bfab..40e82b53b3f 100644
--- a/demo/kitchen-sink/inline_editor.js
+++ b/demo/kitchen-sink/inline_editor.js
@@ -21,7 +21,7 @@ require("ace/commands/default_commands").commands.push({
return;
}
- var rowCount = 10;
+ var rowCount = 5.5;
var w = {
row: row,
// rowCount: rowCount,
diff --git a/experiments/bidi.html b/experiments/bidi.html
new file mode 100644
index 00000000000..f299ebc9bb9
--- /dev/null
+++ b/experiments/bidi.html
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/experiments/transform.html b/experiments/transform.html
new file mode 100644
index 00000000000..6f53f791074
--- /dev/null
+++ b/experiments/transform.html
@@ -0,0 +1,441 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/src/bidihandler.js b/src/bidihandler.js
index 20f023613e2..16ce32f334b 100644
--- a/src/bidihandler.js
+++ b/src/bidihandler.js
@@ -51,12 +51,7 @@ class BidiHandler {
isBidiRow(screenRow, docRow, splitIndex) {
if (!this.seenBidi)
return false;
- if (screenRow !== this.currentRow) {
- this.currentRow = screenRow;
- this.updateRowLine(docRow, splitIndex);
- this.updateBidiMap();
- }
- return this.bidiMap.bidiLevels;
+ return true;
}
/**
@@ -107,65 +102,6 @@ class BidiHandler {
return splitIndex;
}
- updateRowLine(docRow, splitIndex) {
- if (docRow === undefined)
- docRow = this.getDocumentRow();
-
- var isLastRow = (docRow === this.session.getLength() - 1),
- endOfLine = isLastRow ? this.EOF : this.EOL;
-
- this.wrapIndent = 0;
- this.line = this.session.getLine(docRow);
- this.isRtlDir = this.$isRtl || this.line.charAt(0) === this.RLE;
- if (this.session.$useWrapMode) {
- var splits = this.session.$wrapData[docRow];
- if (splits) {
- if (splitIndex === undefined)
- splitIndex = this.getSplitIndex();
-
- if(splitIndex > 0 && splits.length) {
- this.wrapIndent = splits.indent;
- this.wrapOffset = this.wrapIndent * this.charWidths[bidiUtil.L];
- this.line = (splitIndex < splits.length) ?
- this.line.substring(splits[splitIndex - 1], splits[splitIndex]) :
- this.line.substring(splits[splits.length - 1]);
- } else {
- this.line = this.line.substring(0, splits[splitIndex]);
- }
-
- if (splitIndex == splits.length) {
- this.line += (this.showInvisibles) ? endOfLine : bidiUtil.DOT;
- }
- }
- } else {
- this.line += this.showInvisibles ? endOfLine : bidiUtil.DOT;
- }
-
- /* replace tab and wide characters by commensurate spaces */
- var session = this.session, shift = 0, size;
- this.line = this.line.replace(/\t|[\u1100-\u2029, \u202F-\uFFE6]/g, function(ch, i){
- if (ch === '\t' || session.isFullWidth(ch.charCodeAt(0))) {
- size = (ch === '\t') ? session.getScreenTabSize(i + shift) : 2;
- shift += size - 1;
- return lang.stringRepeat(bidiUtil.DOT, size);
- }
- return ch;
- });
-
- if (this.isRtlDir) {
- this.fontMetrics.$main.textContent = (this.line.charAt(this.line.length - 1) == bidiUtil.DOT) ? this.line.substr(0, this.line.length - 1) : this.line;
- this.rtlLineOffset = this.contentWidth - this.fontMetrics.$main.getBoundingClientRect().width;
- }
- }
-
- updateBidiMap() {
- var textCharTypes = [];
- if (bidiUtil.hasBidiCharacters(this.line, textCharTypes) || this.isRtlDir) {
- this.bidiMap = bidiUtil.doBidiReorder(this.line, textCharTypes, this.isRtlDir);
- } else {
- this.bidiMap = {};
- }
- }
/**
* Resets stored info related to current screen row
@@ -174,27 +110,6 @@ class BidiHandler {
this.currentRow = null;
}
- /**
- * Updates array of character widths
- * @param {Object} fontMetrics metrics
- *
- **/
- updateCharacterWidths(fontMetrics) {
- if (this.characterWidth === fontMetrics.$characterSize.width)
- return;
-
- this.fontMetrics = fontMetrics;
- var characterWidth = this.characterWidth = fontMetrics.$characterSize.width;
- var bidiCharWidth = fontMetrics.$measureCharWidth("\u05d4");
-
- this.charWidths[bidiUtil.L] = this.charWidths[bidiUtil.EN] = this.charWidths[bidiUtil.ON_R] = characterWidth;
- this.charWidths[bidiUtil.R] = this.charWidths[bidiUtil.AN] = bidiCharWidth;
- this.charWidths[bidiUtil.R_H] = bidiCharWidth * 0.45;
- this.charWidths[bidiUtil.B] = this.charWidths[bidiUtil.RLE] = 0;
-
- this.currentRow = null;
- }
-
setShowInvisibles(showInvisibles) {
this.showInvisibles = showInvisibles;
this.currentRow = null;
@@ -225,138 +140,6 @@ class BidiHandler {
editor.session.doc.insert({column: 0, row: row}, editor.session.$bidiHandler.RLE);
}
}
-
-
- /**
- * Returns offset of character at position defined by column.
- * @param {Number} col the screen column position
- *
- * @return {Number} horizontal pixel offset of given screen column
- **/
- getPosLeft(col) {
- col -= this.wrapIndent;
- var leftBoundary = (this.line.charAt(0) === this.RLE) ? 1 : 0;
- var logicalIdx = (col > leftBoundary) ? (this.session.getOverwrite() ? col : col - 1) : leftBoundary;
- var visualIdx = bidiUtil.getVisualFromLogicalIdx(logicalIdx, this.bidiMap),
- levels = this.bidiMap.bidiLevels, left = 0;
-
- if (!this.session.getOverwrite() && col <= leftBoundary && levels[visualIdx] % 2 !== 0)
- visualIdx++;
-
- for (var i = 0; i < visualIdx; i++) {
- left += this.charWidths[levels[i]];
- }
-
- if (!this.session.getOverwrite() && (col > leftBoundary) && (levels[visualIdx] % 2 === 0))
- left += this.charWidths[levels[visualIdx]];
-
- if (this.wrapIndent)
- left += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
-
- if (this.isRtlDir)
- left += this.rtlLineOffset;
-
- return left;
- }
-
- /**
- * Returns 'selections' - array of objects defining set of selection rectangles
- * @param {Number} startCol the start column position
- * @param {Number} endCol the end column position
- *
- * @return {Object[]} Each object contains 'left' and 'width' values defining selection rectangle.
- **/
- getSelections(startCol, endCol) {
- var map = this.bidiMap, levels = map.bidiLevels, level, selections = [], offset = 0,
- selColMin = Math.min(startCol, endCol) - this.wrapIndent, selColMax = Math.max(startCol, endCol) - this.wrapIndent,
- isSelected = false, isSelectedPrev = false, selectionStart = 0;
-
- if (this.wrapIndent)
- offset += this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
-
- for (var logIdx, visIdx = 0; visIdx < levels.length; visIdx++) {
- logIdx = map.logicalFromVisual[visIdx];
- level = levels[visIdx];
- isSelected = (logIdx >= selColMin) && (logIdx < selColMax);
- if (isSelected && !isSelectedPrev) {
- selectionStart = offset;
- } else if (!isSelected && isSelectedPrev) {
- selections.push({left: selectionStart, width: offset - selectionStart});
- }
- offset += this.charWidths[level];
- isSelectedPrev = isSelected;
- }
-
- if (isSelected && (visIdx === levels.length)) {
- selections.push({left: selectionStart, width: offset - selectionStart});
- }
-
- if(this.isRtlDir) {
- for (var i = 0; i < selections.length; i++) {
- selections[i].left += this.rtlLineOffset;
- }
- }
- return selections;
- }
-
- /**
- * Converts character coordinates on the screen to respective document column number
- * @param {Number} posX character horizontal offset
- *
- * @return {Number} screen column number corresponding to given pixel offset
- **/
- offsetToCol(posX) {
- if(this.isRtlDir)
- posX -= this.rtlLineOffset;
-
- var logicalIdx = 0, posX = Math.max(posX, 0),
- offset = 0, visualIdx = 0, levels = this.bidiMap.bidiLevels,
- charWidth = this.charWidths[levels[visualIdx]];
-
- if (this.wrapIndent)
- posX -= this.isRtlDir ? (-1 * this.wrapOffset) : this.wrapOffset;
-
- while(posX > offset + charWidth/2) {
- offset += charWidth;
- if(visualIdx === levels.length - 1) {
- /* quit when we on the right of the last character, flag this by charWidth = 0 */
- charWidth = 0;
- break;
- }
- charWidth = this.charWidths[levels[++visualIdx]];
- }
-
- if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && (levels[visualIdx] % 2 === 0)){
- /* Bidi character on the left and None Bidi character on the right */
- if(posX < offset)
- visualIdx--;
- logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
-
- } else if (visualIdx > 0 && (levels[visualIdx - 1] % 2 === 0) && (levels[visualIdx] % 2 !== 0)){
- /* None Bidi character on the left and Bidi character on the right */
- logicalIdx = 1 + ((posX > offset) ? this.bidiMap.logicalFromVisual[visualIdx]
- : this.bidiMap.logicalFromVisual[visualIdx - 1]);
-
- } else if ((this.isRtlDir && visualIdx === levels.length - 1 && charWidth === 0 && (levels[visualIdx - 1] % 2 === 0))
- || (!this.isRtlDir && visualIdx === 0 && (levels[visualIdx] % 2 !== 0))){
- /* To the right of last character, which is None Bidi, in RTL direction or */
- /* to the left of first Bidi character, in LTR direction */
- logicalIdx = 1 + this.bidiMap.logicalFromVisual[visualIdx];
- } else {
- /* Tweak visual position when Bidi character on the left in order to map it to corresponding logical position */
- if (visualIdx > 0 && (levels[visualIdx - 1] % 2 !== 0) && charWidth !== 0)
- visualIdx--;
-
- /* Regular case */
- logicalIdx = this.bidiMap.logicalFromVisual[visualIdx];
- }
-
- if (logicalIdx === 0 && this.isRtlDir)
- logicalIdx++;
-
- return (logicalIdx + this.wrapIndent);
- }
-
}
exports.BidiHandler = BidiHandler;
diff --git a/src/edit_session.js b/src/edit_session.js
index fd03473cdd0..f58492aacf0 100644
--- a/src/edit_session.js
+++ b/src/edit_session.js
@@ -2102,10 +2102,6 @@ class EditSession {
// tab
if (c == 9) {
screenColumn += this.getScreenTabSize(screenColumn);
- }
- // full width characters
- else if (c >= 0x1100 && isFullWidth(c)) {
- screenColumn += 2;
} else {
screenColumn += 1;
}
@@ -2238,13 +2234,12 @@ class EditSession {
* Converts characters coordinates on the screen to characters coordinates within the document. [This takes into account code folding, word wrap, tab size, and any other visual modifications.]{: #conversionConsiderations}
* @param {Number} screenRow The screen row to check
* @param {Number} screenColumn The screen column to check
- * @param {Number} [offsetX] screen character x-offset [optional]
*
* @returns {Point} The object returned has two properties: `row` and `column`.
*
* @related EditSession.documentToScreenPosition
**/
- screenToDocumentPosition(screenRow, screenColumn, offsetX) {
+ screenToDocumentPosition(screenRow, screenColumn) {
if (screenRow < 0)
return {row: 0, column: 0};
@@ -2316,9 +2311,6 @@ class EditSession {
}
}
- if (offsetX !== undefined && this.$bidiHandler.isBidiRow(row + splitIndex, docRow, splitIndex))
- screenColumn = this.$bidiHandler.offsetToCol(offsetX);
-
docColumn += this.$getStringScreenWidth(line, screenColumn - wrapIndent)[1];
// We remove one character at the end so that the docColumn
diff --git a/src/edit_session_test.js b/src/edit_session_test.js
index 05b105ab32a..166817509ea 100644
--- a/src/edit_session_test.js
+++ b/src/edit_session_test.js
@@ -207,7 +207,7 @@ module.exports = {
assert.equal(session.getScreenLastRowColumn(0), 4);
assert.equal(session.getScreenLastRowColumn(1), 10);
- assert.equal(session.getScreenLastRowColumn(2), 5);
+ assert.equal(session.getScreenLastRowColumn(2), 3);
},
"test: convert document to screen coordinates" : function() {
@@ -252,7 +252,7 @@ module.exports = {
assert.position(session.documentToScreenPosition(0, 3), 0, 3);
assert.position(session.documentToScreenPosition(1, 3), 1, 4);
assert.position(session.documentToScreenPosition(1, 4), 1, 8);
- assert.position(session.documentToScreenPosition(2, 2), 2, 4);
+ assert.position(session.documentToScreenPosition(2, 2), 2, 2);
},
"test: documentToScreen with soft wrap": function() {
@@ -326,9 +326,9 @@ module.exports = {
session.setUseWrapMode(true);
session.adjustWrapLimit(80);
- assert.position(session.screenToDocumentPosition(0, 1), 0, 0);
- assert.position(session.screenToDocumentPosition(0, 2), 0, 1);
- assert.position(session.screenToDocumentPosition(0, 3), 0, 2);
+ assert.position(session.screenToDocumentPosition(0, 1), 0, 1);
+ assert.position(session.screenToDocumentPosition(0, 2), 0, 2);
+ assert.position(session.screenToDocumentPosition(0, 3), 0, 3);
assert.position(session.screenToDocumentPosition(0, 4), 0, 3);
assert.position(session.screenToDocumentPosition(0, 5), 0, 3);
},
diff --git a/src/editor.js b/src/editor.js
index 6f9391a2bf4..408e72aa24f 100644
--- a/src/editor.js
+++ b/src/editor.js
@@ -272,6 +272,7 @@ class Editor {
this.session.off("endOperation", this.$onEndOperation);
var selection = this.session.getSelection();
+ this.session.$fontMetrics = null;
selection.off("changeCursor", this.$onCursorChange);
selection.off("changeSelection", this.$onSelectionChange);
}
@@ -281,6 +282,7 @@ class Editor {
this.$onDocumentChange = this.onDocumentChange.bind(this);
session.on("change", this.$onDocumentChange);
this.renderer.setSession(session);
+ session.$fontMetrics = this.renderer.$fontMetrics;
this.$onChangeMode = this.onChangeMode.bind(this);
session.on("changeMode", this.$onChangeMode);
diff --git a/src/ext/diff/inline_diff_view.js b/src/ext/diff/inline_diff_view.js
index e3cb1984e51..73f24ce5447 100644
--- a/src/ext/diff/inline_diff_view.js
+++ b/src/ext/diff/inline_diff_view.js
@@ -197,8 +197,8 @@ class InlineDiffView extends BaseDiffView {
var screenPos = ev.editor.renderer.pixelToScreenCoordinates(ev.clientX, ev.clientY);
var sessionA = this.activeEditor.session;
var sessionB = this.otherEditor.session;
- var posA = sessionA.screenToDocumentPosition(screenPos.row, screenPos.column, screenPos.offsetX);
- var posB = sessionB.screenToDocumentPosition(screenPos.row, screenPos.column, screenPos.offsetX);
+ var posA = sessionA.screenToDocumentPosition(screenPos.row, screenPos.column);
+ var posB = sessionB.screenToDocumentPosition(screenPos.row, screenPos.column);
var posAx = sessionA.documentToScreenPosition(posA);
var posBx = sessionB.documentToScreenPosition(posB);
@@ -381,13 +381,6 @@ class InlineDiffView extends BaseDiffView {
cloneRenderer.$computeLayerConfig();
var newConfig = cloneRenderer.layerConfig;
-
- this.gutterLayer.update(newConfig);
-
- newConfig.firstRowScreen = config.firstRowScreen;
-
- cloneRenderer.$cursorLayer.config = newConfig;
- cloneRenderer.$cursorLayer.update(newConfig);
if (changes & cloneRenderer.CHANGE_LINES
|| changes & cloneRenderer.CHANGE_FULL
@@ -395,6 +388,13 @@ class InlineDiffView extends BaseDiffView {
|| changes & cloneRenderer.CHANGE_TEXT
)
this.textLayer.update(newConfig);
+
+ this.gutterLayer.update(newConfig);
+
+ newConfig.firstRowScreen = config.firstRowScreen;
+
+ cloneRenderer.$cursorLayer.config = newConfig;
+ cloneRenderer.$cursorLayer.update(newConfig);
this.markerLayer.setMarkers(this.otherSession.getMarkers());
this.markerLayer.update(newConfig);
diff --git a/src/keyboard/textinput_test.js b/src/keyboard/textinput_test.js
index b4af590ac09..1b84d38d992 100644
--- a/src/keyboard/textinput_test.js
+++ b/src/keyboard/textinput_test.js
@@ -187,7 +187,7 @@ module.exports = {
{ _: "input", range: [3,3], value: "きもの"},
function() {
assert.ok(editor.renderer.$composition);
- assert.ok(Math.abs(parseFloat(textarea.style.width) - editor.renderer.characterWidth * 6) < 1);
+ assert.ok(Math.abs(parseFloat(textarea.style.width) - editor.renderer.characterWidth * 6) < 2);
assert.ok(Math.abs(parseFloat(textarea.style.height) - (editor.renderer.lineHeight)) < 1);
assert.ok(Math.abs(parseFloat(textarea.style.top)) < 1);
assert.ok(/ace_composition/.test(textarea.className));
diff --git a/src/keyboard/vim.js b/src/keyboard/vim.js
index f057eb5984d..b0c829fd316 100644
--- a/src/keyboard/vim.js
+++ b/src/keyboard/vim.js
@@ -7351,7 +7351,7 @@ domLib.importCssString(`.normal-mode .ace_cursor{
$id: "ace/keyboard/vim",
drawCursor: function(element, pixelPos, config, sel, session) {
var vim = this.state.vim || {};
- var w = config.characterWidth;
+ var w = pixelPos.width || config.characterWidth;
var h = config.lineHeight;
var top = pixelPos.top;
var left = pixelPos.left;
diff --git a/src/layer/cursor.js b/src/layer/cursor.js
index 463cb8db2c4..919b4230bf9 100644
--- a/src/layer/cursor.js
+++ b/src/layer/cursor.js
@@ -175,22 +175,23 @@ class Cursor {
/**
* @param {import("../../ace-internal").Ace.Point} [position]
* @param {boolean} [onScreen]
+ * @return {{left: number, top: number, width: number}}
*/
getPixelPosition(position, onScreen) {
if (!this.config || !this.session)
- return {left : 0, top : 0};
+ return {left : 0, top : 0, width: 0};
if (!position)
position = this.session.selection.getCursor();
var pos = this.session.documentToScreenPosition(position);
- var cursorLeft = this.$padding + (this.session.$bidiHandler.isBidiRow(pos.row, position.row)
- ? this.session.$bidiHandler.getPosLeft(pos.column)
- : pos.column * this.config.characterWidth);
+ var textWidth = this.config.fontMetrics.textWidth(pos.row, pos.column);
+ var cursorLeft = this.$padding + textWidth;
var cursorTop = (pos.row - (onScreen ? this.config.firstRowScreen : 0)) *
this.config.lineHeight;
+ var cursorWidth = this.config.fontMetrics.textWidth(pos.row, pos.column + 1) - textWidth;
- return {left : cursorLeft, top : cursorTop};
+ return {left : cursorLeft, top : cursorTop, width : Math.abs(cursorWidth)};
}
isCursorInView(pixelPos, config) {
@@ -223,7 +224,7 @@ class Cursor {
} else {
dom.setStyle(style, "display", "block");
dom.translate(element, pixelPos.left, pixelPos.top);
- dom.setStyle(style, "width", Math.round(config.characterWidth) + "px");
+ dom.setStyle(style, "width", Math.round(pixelPos.width) + "px");
dom.setStyle(style, "height", config.lineHeight + "px");
}
} else {
diff --git a/src/layer/font_metrics.js b/src/layer/font_metrics.js
index 4ddc758dc82..d7a78dd338b 100644
--- a/src/layer/font_metrics.js
+++ b/src/layer/font_metrics.js
@@ -1,8 +1,8 @@
+/*global Node, NodeFilter*/
var oop = require("../lib/oop");
var dom = require("../lib/dom");
var lang = require("../lib/lang");
var event = require("../lib/event");
-var useragent = require("../lib/useragent");
var EventEmitter = require("../lib/event_emitter").EventEmitter;
var CHAR_COUNT = 512;
@@ -14,7 +14,12 @@ class FontMetrics {
/**
* @param {HTMLElement} parentEl
*/
- constructor(parentEl) {
+ constructor(parentEl, textLayer, renderer) {
+ this.config = {characterWidth: 1};
+ this.$characterSize = {width: 0, height: 0};
+ this.textLayer = textLayer;
+ this.renderer = renderer;
+
this.el = dom.createElement("div");
this.$setMeasureNodeStyles(this.el.style, true);
@@ -31,13 +36,14 @@ class FontMetrics {
this.$measureNode.textContent = lang.stringRepeat("X", CHAR_COUNT);
- this.$characterSize = {width: 0, height: 0};
-
-
if (USE_OBSERVER)
this.$addObserver();
else
this.checkForSizeChanges();
+
+ this.textLayer.$setFontMetrics(this);
+
+ this.$scratchRange = document.createRange();
}
$setMeasureNodeStyles(style, isRoot) {
@@ -47,11 +53,7 @@ class FontMetrics {
style.position = "absolute";
style.whiteSpace = "pre";
- if (useragent.isIE < 8) {
- style["font-family"] = "inherit";
- } else {
- style.font = "inherit";
- }
+ style.font = "inherit";
style.overflow = isRoot ? "hidden" : "visible";
}
@@ -134,6 +136,12 @@ class FontMetrics {
return w;
}
+ getTextWidth(text) {
+ if (!text) return 0;
+ this.$main.textContent = text;
+ return this.$main.clientWidth;
+ }
+
destroy() {
clearInterval(this.$pollSizeChangesTimer);
if (this.$observer)
@@ -142,16 +150,10 @@ class FontMetrics {
this.el.parentNode.removeChild(this.el);
}
-
- $getZoom(element) {
- if (!element || !element.parentElement) return 1;
- return (Number(window.getComputedStyle(element)["zoom"]) || 1) * this.$getZoom(element.parentElement);
- }
-
$initTransformMeasureNodes() {
- var t = function(t, l) {
+ var t = function(l, t) {
return ["div", {
- style: "position: absolute;top:" + t + "px;left:" + l + "px;"
+ style: "position: absolute;left:" + l + "px;top:" + t + "px;"
}];
};
this.els = dom.buildDom([t(0, 0), t(L, 0), t(0, L), t(L, L)], this.el);
@@ -162,25 +164,13 @@ class FontMetrics {
// | h[0] h[1] 1 | | 1 | | 1 |
// this function finds the coeeficients of the matrix using positions of four points
//
- transformCoordinates(clientPos, elPos) {
- if (clientPos) {
- var zoom = this.$getZoom(this.el);
- clientPos = mul(1 / zoom, clientPos);
- }
- function solve(l1, l2, r) {
- var det = l1[1] * l2[0] - l1[0] * l2[1];
- return [
- (-l2[1] * r[0] + l2[0] * r[1]) / det,
- (+l1[1] * r[0] - l1[0] * r[1]) / det
- ];
+ getTransform() {
+ if (this.config.$transformData) {
+ return this.config.$transformData;
}
- function sub(a, b) { return [a[0] - b[0], a[1] - b[1]]; }
- function add(a, b) { return [a[0] + b[0], a[1] + b[1]]; }
- function mul(a, b) { return [a * b[0], a * b[1]]; }
-
if (!this.els)
this.$initTransformMeasureNodes();
-
+
function p(el) {
var r = el.getBoundingClientRect();
return [r.left, r.top];
@@ -193,23 +183,516 @@ class FontMetrics {
var h = solve(sub(d, b), sub(d, c), sub(add(b, c), add(d, a)));
- var m1 = mul(1 + h[0], sub(b, a));
- var m2 = mul(1 + h[1], sub(c, a));
-
+ var m1 = mul((1 + h[0])/L, sub(b, a));
+ var m2 = mul((1 + h[1])/L, sub(c, a));
+
+ var M = [
+ m1[0], m2[0], 0,
+ m1[1], m2[1], 0,
+ h[0]/L, h[1]/L, 1
+ ];
+
+ var detM = 1 / (M[0] * M[4] - M[3] * M[1]);
+ var MInv = [
+ M[4] * detM, -M[1] * detM, 0,
+ -M[3] * detM, M[0] * detM, 0,
+ (M[3] * M[7] - M[4] * M[6]) * detM, (M[1] * M[6] - M[0] * M[7]) * detM, 1
+ ];
+
+ this.config.$transformData = {
+ M, MInv, t: a
+ };
+ return this.config.$transformData;
+ }
+
+ transformCoordinates(clientPos, elPos) {
+ if (!this.config.$transformData)
+ this.getTransform();
+ var tr = this.config.$transformData;
+
if (elPos) {
- var x = elPos;
- var k = h[0] * x[0] / L + h[1] * x[1] / L + 1;
- var ut = add(mul(x[0], m1), mul(x[1], m2));
- return add(mul(1 / k / L, ut), a);
+ return add(project(tr.M, elPos[0], elPos[1]), tr.t);
+ }
+ return project(tr.MInv, clientPos[0] - tr.t[0], clientPos[1] - tr.t[1]);
+ }
+
+ /**
+ * @param {{M: number[], t: number[]}} transform
+ * @param {{left: number, top: number, width: number, height: number}} bbox
+ */
+ recoverRect(transform, bbox) {
+ var M = transform.M;
+
+ // 1. Detect Affine Case (Perspective components are zero)
+ var isAffine = Math.abs(M[6]) < 1e-10 && Math.abs(M[7]) < 1e-10;
+
+ var { left, top, width: Wt, height: Ht } = bbox;
+ left -= transform.t[0];
+ top -= transform.t[1];
+
+ if (isAffine) {
+ var [m00, m01, m02, m10, m11, m12] = M;
+
+ // analytical formula: {w, h} = inverse(|M|) * {Wt, Ht}
+ var absM = [Math.abs(m00), Math.abs(m01), Math.abs(m10), Math.abs(m11)];
+ var delta = absM[0] * absM[3] - absM[2] * absM[1];
+
+ let w, h;
+ // Handle 45-degree ambiguity (Delta is near zero)
+ if (Math.abs(delta) < 1e-10) {
+ // At 45 deg: Wt = |m00|*w + |m01|*h
+ // We use lineHeight as h and solve for w
+ h = this.config && this.config.lineHeight || 0;
+ // Solve: w = (Wt - |m01|*h) / |m00|
+ w = (Wt - absM[1] * h) / absM[0];
+ } else {
+ w = (absM[3] * Wt - absM[1] * Ht) / delta;
+ h = (-absM[2] * Wt + absM[0] * Ht) / delta;
+ }
+
+ // Recover position by back-projecting center
+ var detM = m00 * m11 - m01 * m10;
+ var ctx = left + Wt / 2 - m02;
+ var cty = top + Ht / 2 - m12;
+
+ var cx = (m11 * ctx - m01 * cty) / detM;
+ var cy = (-m10 * ctx + m00 * cty) / detM;
+
+ return { left: cx - w / 2, top: cy - h / 2, width: w, height: h, right: cx + w / 2, bottom: cy + h / 2 };
+ }
+
+ return recoverRect(transform, bbox);
+ }
+
+ /**
+ * Finds and returns the DOM element corresponding to a given screen row.
+ *
+ * @param {number} screenRow - The screen row number for which to find the element.
+ * @returns {HTMLElement|null} The DOM element corresponding to the screen row, or null if not found.
+ */
+ $findElementForScreenRow(screenRow) {
+ var textLayer = this.textLayer;
+ if (!this.config) return null; // not initialized yet
+ var data = textLayer.$lines.$getCellByScreenRow(screenRow, this.config);
+ var lineElement = data && data.cell.element;
+
+ if (lineElement && textLayer.$useLineGroups()) {
+ var index = Math.floor(data.offset / this.config.lineHeight);
+ lineElement = lineElement.children[Math.max(index, 0)];
}
- var u = sub(clientPos, a);
- var f = solve(sub(m1, mul(h[0], u)), sub(m2, mul(h[1], u)), u);
- return mul(L, f);
+ return lineElement;
}
+ /**
+ * Calculates the width of the text up to a specific scrrenColumn on a given screen row.
+ *
+ * @param {number} screenRow - The row index on the screen for which the text width is calculated.
+ * @param {number} screenColumn - The column index up to which the text width is measured.
+ * @returns {number} The width of the text in pixels up to the specified column.
+ */
+ textWidth(screenRow, screenColumn) {
+ var lineElement = this.$findElementForScreenRow(screenRow);
+ if (!lineElement || !document.createRange) {
+ // Fallback for lines not currently rendered
+ return screenColumn * this.config.characterWidth;
+ }
+ return this.$measureLineToColumn(lineElement, screenColumn);
+ }
+
+
+ /**
+ * Measures the horizontal position (in pixels) of a specific screen column
+ * within a given line element. This method calculates the pixel offset
+ * from the left edge of the text layer's container to the specified column.
+ *
+ * @param {HTMLElement} lineElement - The DOM element representing the line of text.
+ * @param {number} screenColumn - The screen column index to measure.
+ * @returns {number} The horizontal position (in pixels) of the specified column
+ * relative to the left edge of the text layer's container. If the position cannot
+ * be determined, it falls back to an approximation based on the character width.
+ */
+ $measureLineToColumn(lineElement, screenColumn) {
+ var textLayer = this.textLayer;
+
+ try {
+ var position = this.$findColumnPosition(lineElement, screenColumn);
+ if (!position) {
+ return screenColumn * this.config.characterWidth;
+ }
+
+ this.$scratchRange.setStart(position.node, position.offset);
+ this.$scratchRange.setEnd(position.node, position.offset);
+
+ var rangeRect = this.$scratchRange.getBoundingClientRect();
+ if (this.renderer.$hasCssTransforms) {
+ var tr = this.getTransform();
+ var transformed = this.recoverRect(tr, rangeRect);
+ var leftOffset = this.renderer.gutterWidth + this.renderer.margin.left + this.renderer.$padding - this.renderer.scrollLeft;
+ return transformed.left - leftOffset + position.overflow * this.config.characterWidth;
+ }
+ var rect = textLayer.element.getBoundingClientRect();
+ return rangeRect.left - rect.left + position.overflow * this.config.characterWidth;
+ } catch (e) {
+ console.error("Error measuring text width:", e);
+ return screenColumn * this.config.characterWidth;
+ }
+ }
+
+ /**
+ * Finds the position of a specific column within a line element.
+ *
+ * This method traverses the text nodes within the given line element to locate
+ * the node and offset corresponding to the specified screen column. If the
+ * column exceeds the total length of the text, it returns the last node and its length.
+ *
+ * @param {HTMLElement} lineElement - The DOM element representing the line of text.
+ * @param {number} screenColumn - The target column position within the line (0-based).
+ * @returns {{node: Node, offset: number, overflow: number} | null} An object containing the text node and the offset
+ * within that node corresponding to the column position, or `null` if no nodes are found.
+ */
+ $findColumnPosition(lineElement, screenColumn) {
+ var walker = document.createTreeWalker(lineElement, NodeFilter.SHOW_TEXT, null);
+
+ var currentColumn = 0;
+ var node, lastNode;
+
+ while (node = walker.nextNode()) {
+ var nodeText = node.nodeValue;
+ var nodeLength = nodeText.length;
+
+ if (currentColumn + nodeLength >= screenColumn) {
+ return {
+ node: node,
+ offset: screenColumn - currentColumn,
+ overflow: 0
+ };
+ }
+ currentColumn += nodeLength;
+ lastNode = node;
+ }
+
+ return lastNode && {
+ node: lastNode,
+ offset: lastNode.nodeValue.length,
+ overflow: screenColumn - currentColumn,
+ };
+ }
+
+ /**
+ * Converts a pixel position (x-coordinate) to a screen column index within a given row.
+ *
+ * @param {number} screenRow - The row index on the screen.
+ * @param {number} screenColumnMonospace - Screen column if text was monospace.
+ * @param {number} x - The x-coordinate (in pixels) to convert to a column index.
+ * @param {boolean} blockCursor - Whether the cursor is in block mode.
+ * @param {boolean} [isTextWidthCoordinate] - Whether x is in the same coordinate space as textWidth().
+ * @returns {number} The calculated screen column index corresponding to the x-coordinate.
+ */
+ $pixelToColumn(screenRow, screenColumnMonospace, x, blockCursor, isTextWidthCoordinate) {
+ var scratchRange = this.$scratchRange;
+ var lineElement = this.$findElementForScreenRow(screenRow);
+ if (!lineElement || screenColumnMonospace < 0) return screenColumnMonospace;
+
+ var hasCssTransform = this.renderer.$hasCssTransforms;
+ var tr = hasCssTransform && this.getTransform();
+ if (isTextWidthCoordinate) {
+ if (!hasCssTransform) {
+ x += this.textLayer.element.getBoundingClientRect().left;
+ } else {
+ x += this.renderer.gutterWidth + this.renderer.margin.left + this.renderer.$padding - this.renderer.scrollLeft;
+ }
+ }
+
+ var screenColumn = 0;
+ var getRects = (node) => {
+ var rects = [];
+ if (node.nodeType === Node.TEXT_NODE) {
+ scratchRange.setStart(node, 0);
+ scratchRange.setEnd(node, node.nodeValue.length);
+ rects = Array.from(scratchRange.getClientRects());
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ rects = Array.from(node.getClientRects());
+ }
+ if (hasCssTransform) {
+ var fixedRects = [];
+ for (var i = 0; i < rects.length; i++) {
+ var rect = rects[i];
+ fixedRects.push(this.recoverRect(tr, rect));
+ }
+ rects = fixedRects;
+ }
+ return rects;
+ };
+ var self = this;
+ function search(node) {
+ if (node.nodeType === Node.TEXT_NODE) {
+ var textLength = node.nodeValue.length;
+ var maxDistance = Number.MAX_VALUE;
+ var index = -1;
+ var value = node.nodeValue;
+ for (var j = 0; j <= textLength; j++) {
+ scratchRange.setStart(node, j);
+ scratchRange.setEnd(node, j);
+ if (
+ j > 0 && j < textLength && /[\uDC00-\uDFFF]/.test(value.charAt(j)) &&
+ /[\uD800-\uDBFF]/.test(value.charAt(j - 1))
+
+ ) {
+ continue;
+ }
+ let rect = /** @type {ReturnType}*/(scratchRange.getBoundingClientRect());
+ if (hasCssTransform) {
+ rect = self.recoverRect(tr, rect);
+ }
+ var d = Math.abs(x - rect.left);
+ if (d < maxDistance) {
+ index = j;
+ maxDistance = d;
+ }
+ }
+ if (blockCursor) {
+ // TODO
+ }
+ return screenColumn = screenColumn + index;
+ } else if (node.nodeType === Node.ELEMENT_NODE) {
+ var childNodes = node.childNodes;
+
+ for (var i = 0; i < childNodes.length; i++) {
+ var child = childNodes[i];
+ var rects = getRects(child);
+ for (var j = 0; j < rects.length; j++) {
+ let rect = rects[j];
+ if (rect.left < x && x < rect.left + rect.width) {
+ search(child);
+ return screenColumn;
+ }
+ }
+ screenColumn += child.nodeType === Node.TEXT_NODE ? child.nodeValue.length : child.textContent.length;
+ }
+ }
+ }
+ search(lineElement);
+
+ return screenColumn;
+ }
+
+ /**
+ * Calculates and returns an array of rectangles representing the visual positions
+ * of a range of text between two screen positions within a text layer.
+ *
+ * @param {Object} startScreenPos - The starting screen position of the range.
+ * @param {number} startScreenPos.row - The row index of the starting position.
+ * @param {number} startScreenPos.column - The column index of the starting position.
+ * @param {Object} endScreenPos - The ending screen position of the range.
+ * @param {number} endScreenPos.row - The row index of the ending position.
+ * @param {number} endScreenPos.column - The column index of the ending position.
+ * @returns {Array