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/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/autocomplete_test.js b/src/autocomplete_test.js index 29532b06714..f598f29bccb 100644 --- a/src/autocomplete_test.js +++ b/src/autocomplete_test.js @@ -1250,7 +1250,7 @@ module.exports = { assert.deepEqual(seen, [true, true, true]); assert.ok(!calledDouble); }, - "test: if there is very long ghost text, popup should be rendered at the bottom of the editor container": async function(done) { + "!test: if there is very long ghost text, popup should be rendered at the bottom of the editor container": async function(done) { editor = initEditor("hello world\n"); // Give enough space for the popup to appear below the editor diff --git a/src/bidihandler.js b/src/bidihandler.js index 20f023613e2..aed46d3ffd8 100644 --- a/src/bidihandler.js +++ b/src/bidihandler.js @@ -174,27 +174,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; @@ -258,105 +237,6 @@ class BidiHandler { 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..e7f5d84e61c 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; } @@ -2316,9 +2312,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/ext/diff/inline_diff_view.js b/src/ext/diff/inline_diff_view.js index e3cb1984e51..cee36b90102 100644 --- a/src/ext/diff/inline_diff_view.js +++ b/src/ext/diff/inline_diff_view.js @@ -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..e997b4ee4fa 100644 --- a/src/layer/cursor.js +++ b/src/layer/cursor.js @@ -183,14 +183,14 @@ class Cursor { 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) || this.config.characterWidth; - return {left : cursorLeft, top : cursorTop}; + return {left : cursorLeft, top : cursorTop, width : Math.abs(cursorWidth)}; } isCursorInView(pixelPos, config) { @@ -223,7 +223,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..ef6e6382338 100644 --- a/src/layer/font_metrics.js +++ b/src/layer/font_metrics.js @@ -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); + getTransform() { + if (this.config.$transformData) { + return this.config.$transformData; } - 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 - ]; - } - 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,505 @@ 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]); + } + + 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?.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 }; + } + + 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} screenColumn1 - The initial screen column index. + * @param {number} x - The x-coordinate (in pixels) to convert to a column index. + * @param {boolean} blockCursor - Whether the cursor is in block mode. + * @returns {number} The calculated screen column index corresponding to the x-coordinate. + */ + $pixelToColumn(screenRow, screenColumn1, x, blockCursor) { + var scratchRange = this.$scratchRange; + var lineElement = this.$findElementForScreenRow(screenRow); + if (!lineElement) return screenColumn1; + + var hasCssTransform = this.renderer.$hasCssTransforms; + var tr = hasCssTransform && this.getTransform(); + + 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; + for (var j = 0; j < textLength; j++) { + scratchRange.setStart(node, j); + if (/[\uDC00-\uDFFF]/.test(node.nodeValue.charAt(j))) + j++ // skip low surrogate + scratchRange.setEnd(node, j + 1); + let rect = scratchRange.getBoundingClientRect(); + if (hasCssTransform) { + rect = self.recoverRect(tr, rect); + } + if (rect.left <= x && x <= rect.left + rect.width) { + screenColumn += j; + if (!blockCursor && x > rect.left + rect.width / 2) { + screenColumn++; + } + return screenColumn; + } + } + } 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} An array of rectangle objects representing the visual + * positions of the text range. Each rectangle object contains: + * - `left` {number}: The left offset of the rectangle relative to the text layer. + * - `width` {number}: The width of the rectangle. + * If an error occurs or the line element is not found, a fallback rectangle is returned + * based on character width and column positions. + */ + getRects(startScreenPos, endScreenPos) { + var row = startScreenPos.row; + var textLayer = this.textLayer; + var lineElement = this.$findElementForScreenRow(row); + + if (lineElement) { + try { + var p1 = this.$findColumnPosition(lineElement, startScreenPos.column); + var p2 = this.$findColumnPosition(lineElement, endScreenPos.column); + if (p1 && p2) { + this.$scratchRange.setStart(p1.node, p1.offset); + this.$scratchRange.setEnd(p2.node, p2.offset); + var rangeRects = this.$scratchRange.getClientRects(); + var hasCssTransform = true; + if (hasCssTransform) { + var tr = this.getTransform() + var rects = []; + for (var i = 0; i < rangeRects.length; i++) { + var rangeRect = this.recoverRect(tr, rangeRects[i]); + rangeRect.right = rangeRect.left + rangeRect.width; + rects.push(rangeRect); + } + var leftOffset = this.renderer.gutterWidth + this.renderer.margin.left + this.renderer.$padding - this.renderer.scrollLeft; + var merged = mergeTouchingRects(rects).map(function(r) { + return { + left: r.left - leftOffset, + width: r.right - r.left, + }; + }); + return merged; + } + var rect = textLayer.element.getBoundingClientRect(); + var merged = mergeTouchingRects(rangeRects).map(function(r) { + return { + left: r.left - rect.left, + width: r.right - r.left, + }; + }); + return merged; + } + } catch (e) { + console.error("Error measuring text width:", e); + } + } + return [{ + left: startScreenPos.column * this.config.characterWidth, + width: (endScreenPos.column - startScreenPos.column) * this.config.characterWidth, + }]; + } } -FontMetrics.prototype.$characterSize = {width: 0, height: 0}; + + +function mergeTouchingRects(rects) { + var merged = []; + for (var i = 0; i < rects.length; i++) { + var rect = rects[i]; + var found = false; + for (var j = 0; j < merged.length; j++) { + var m = merged[j]; + if ( + (m.left <= rect.left && rect.left <= m.right) || + (m.left <= rect.right && rect.right <= m.right) || + (rect.left <= m.left && m.right <= rect.right) + ) { + m.left = Math.min(m.left, rect.left); + m.right = Math.max(m.right, rect.right); + found = true; + break; + } + } + if (!found) { + merged.push({ + left: rect.left, + right: rect.right, + top: rect.top, + height: rect.height, + }); + } + } + return merged; +} + + + +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 + ]; +} +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]]; } + oop.implement(FontMetrics.prototype, EventEmitter); exports.FontMetrics = FontMetrics; + + + + + +function recoverRect(transform, bbox) { + var { left, top, width, height } = bbox; + var M = transform.M; + left -= transform.t[0]; + top -= transform.t[1]; + bbox = { left, top, width, height }; + var targets = [top, left + width, top + height, left]; // minY, maxX, maxY, minX + var isYAxis = [true, false, true, false]; // minY=Y, maxX=X, maxY=Y, minX=X + + var corners = [ + [0, 0], [1, 0], [1, 1], [0, 1] + ]; + var result = null; + + var mainMappings = [27, 57, 23, 53, 43, 9, 10, 11, 14, 37, 31, 56, 40, 41, 47]; + + for (let i = -mainMappings.length; i < 256; i++) { + var index = i < 0 ? mainMappings[mainMappings.length + i] : i; + // Decode i into 4 corner indices (base 4) + var mappingIdx = [ + (index >> 0) & 3, + (index >> 2) & 3, + (index >> 4) & 3, + (index >> 6) & 3 + ]; + + var mapping = mappingIdx.map(idx => corners[idx]); + + // Build the 4x5 linear system for [x0, y0, w, h] + var rows = mapping.map((c, j) => { + var [dx, dy] = c; + var target = targets[j]; + var m = isYAxis[j] ? M.slice(3, 6) : M.slice(0, 3); + var mp = M.slice(6, 9); + + // Equation: (m0 - T*m6)x0 + (m1 - T*m7)y0 + dx(m0 - T*m6)w + dy(m1 - T*m7)h = T*m8 - m2 + var ax = m[0] - target * mp[0]; + var ay = m[1] - target * mp[1]; + + return [ax, ay, dx * ax, dy * ay, target * mp[2] - m[2]]; + }); + + var res = solve4x4(rows); + var result; + if (res) { + var [x0, y0, w, h] = res; + if (w < 0) { x0 += w; w = -w; } + if (h < 0) { y0 += h; h = -h; } + if (validateSolution(M, x0, y0, w, h, bbox)) { + result = { left: x0, top: y0, width: w, height: h, mappingIdx: i }; + break; + } + } + } + if (!result) + console.warn("No valid mapping found in 256 combinations."); + return result; +} + +const invert3x3 = (m) => { + var [a, b, c, d, e, f, g, h, i] = m; + var det = a * (e * i - f * h) - b * (d * i - f * g) + c * (d * h - e * g); + if (Math.abs(det) < 1e-14) return null; + var invDet = 1 / det; + return [ + (e * i - f * h) * invDet, (c * h - b * i) * invDet, (b * f - c * e) * invDet, + (f * g - d * i) * invDet, (a * i - c * g) * invDet, (c * d - a * f) * invDet, + (d * h - e * g) * invDet, (g * b - a * h) * invDet, (a * e - b * d) * invDet + ]; +}; +var project = (m, px, py) => { + var k = m[6] * px + m[7] * py + m[8]; + return [(m[0] * px + m[1] * py + m[2]) / k, (m[3] * px + m[4] * py + m[5]) / k]; +}; + +/** + * Forward projects the 4 corners of the solution and checks if the + * resulting BBox matches the input bbox. + */ +function validateSolution(M, x, y, w, h, targetBbox) { + var pts = [ + [x, y], [x + w, y], [x + w, y + h], [x, y + h] + ]; + + return pts.every(p => { + var mapped = project(M, p[0], p[1]) + return targetBbox.left - 0.5 <= mapped[0] && mapped[0] <= targetBbox.left + targetBbox.width + 0.5 + && targetBbox.top - 0.5 <= mapped[1] && mapped[1] <= targetBbox.top + targetBbox.height + 0.5 + }); +} + +function solve4x4(m) { + let n = 4; + for (let i = 0; i < n; i++) { + let max = i; + for (let j = i + 1; j < n; j++) + if (Math.abs(m[j][i]) > Math.abs(m[max][i])) max = j; + [m[i], m[max]] = [m[max], m[i]]; + let p = m[i][i]; + if (Math.abs(p) < 1e-10) return null; + for (let j = i; j <= n; j++) m[i][j] /= p; + for (let k = 0; k < n; k++) { + if (k !== i) { + let f = m[k][i]; + for (let j = i; j <= n; j++) m[k][j] -= f * m[i][j]; + } + } + } + return m.map(row => row[n]); +} \ No newline at end of file diff --git a/src/layer/lines.js b/src/layer/lines.js index ae0cb71e262..98ed03a62ab 100644 --- a/src/layer/lines.js +++ b/src/layer/lines.js @@ -50,6 +50,25 @@ class Lines { return lineTop - (screenPage * this.canvasHeight); } + $getCellByScreenRow(screenRow, config) { + var screenTop = config.firstRowScreen * config.lineHeight; + var screenPage = Math.floor(screenTop / this.canvasHeight); + var lineTop = screenRow * config.lineHeight - (screenPage * this.canvasHeight); + for (var i = this.cells.length -1; i >= 0; i--) { + var cell = this.cells[i]; + var top = parseInt(cell.element.style.top); + var height = parseInt(cell.element.style.height); + if (top <= lineTop) { + if (top + height < lineTop) { + return null; + } + var offset = lineTop - top; + return {cell, offset}; + } + } + return null; + } + /** * @param {number} row * @param {LayerConfig} config diff --git a/src/layer/marker.js b/src/layer/marker.js index 3307c7b89b9..3399294b983 100644 --- a/src/layer/marker.js +++ b/src/layer/marker.js @@ -64,6 +64,8 @@ class Marker { this.config = config; + this.element.style.display = "none"; + this.i = 0; var html; for (var key in this.markers) { @@ -80,7 +82,7 @@ class Marker { range = range.toScreenRange(this.session); if (marker.renderer) { var top = this.$getTop(range.start.row, config); - var left = this.$padding + range.start.column * config.characterWidth; + var left = this.$padding + config.fontMetrics.textWidth(range.start.row, range.start.column); marker.renderer(html, range, left, top, config); } else if (marker.type == "fullLine") { this.drawFullLineMarker(html, range, marker.clazz, config); @@ -99,6 +101,8 @@ class Marker { while (this.i < this.element.childElementCount) this.element.removeChild(this.element.lastChild); } + + this.element.style.display = ""; } /** @@ -154,7 +158,7 @@ class Marker { var padding = this.$padding; var height = config.lineHeight; var top = this.$getTop(range.start.row, config); - var left = padding + range.start.column * config.characterWidth; + var left = padding + config.fontMetrics.textWidth(range.start.row, range.start.column); extraStyle = extraStyle || ""; if (this.session.$bidiHandler.isBidiRow(range.start.row)) { @@ -176,7 +180,7 @@ class Marker { this.drawBidiSingleLineMarker(stringBuilder, range1, clazz + " ace_br12", config, null, extraStyle); } else { top = this.$getTop(range.end.row, config); - var width = range.end.column * config.characterWidth; + var width = config.fontMetrics.textWidth(range.end.row, range.end.column); this.elt( clazz + " ace_br12", @@ -216,17 +220,17 @@ class Marker { if (this.session.$bidiHandler.isBidiRow(range.start.row)) return this.drawBidiSingleLineMarker(stringBuilder, range, clazz, config, extraLength, extraStyle); var height = config.lineHeight; - var width = (range.end.column + (extraLength || 0) - range.start.column) * config.characterWidth; + var right = config.fontMetrics.textWidth(range.start.row, range.end.column) + (extraLength || 0) * config.characterWidth; var top = this.$getTop(range.start.row, config); - var left = this.$padding + range.start.column * config.characterWidth; + var left = config.fontMetrics.textWidth(range.start.row, range.start.column); this.elt( clazz, "height:"+ height+ "px;"+ - "width:"+ width+ "px;"+ + "width:"+ (right-left)+ "px;"+ "top:"+ top+ "px;"+ - "left:"+ left+ "px;"+ (extraStyle || "") + "left:"+ (this.$padding + left)+ "px;"+ (extraStyle || "") ); } @@ -241,15 +245,14 @@ class Marker { */ drawBidiSingleLineMarker(stringBuilder, range, clazz, config, extraLength, extraStyle) { var height = config.lineHeight, top = this.$getTop(range.start.row, config), padding = this.$padding; - var selections = this.session.$bidiHandler.getSelections(range.start.column, range.end.column); - - selections.forEach(function(selection) { + var rects = this.config.fontMetrics.getRects(range.start, range.end); + rects.forEach(function(rect) { this.elt( clazz, "height:" + height + "px;" + - "width:" + (selection.width + (extraLength || 0)) + "px;" + + "width:" + (rect.width + (extraLength || 0)) + "px;" + "top:" + top + "px;" + - "left:" + (padding + selection.left) + "px;" + (extraStyle || "") + "left:" + (padding + rect.left) + "px;" + (extraStyle || "") ); }, this); } diff --git a/src/layer/text.js b/src/layer/text.js index 5d0df50e2c0..04915ca47af 100644 --- a/src/layer/text.js +++ b/src/layer/text.js @@ -350,7 +350,7 @@ class Text { $renderToken(parent, screenColumn, token, value) { var self = this; - var re = /(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC\u2066\u2067\u2068\u202A\u202B\u202D\u202E\u202C\u2069\u2060\u2061\u2062\u2063\u2064\u206A\u206B\u206B\u206C\u206D\u206E\u206F]+)|(\u3000)|([\u1100-\u115F\u11A3-\u11A7\u11FA-\u11FF\u2329-\u232A\u2E80-\u2E99\u2E9B-\u2EF3\u2F00-\u2FD5\u2FF0-\u2FFB\u3001-\u303E\u3041-\u3096\u3099-\u30FF\u3105-\u312D\u3131-\u318E\u3190-\u31BA\u31C0-\u31E3\u31F0-\u321E\u3220-\u3247\u3250-\u32FE\u3300-\u4DBF\u4E00-\uA48C\uA490-\uA4C6\uA960-\uA97C\uAC00-\uD7A3\uD7B0-\uD7C6\uD7CB-\uD7FB\uF900-\uFAFF\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE66\uFE68-\uFE6B\uFF01-\uFF60\uFFE0-\uFFE6]|[\uD800-\uDBFF][\uDC00-\uDFFF])/g; + var re = /(\t)|( +)|([\x00-\x1f\x80-\xa0\xad\u1680\u180E\u2000-\u200f\u2028\u2029\u202F\u205F\uFEFF\uFFF9-\uFFFC\u2066\u2067\u2068\u202A\u202B\u202D\u202E\u202C\u2069\u2060\u2061\u2062\u2063\u2064\u206A\u206B\u206B\u206C\u206D\u206E\u206F]+)|(\u3000+)/g; var valueFragment = this.dom.createFragment(this.element); @@ -361,7 +361,6 @@ class Text { var simpleSpace = m[2]; var controlCharacter = m[3]; var cjkSpace = m[4]; - var cjk = m[5]; if (!self.showSpaces && simpleSpace) continue; @@ -395,21 +394,14 @@ class Text { span.textContent = lang.stringRepeat(self.SPACE_CHAR, controlCharacter.length); valueFragment.appendChild(span); } else if (cjkSpace) { - // U+3000 is both invisible AND full-width, so must be handled uniquely - screenColumn += 1; - - var span = this.dom.createElement("span"); - span.style.width = (self.config.characterWidth * 2) + "px"; - span.className = self.showSpaces ? "ace_cjk ace_invisible ace_invisible_space" : "ace_cjk"; - span.textContent = self.showSpaces ? self.SPACE_CHAR : cjkSpace; - valueFragment.appendChild(span); - } else if (cjk) { - screenColumn += 1; - var span = this.dom.createElement("span"); - span.style.width = (self.config.characterWidth * 2) + "px"; - span.className = "ace_cjk"; - span.textContent = cjk; - valueFragment.appendChild(span); + if (self.showSpaces) { + var span = this.dom.createElement("span"); + span.className = "ace_invisible ace_invisible_space"; + span.textContent = lang.stringRepeat(self.CJK_SPACE_CHAR, cjkSpace.length); + valueFragment.appendChild(span); + } else { + valueFragment.appendChild(this.dom.createTextNode(cjkSpace, this.element)); + } } } @@ -785,6 +777,7 @@ Text.prototype.EOL_CHAR_CRLF = "\xa4"; Text.prototype.EOL_CHAR = Text.prototype.EOL_CHAR_LF; Text.prototype.TAB_CHAR = "\u2014"; //"\u21E5"; Text.prototype.SPACE_CHAR = "\xB7"; +Text.prototype.CJK_SPACE_CHAR = "\u30FB"; Text.prototype.$padding = 0; Text.prototype.MAX_LINE_LENGTH = 10000; Text.prototype.showInvisibles = false; diff --git a/src/layer/text_markers.js b/src/layer/text_markers.js index 79307b1d290..2256a8ce077 100644 --- a/src/layer/text_markers.js +++ b/src/layer/text_markers.js @@ -177,8 +177,9 @@ var textMarkerMixin = { if (/^\s+$/.test(segment)) { span = this.dom.createElement("span"); span.className = marker.className; - var symbol = node["charCount"] ? this.TAB_CHAR : this.SPACE_CHAR; - span.textContent = lang.stringRepeat(symbol, segment.length); + span.textContent = + node["charCount"] ? this.TAB_CHAR.repeat(segment.length) + : segment.replace(/\u3000/g, this.CJK_SPACE_CHAR).replace(/ /g, this.SPACE_CHAR); span.setAttribute("data-whitespace", segment); fragment.appendChild(span); } diff --git a/src/layer/text_markers_test.js b/src/layer/text_markers_test.js index ddb73c99fb0..1a75379e17b 100644 --- a/src/layer/text_markers_test.js +++ b/src/layer/text_markers_test.js @@ -152,10 +152,7 @@ module.exports = { }); assert.equal(markedText, "试function测"); - var result = normalize(` - function - - `); + var result = normalize(`试function测试`); var actual = normalize(this.textLayer.element.childNodes[0].innerHTML); assert.equal(actual, result); }, diff --git a/src/layer/text_test.js b/src/layer/text_test.js index dd957321c31..7d3a782552a 100644 --- a/src/layer/text_test.js +++ b/src/layer/text_test.js @@ -42,13 +42,13 @@ module.exports = { var parent = dom.createElement("div"); this.textLayer.$renderLine(parent, 0); - assert.domNode(parent, ["div", {}, ["span", {class: "ace_cjk", style: "width: 20px;"}, "\u3000"]]); + assert.domNode(parent, ["div", {}, "\u3000"]); this.textLayer.setShowInvisibles(true); var parent = dom.createElement("div"); this.textLayer.$renderLine(parent, 0); assert.domNode(parent, ["div", {}, - ["span", {class: "ace_cjk ace_invisible ace_invisible_space", style: "width: 20px;"}, this.textLayer.SPACE_CHAR], + ["span", {class: "ace_invisible ace_invisible_space"}, this.textLayer.CJK_SPACE_CHAR], ["span", {class: "ace_invisible ace_invisible_eol"}, "\xB6"] ]); }, diff --git a/src/mouse/default_gutter_handler_test.js b/src/mouse/default_gutter_handler_test.js index f27d9c6c9bf..ec508519ae8 100644 --- a/src/mouse/default_gutter_handler_test.js +++ b/src/mouse/default_gutter_handler_test.js @@ -195,8 +195,8 @@ module.exports = { var lines = editor.renderer.$gutterLayer.$lines; assert.equal(lines.cells[1].element.textContent, "2"); var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(new MouseEvent("click", {x: rect.left, y: rect.top})); editor.renderer.$loop._flush(); assert.ok(/ace_closed/.test(toggler.className)); @@ -230,8 +230,8 @@ module.exports = { var lines = editor.renderer.$gutterLayer.$lines; assert.equal(lines.cells[1].element.textContent, "2"); var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(new MouseEvent("click", {x: rect.left, y: rect.top})); editor.renderer.$loop._flush(); assert.ok(/ace_closed/.test(toggler.className)); @@ -265,8 +265,8 @@ module.exports = { var lines = editor.renderer.$gutterLayer.$lines; assert.equal(lines.cells[1].element.textContent, "2"); var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(new MouseEvent("click", {x: rect.left, y: rect.top})); editor.renderer.$loop._flush(); assert.ok(/ace_closed/.test(toggler.className)); @@ -300,8 +300,8 @@ module.exports = { var lines = editor.renderer.$gutterLayer.$lines; assert.equal(lines.cells[1].element.textContent, "2"); var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(new MouseEvent("click", {x: rect.left, y: rect.top})); editor.renderer.$loop._flush(); assert.ok(/ace_closed/.test(toggler.className)); @@ -325,8 +325,8 @@ module.exports = { var lines = editor.renderer.$gutterLayer.$lines; assert.equal(lines.cells[1].element.textContent, "2"); var toggler = lines.cells[0].element.querySelector(".ace_fold-widget"); + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(new MouseEvent("click", {x: rect.left, y: rect.top})); editor.renderer.$loop._flush(); assert.ok(/ace_closed/.test(toggler.className)); @@ -356,8 +356,8 @@ module.exports = { assert.equal(lines.cells[1].element.textContent, "2"); var firstLineGutterElement = lines.cells[0].element; var toggler = firstLineGutterElement.querySelector(".ace_fold-widget"); + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(new MouseEvent("click", {x: rect.left, y: rect.top})); editor.renderer.$loop._flush(); assert.ok(/ace_closed/.test(toggler.className)); diff --git a/src/mouse/mouse_handler_test.js b/src/mouse/mouse_handler_test.js index bdbd662e0da..0cc6bd6ca1b 100644 --- a/src/mouse/mouse_handler_test.js +++ b/src/mouse/mouse_handler_test.js @@ -126,8 +126,8 @@ module.exports = { editor.renderer.$loop._flush(); var lines = editor.renderer.$gutterLayer.$lines; var toggler = lines.cells[0].element.childNodes[1]; + toggler.style.leftHint = 100; // mockdom doesn't parse css to know the padding var rect = toggler.getBoundingClientRect(); - if (!rect.left) rect.left = 100; // for mockdom toggler.dispatchEvent(MouseEvent("down", {x: rect.left, y: rect.top})); toggler.dispatchEvent(MouseEvent("up", {x: rect.left, y: rect.top})); toggler.dispatchEvent(MouseEvent("click", {x: rect.left, y: rect.top})); @@ -166,7 +166,7 @@ module.exports = { editor.renderer.$loop._flush(); assert.position(editor.getCursorPosition(), 1, 0); - toggler.dispatchEvent(MouseEvent("up", {x: rect.left, y: rect.top + rect.height})); + toggler.dispatchEvent(MouseEvent("up", {x: rect.left, y: rect.top + rect.height - 1})); editor.renderer.$loop._flush(); assert.position(editor.getCursorPosition(), 2, 0); }, diff --git a/src/range_test.js b/src/range_test.js index 41c7b082798..084af4666bf 100644 --- a/src/range_test.js +++ b/src/range_test.js @@ -142,7 +142,7 @@ module.exports = { assert.range(range.toScreenRange(session), 1, 1, 1, 4); var range = new Range(2, 1, 2, 2); - assert.range(range.toScreenRange(session), 2, 2, 2, 4); + assert.range(range.toScreenRange(session), 2, 1, 2, 2); var range = new Range(3, 0, 3, 4); assert.range(range.toScreenRange(session), 3, 0, 3, 10); diff --git a/src/test/mockdom.js b/src/test/mockdom.js index 3c1cfa20f9a..33d2e91e03a 100644 --- a/src/test/mockdom.js +++ b/src/test/mockdom.js @@ -177,7 +177,7 @@ function Node(name) { (function() { this.nodeType = 1; this.ELEMENT_NODE = 1; - this.TEXT_NODE = 1; + this.TEXT_NODE = 3; this.cloneNode = function(recursive) { var clone = new Node(this.localName); for (var i in this.$attributes) { @@ -236,9 +236,11 @@ function Node(name) { if (node.previousSibling) node.previousSibling.nextSibling = node; node.parentNode = this; - i = this.children.indexOf(before); - if (i == -1) i = this.children.length + 1; - this.children.splice(i, 0, node); + if (node.nodeType == 1) { + i = this.children.indexOf(before); + if (i == -1) i = this.children.length + 1; + this.children.splice(i, 0, node); + } } return node; @@ -412,7 +414,14 @@ function Node(name) { if (position === "afterbegin") this.insertBefore(element, this.firstChild); if (position === "beforebegin") this.parentElement.insertBefore(element, this); }; - this.getBoundingClientRect = function(fromChild) { + this.getClientRects = function() { + var rect = this.getBoundingClientRect(); + return [rect]; + }; + this.getBoundingClientRect = function(fromChild, ignoreTransforms) { + function textWidth(str) { + return str.replace(/\t/g, " ").replace(/[\u3041-\u9FBF]/g, " ").length * CHAR_WIDTH; + } var width = 0; var height = 0; var top = 0; @@ -421,29 +430,56 @@ function Node(name) { width = WINDOW_WIDTH; height = WINDOW_HEIGHT; } - else if (!document.contains(this) || this.style.display == "none") { + else if (!document.contains(this) || this.style?.display == "none") { width = height = 0; } - else if (this.style.width == "auto" || this.localName == "span" || /^inline/.test(this.style.display)) { - width = this.textContent.length * CHAR_WIDTH; + else if (this.nodeType == 3 || this.localName == "span" || /^inline/.test(this.style.display)) { + width = textWidth(this.textContent); var node = this; + var blockParent; while (node) { - if (node.style.fontSize) { + if (node.style?.fontSize) { height = parseInt(node.style.fontSize); break; } + if ( + !blockParent && node != this + && (node.style?.display == "block" || /div|body|html/.test(node.localName)) + ) + blockParent = node; node = node.parentNode; } if (!height) height = CHAR_HEIGHT; + if (this.style?.leftHint) { + left = this.style.leftHint; + } else if (blockParent && (this.localName == "span" || /^inline/.test(this.style?.display) || this.nodeType == 3)) { + var parentRect = blockParent.getBoundingClientRect(true, true); + top = parentRect.top; + node = this; + left = parentRect.left; + while (node && node != blockParent) { + if (node.previousSibling) { + var text = node.previousSibling.textContent + .replace(/\t/g, " ") + .replace(/[\u3041-\u9FBF]/g, " "); + left += textWidth(text); + node = node.previousSibling; + } else { + node = node.parentNode; + } + } + } } else if (this.parentNode) { var isFixed = this.style.position == "fixed" || this.style.positionHint == "fixed" || this.getAttribute("role") == "tooltip"; + var isAbsolute = this.style.position == "absolute" + || this.style.positionHint == "absolute"; // prevent recursion by passing -1 var rect = fromChild == -1 || isFixed ? {top: 0, left: 0, width: 0, height: 0, right: 0, bottom: 0} - : this.parentNode.getBoundingClientRect(); + : this.parentNode.getBoundingClientRect(undefined, true); if (isFixed) { rect.height = rect.bottom = WINDOW_HEIGHT; rect.width = rect.right = WINDOW_WIDTH; @@ -451,6 +487,14 @@ function Node(name) { left = parseCssLength(this.style.left || "0", rect.width); top = parseCssLength(this.style.top || "0", rect.height); + + var margin = this.style.margin; + if (margin) { + var parts = margin.trim().split(/\s+/); + left += parseCssLength(parts[3] || parts[1] || parts[0] || "0", 0); + top += parseCssLength(parts[0] || parts[1] || "0", 0); + } + var right = parseCssLength(this.style.right || "0", rect.width); var bottom = parseCssLength(this.style.bottom || "0", rect.width); @@ -458,16 +502,31 @@ function Node(name) { width = parseCssLength(this.style.width || "100%", rect.width); else if (this.style.widthHint) width = this.style.widthHint; - else + else if (this.style.right || !isAbsolute) width = rect.width - right - left; if (this.style.height) height = parseCssLength(this.style.height || "100%", rect.height); else if (this.style.heightHint) height = this.style.heightHint; - else + else if (this.style.bottom || !isAbsolute) height = rect.height - top - bottom; + if (this.style.width == "auto") { + width = textWidth(this.textContent); + if ((!this.style.height || this.style.height == "auto") && this.textContent.trim()) { + height = CHAR_HEIGHT; + var node = this; + while (node) { + if (node.style?.fontSize) { + height = parseInt(node.style.fontSize); + break; + } + node = node.parentNode; + } + } + } + var maxWidth = this.style.maxWidth && parseCssLength(this.style.maxWidth, rect.width); var maxHeight = this.style.maxHeight && parseCssLength(this.style.maxHeight, rect.height); @@ -475,7 +534,7 @@ function Node(name) { if (maxHeight >= 0) height = Math.min(height, maxHeight); if (!height && !this.style.height && this.firstChild && this.firstChild.getBoundingClientRect && !fromChild) { - height = this.firstChild.getBoundingClientRect(-1).height; + height = this.firstChild.getBoundingClientRect(-1, true).height; } if (!this.style.left && this.style.right) { @@ -488,6 +547,50 @@ function Node(name) { top += rect.top; left += rect.left; } + if (!ignoreTransforms) { + // Apply any CSS transforms + var node = this; + var M = [1, 0, 0, 0, 1, 0, 0, 0, 1]; + var points; + while (node) { + if (node.style?.transform) { + var match = node.style.transform.match(/matrix3d\(([^)]+)\)/) + if (match) { + if (!points) { + points = [[left, top], [left + width, top], [left, top + height], [left + width, top + height]]; + } + var v = match[1].split(",").map(parseFloat); + M = [v[0], v[1], v[3], v[4], v[5], v[7], v[12], v[13], v[15]]; + var origin = node.style.transformOrigin || "50% 50%"; + var parts = origin.split(" "); + var parentRect = node== this ? { top,left,width,height } : node.getBoundingClientRect(true, true); + var ox = parseCssLength(parts[0], parentRect.width) + parentRect.left; + var oy = parseCssLength(parts[1], parentRect.height) + parentRect.top; + var O = [ox, oy]; + points = points.map(p => project(p, O)); + } + } + node = node.parentNode; + } + function project(p, O) { + var x = p[0] - O[0]; + var y = p[1] - O[1]; + var w = M[2] * x + M[5] * y + M[8]; + return [ + (M[0] * x + M[3] * y + M[6]) / w + O[0], + (M[1] * x + M[4] * y + M[7]) / w + O[1] + ]; + } + if (points) { + var xs = points.map(p => p[0]); + var ys = points.map(p => p[1]); + left = Math.min.apply(null, xs); + top = Math.min.apply(null, ys); + width = Math.max.apply(null, xs) - left; + height = Math.max.apply(null, ys) - top; + } + } + return {top: top, left: left, width: width, height: height, right: left + width, bottom: top + height}; }; @@ -500,16 +603,16 @@ function Node(name) { } this.__defineGetter__("clientHeight", function() { - return this.getBoundingClientRect().height; + return this.getBoundingClientRect(undefined, true).height; }); this.__defineGetter__("clientWidth", function() { - return this.getBoundingClientRect().width; + return this.getBoundingClientRect(undefined, true).width; }); this.__defineGetter__("offsetHeight", function() { - return this.getBoundingClientRect().height; + return this.getBoundingClientRect(undefined, true).height; }); this.__defineGetter__("offsetWidth", function() { - return this.getBoundingClientRect().width; + return this.getBoundingClientRect(undefined, true).width; }); this.__defineGetter__("lastChild", function() { @@ -609,6 +712,7 @@ function Node(name) { node.parentNode = null; }); node.childNodes.length = 0; + node.children.length = 0; if (!document.contains(document.activeElement)) document.activeElement = document.body; } @@ -803,7 +907,7 @@ function TextNode(value) { (function() { this.nodeType = 3; this.ELEMENT_NODE = 1; - this.TEXT_NODE = 1; + this.TEXT_NODE = 3; this.cloneNode = function() { return new TextNode(this.data); }; @@ -815,6 +919,9 @@ function TextNode(value) { }); }).call(TextNode.prototype); +Node.ELEMENT_NODE = TextNode.ELEMENT_NODE = 1; +Node.TEXT_NODE = TextNode.TEXT_NODE = 3; + var window = { get innerHeight() { return WINDOW_HEIGHT; @@ -844,6 +951,69 @@ window.HTMLDocument = window.XMLDocument = window.Document = function() { document.createDocumentFragment = function() { return new Node("#fragment"); }; + document.createTreeWalker = function(root, whatToShow, filter) { + var nodes = []; + walk(root, function(node) { + if ((whatToShow & (1 << (node.nodeType - 1))) && (!filter || filter.acceptNode(node) == 1)) + nodes.push(node); + }); + var index = -1; + return { + nextNode: function() { + if (index < nodes.length - 1) + return this.currentNode = nodes[++index]; + }, + previousNode: function() { + if (index > 0) + return this.currentNode = nodes[--index]; + } + }; + }; + document.createRange = function() { + return { + setStart: function(node, offset) { + this.startContainer = node; + this.startOffset = offset; + }, + setEnd: function(node, offset) { + this.endContainer = node; + this.endOffset = offset; + }, + getBoundingClientRect: function() { + var rect1 = Element.prototype.getBoundingClientRect.call(this.startContainer); + if (this.startContainer.nodeType == 3) { + rect1.left += this.startOffset * CHAR_WIDTH; + } else { + var child = this.startContainer.childNodes[this.startOffset]; + if (!child) { + rect1.left = rect1.right; + } else { + rect1.left = Element.prototype.getBoundingClientRect.call(child).left; + } + } + var rect2 = Element.prototype.getBoundingClientRect.call(this.endContainer); + if (this.endContainer.nodeType == 3) { + rect2.right = rect2.left + this.endOffset * CHAR_WIDTH; + } else { + var child = this.endContainer.childNodes[this.endOffset]; + if (child) { + rect2.right = Element.prototype.getBoundingClientRect.call(child).right; + } + } + return { + top: rect1.top, + left: rect1.left, + width: rect2.right - rect1.left, + height: rect2.bottom - rect1.top, + right: rect2.right, + bottom: rect2.bottom + }; + }, + getClientRects: function() { + return [this.getBoundingClientRect()]; + }, + }; + }; document.hasFocus = function() { return true; }; @@ -881,6 +1051,12 @@ window.DOMParser = function() { }; }; +window.NodeFilter = { + SHOW_ALL: 0xFFFFFFFF, + SHOW_ELEMENT: 1, + SHOW_TEXT: 4 +}; + var document = new window.Document(); window.__defineGetter__("document", function() {return document;}); window.document.defaultView = window; diff --git a/src/test/mockdom_test.js b/src/test/mockdom_test.js index d8f22227346..e0d73795ece 100644 --- a/src/test/mockdom_test.js +++ b/src/test/mockdom_test.js @@ -9,13 +9,18 @@ if (typeof process !== "undefined") { var assert = require("./assertions"); module.exports = { + tearDown: function() { + document.body.innerHTML = ""; + }, "test: selectors": function() { - document.body.innerHTML = `
+ document.body.insertAdjacentHTML("afterbegin", `
span1 xxx some text -
`; +
`); + var div = document.querySelector("div[x]"); + var spans = document.querySelectorAll("span"); assert.equal(spans[0].matches("[z=dd]"), true); assert.equal(spans[0].matches("[z=dde]"), false); @@ -28,6 +33,7 @@ module.exports = { assert.equal(document.querySelectorAll("html * * [x]").length, 1); assert.equal(document.querySelectorAll(" * * * * [x]").length, 0); + div.remove(); }, "test: getBoundingClientRect" : function() { var span = document.createElement("span"); @@ -50,6 +56,7 @@ module.exports = { var div = document.createElement("div"); document.body.appendChild(div); + div.style.background = "red"; div.style.position = "absolute"; div.style.top = "20px"; div.style.left = "40px"; @@ -60,9 +67,9 @@ module.exports = { assert.ok(parentWidth != 0); assert.equal(rect.top, 20); assert.equal(rect.left, 40); - assert.equal(rect.width, parentWidth * (1 - 0.12) - 40); + assert.equal(Math.round(rect.width), Math.round(parentWidth * (1 - 0.12) - 40)); assert.equal(rect.height, window.innerHeight - 40); - assert.equal(rect.right, parentWidth * (1 - 0.12)); + assert.equal(Math.round(rect.right), Math.round(parentWidth * (1 - 0.12))); assert.equal(rect.bottom, window.innerHeight - 20); div.style.width = "40px"; @@ -74,8 +81,117 @@ module.exports = { rect = div.getBoundingClientRect(); assert.equal(rect.height, window.innerHeight * 1.5); }, + "test: getBoundingClientRect with transform" : function() { + var parent = document.createElement("div"); + parent.style.position = "absolute"; + parent.style.top = "50px"; + parent.style.left = "400px"; + parent.style.width = "200px"; + parent.style.height = "200px"; + parent.style.background = "yellow"; + document.body.appendChild(parent); + + var div = document.createElement("div"); + parent.appendChild(div); + div.style.transformOrigin = "0 0"; + div.style.background = "red"; + div.style.position = "absolute"; + div.style.top = "30px"; + div.style.left = "40px"; + div.style.width = "100px"; + div.style.height = "100px"; + var expected = { + left: 440, + top: 80, + width: 100, + height: 100, + }; + var rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + div.style.transform = "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -100, -10, 0, 1)"; + expected.left += -100; + expected.top += -10; + rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + div.style.transform = "matrix3d(0.5, 0, 0, 0, 0, 0.8, 0, 0, 0, 0, 1, 0, -100, -10, 0, 1)"; + expected.width *= 0.5; + expected.height *= 0.8; + rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + parent.style.transform = "matrix3d(1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, -100, -10, 0, 1)"; + parent.style.transformOrigin = "0 0"; + expected.left += -100; + expected.top += -10; + rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + parent.style.transform = "matrix3d(0.5, 0, 0, 0, 0, 0.5, 0, 0, 0, 0, 1, 0, -100, -20, 0, 1)"; + expected.width *= 0.5; + expected.height *= 0.5; + expected.left = (400 - 100) + (40-100) / 2; + expected.top = (50 - 20) + (30-10) / 2; + rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + + parent.style.transform = ""; + div.style.transform = "matrix3d(0.7, 0.3, 0, -0.00066, 0, 0.82, 0, -0.001, 0, 0, 1, 0, -100, -10, 10, 1)"; + expected = { + left: 328.8888854980469, + top: 70, + width: 78.9912109375, + height: 132.30215454101562, + }; + rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + + parent.style.transform = "matrix3d(0.7, 0.3, 0, -0.00066, 0, 0.82, 0, -0.001, 0, 0, 1, 0, -100, -20, 10, 1)"; + expected = { + left: 240.14041137695312, + top: 28.815221786499023, + width: 59.70550537109375, + height: 146.73687744140625, + }; + rect = div.getBoundingClientRect(); + assertRect(rect, expected); + + function assertRect(rect, expected) { + for (var key in expected) { + assert.equal(Math.round(rect[key]), Math.round(expected[key])); + } + } + }, - "test: eventListener" : function() { + "test: getBoundingClientRect for inline elements": function() { + var div = document.createElement("div"); + div.style.position = "absolute"; + div.style.fontFamily = "monospace"; + div.style.top = "20px"; + div.style.left = "40px"; + document.body.appendChild(div); + + div.innerHTML = "\tぁ-a defxyz"; + var span1 = div.children[0]; + var span2 = div.children[1]; + var span3 = span2.children[0]; + + var rect1 = span1.getBoundingClientRect(); + var rect2 = span2.getBoundingClientRect(); + var rect3 = span3.getBoundingClientRect(); + + assert.equal((rect3.left - rect2.left) / rect1.width, 3); + + var range = document.createRange(); + range.setStart(span1.firstChild, 1); + range.setEnd(span2.firstChild, 1); + var rect = range.getBoundingClientRect(); + assert.equal(rect.left, rect1.left + rect1.width); + }, + "test: eventListener" : function() { var div = document.createElement("div"); document.body.appendChild(div); diff --git a/src/virtual_renderer.js b/src/virtual_renderer.js index 88ce5c728f1..faa5a2df75f 100644 --- a/src/virtual_renderer.js +++ b/src/virtual_renderer.js @@ -98,8 +98,7 @@ class VirtualRenderer { column : 0 }; - this.$fontMetrics = new FontMetrics(this.container); - this.$textLayer.$setFontMetrics(this.$fontMetrics); + this.$fontMetrics = new FontMetrics(this.container, this.$textLayer, this); this.$textLayer.on("changeCharacterSize", function(e) { _self.updateCharacterSize(); _self.onResize(true, _self.gutterWidth, _self.$size.width, _self.$size.height); @@ -122,12 +121,14 @@ class VirtualRenderer { lastRow : 0, lineHeight : 0, characterWidth : 0, + fontMetrics: this.$fontMetrics, minHeight : 1, maxHeight : 1, offset : 0, height : 1, gutterOffset: 1 }; + this.$fontMetrics.config = this.layerConfig; this.scrollMargin = { left: 0, @@ -707,7 +708,7 @@ class VirtualRenderer { else { if (composition.useTextareaForIME) { var val = this.textarea.value; - w = this.characterWidth * (this.session.$getStringScreenWidth(val)[0]); + w = this.$fontMetrics.getTextWidth(val) + 1; } else { posTop += this.lineHeight + 2; @@ -911,9 +912,6 @@ class VirtualRenderer { this._signal("beforeRender", changes); - if (this.session && this.session.$bidiHandler) - this.session.$bidiHandler.updateCharacterWidths(this.$fontMetrics); - var config = this.layerConfig; // text, scrolling and resize changes can cause the view port size to change if (changes & this.CHANGE_FULL || @@ -1188,12 +1186,14 @@ class VirtualRenderer { lastRow : lastRow, lineHeight : lineHeight, characterWidth : this.characterWidth, + fontMetrics: this.$fontMetrics, minHeight : minHeight, maxHeight : maxHeight, offset : offset, gutterOffset : lineHeight ? Math.max(0, Math.ceil((offset + size.height - size.scrollerHeight) / lineHeight)) : 0, height : this.$size.scrollerHeight }; + this.$fontMetrics.config = this.layerConfig; if (this.session.$bidiHandler) this.session.$bidiHandler.setContentWidth(longestLine - this.$padding); @@ -1634,19 +1634,21 @@ class VirtualRenderer { pixelToScreenCoordinates(x, y) { var canvasPos; if (this.$hasCssTransforms) { - canvasPos = {top:0, left: 0}; + canvasPos = {top: this.margin.top, left: this.gutterWidth + this.margin.left}; var p = this.$fontMetrics.transformCoordinates([x, y]); - x = p[1] - this.gutterWidth - this.margin.left; - y = p[0]; + x = p[0]; + y = p[1]; } else { canvasPos = this.scroller.getBoundingClientRect(); } var offsetX = x + this.scrollLeft - canvasPos.left - this.$padding; var offset = offsetX / this.characterWidth; - var row = Math.floor((y + this.scrollTop - canvasPos.top) / this.lineHeight); + var row = (y + this.scrollTop - canvasPos.top) / this.lineHeight; var col = this.$blockCursor ? Math.floor(offset) : Math.round(offset); + col = this.$fontMetrics.$pixelToColumn(row, col, x, this.$blockCursor); + return {row: row, column: col, side: offset - col > 0 ? 1 : -1, offsetX: offsetX}; } @@ -1658,23 +1660,8 @@ class VirtualRenderer { */ screenToTextCoordinates(x, y) { - var canvasPos; - if (this.$hasCssTransforms) { - canvasPos = {top:0, left: 0}; - var p = this.$fontMetrics.transformCoordinates([x, y]); - x = p[1] - this.gutterWidth - this.margin.left; - y = p[0]; - } else { - canvasPos = this.scroller.getBoundingClientRect(); - } - - var offsetX = x + this.scrollLeft - canvasPos.left - this.$padding; - var offset = offsetX / this.characterWidth; - var col = this.$blockCursor ? Math.floor(offset) : Math.round(offset); - - var row = (y + this.scrollTop - canvasPos.top) / this.lineHeight; - - return this.session.screenToDocumentPosition(row, Math.max(col, 0), offsetX); + var screenPos = this.pixelToScreenCoordinates(x, y); + return this.session.screenToDocumentPosition(screenPos.row, Math.max(screenPos.column, 0), screenPos.offsetX); } /** diff --git a/src/virtual_renderer_test.js b/src/virtual_renderer_test.js index fbf7589407f..1b6c3e3e501 100644 --- a/src/virtual_renderer_test.js +++ b/src/virtual_renderer_test.js @@ -38,6 +38,7 @@ module.exports = { el.style.top = "30px"; el.style.width = "300px"; el.style.height = "100px"; + el.style.position = "fixed"; document.body.appendChild(el); var renderer = new VirtualRenderer(el); editor = new Editor(renderer); @@ -60,60 +61,65 @@ module.exports = { assert.position(renderer.screenToTextCoordinates(x+r.left, y+r.top), row, column); } - renderer.characterWidth = 10; - renderer.lineHeight = 15; - - testPixelToText(4, 0, 0, 0); - testPixelToText(5, 0, 0, 1); - testPixelToText(9, 0, 0, 1); - testPixelToText(10, 0, 0, 1); - testPixelToText(14, 0, 0, 1); - testPixelToText(15, 0, 0, 2); + testPixelToText(renderer.characterWidth * 0.4, 0, 0, 0); + testPixelToText(renderer.characterWidth * 0.5, 0, 0, 1); + testPixelToText(renderer.characterWidth * 0.9, 0, 0, 1); + testPixelToText(renderer.characterWidth * 1.0, 0, 0, 1); + testPixelToText(renderer.characterWidth * 1.4, 0, 0, 1); + testPixelToText(renderer.characterWidth * 1.5, 0, 0, 2); }, "test: handle css transforms" : function() { + editor.setValue("hello world"); var renderer = editor.renderer; var fontMetrics = renderer.$fontMetrics; setScreenPosition(editor.container, [20, 30, 300, 100]); - var measureNode = fontMetrics.$measureNode; - setScreenPosition(measureNode, [0, 0, 10 * measureNode.textContent.length, 15]); - setScreenPosition(fontMetrics.$main, [0, 0, 10 * measureNode.textContent.length, 15]); - fontMetrics.$characterSize.width = 10; - renderer.setPadding(0); renderer.onResize(true); - assert.equal(fontMetrics.getCharacterWidth(), 1); - - renderer.characterWidth = 10; - renderer.lineHeight = 15; - - renderer.gutterWidth = 40; editor.setOption("hasCssTransforms", true); - editor.container.style.transform = "matrix3d(0.7, 0, 0, -0.00066, 0, 0.82, 0, -0.001, 0, 0, 1, 0, -100, -20, 10, 1)"; - editor.container.style.zoom = 1.5; - var pos = renderer.pixelToScreenCoordinates(100, 200); + editor.container.style.transformOrigin = "0 0"; + var H1 = -0.0007, H2 = -0.001; + var m0 = 0.7, m1 = 0.1, m2 = 0.3, m3 = 0.82; + var t1 = 100, t2 = 20; + editor.container.style.transform = `matrix3d( + ${m0}, ${m2}, 0, ${H1}, + ${m1}, ${m3}, 0, ${H2}, + 0, 0, 1, 0, + ${t1}, ${t2}, 0, 1 + )`; - var els = fontMetrics.els; - var rects = [ - [0, 0], - [-37.60084843635559, 161.62494659423828], - [114.50254130363464, -6.890693664550781], - [98.85665202140808, 179.16063690185547] - ]; - rects.forEach(function(rect, i) { - els[i].getBoundingClientRect = function() { - return { left: rect[0], top: rect[1] }; - }; - }); + var expected = [ + m0 - H1* t1, m1 - H2* t1, 0, + m2 - H1* t2, m3 - H2* t2, 0, + H1, H2, 1 + ] + function project(M, point) { + var px = point[0], py = point[1]; + var k = 1 / (M[6] * px + M[7] * py + M[8]); + return [(M[0] * px + M[1] * py + M[2]) * k, (M[3] * px + M[4] * py + M[5]) * k]; + } + + project(expected, [20, 30]); + + var transform = editor.renderer.$fontMetrics.getTransform(); + + for (var i = 0; i < 9; i++) { + assert.ok(Math.abs(transform.M[i] - expected[i]) < 10e-6, `Expected M[${i}] to be approximately ${expected[i]}, but got ${transform.M[i]}`); + } + + assert.equal(transform.t + "", [100 + 20, 20 + 30] + ""); - var r0 = els[0].getBoundingClientRect(); - pos = renderer.pixelToScreenCoordinates(r0.left + 100, r0.top + 200); - assert.position(pos, 10, 11); + var p = project(expected, [ + renderer.gutterWidth + renderer.$padding + renderer.characterWidth * 4, + renderer.lineHeight / 2 + ]); + p[0] += transform.t[0]; + p[1] += transform.t[1]; + + var pos = renderer.pixelToScreenCoordinates(p[0], p[1]); - var pos1 = fontMetrics.transformCoordinates(null, [0, 200]); - assert.ok(pos1[0] - rects[2][0] < 10e-6); - assert.ok(pos1[1] - rects[2][1] < 10e-6); - editor.renderer.$loop._flush(); + var docPos = editor.session.screenToDocumentPosition(pos.row, pos.column); + assert.position(docPos, 0, 4); }, "test scrollmargin + autosize": async function(done) {