diff --git a/src/editor.js b/src/editor.js index 887b9fcbce0..351aea03f7c 100644 --- a/src/editor.js +++ b/src/editor.js @@ -2936,7 +2936,7 @@ config.defineOptions(Editor.prototype, "editor", { this.$updatePlaceholder(); } }, - + customScrollbar: "renderer", hScrollBarAlwaysVisible: "renderer", vScrollBarAlwaysVisible: "renderer", highlightGutterLine: "renderer", diff --git a/src/ext/options.js b/src/ext/options.js index f99a02ddab4..86144039823 100644 --- a/src/ext/options.js +++ b/src/ext/options.js @@ -191,6 +191,9 @@ var optionGroups = { }, "Live Autocompletion": { path: "enableLiveAutocompletion" + }, + "Custom scrollbar": { + path: "customScrollbar" } } }; diff --git a/src/layer/decorators.js b/src/layer/decorators.js new file mode 100644 index 00000000000..a2be8107540 --- /dev/null +++ b/src/layer/decorators.js @@ -0,0 +1,129 @@ +"use strict"; +var dom = require("../lib/dom"); +var oop = require("../lib/oop"); +var EventEmitter = require("../lib/event_emitter").EventEmitter; + +var Decorator = function (parent, renderer) { + this.canvas = dom.createElement("canvas"); + this.renderer = renderer; + this.pixelRatio = 1; + this.maxHeight = renderer.layerConfig.maxHeight; + this.lineHeight = renderer.layerConfig.lineHeight; + this.canvasHeight = parent.parent.scrollHeight; + this.heightRatio = this.canvasHeight / this.maxHeight; + this.canvasWidth = parent.width; + this.minDecorationHeight = (2 * this.pixelRatio) | 0; + this.halfMinDecorationHeight = (this.minDecorationHeight / 2) | 0; + + this.canvas.width = this.canvasWidth; + this.canvas.height = this.canvasHeight; + this.canvas.style.top = 0 + "px"; + this.canvas.style.right = 0 + "px"; + this.canvas.style.zIndex = 7 + "px"; + this.canvas.style.position = "absolute"; + this.colors = {}; + this.colors.dark = { + "error": "rgba(255, 18, 18, 1)", + "warning": "rgba(18, 136, 18, 1)", + "info": "rgba(18, 18, 136, 1)" + }; + + this.colors.light = { + "error": "rgb(255,51,51)", + "warning": "rgb(32,133,72)", + "info": "rgb(35,68,138)" + }; + + parent.element.appendChild(this.canvas); + +}; + +(function () { + oop.implement(this, EventEmitter); + + this.$updateDecorators = function (config) { + var colors = (this.renderer.theme.isDark === true) ? this.colors.dark : this.colors.light; + if (config) { + this.maxHeight = config.maxHeight; + this.lineHeight = config.lineHeight; + this.canvasHeight = config.height; + var allLineHeight = (config.lastRow + 1) * this.lineHeight; + if (allLineHeight < this.canvasHeight) { + this.heightRatio = 1; + } + else { + this.heightRatio = this.canvasHeight / this.maxHeight; + } + } + var ctx = this.canvas.getContext("2d"); + + function compare(a, b) { + if (a.priority < b.priority) return -1; + if (a.priority > b.priority) return 1; + return 0; + } + + var annotations = this.renderer.session.$annotations; + ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); + if (annotations) { + var priorities = { + "info": 1, + "warning": 2, + "error": 3 + }; + annotations.forEach(function (item) { + item.priority = priorities[item.type] || null; + }); + annotations = annotations.sort(compare); + var foldData = this.renderer.session.$foldData; + + for (let i = 0; i < annotations.length; i++) { + let row = annotations[i].row; + let compensateFold = this.compensateFoldRows(row, foldData); + let currentY = Math.round((row - compensateFold) * this.lineHeight * this.heightRatio); + let y1 = Math.round(((row - compensateFold) * this.lineHeight * this.heightRatio)); + let y2 = Math.round((((row - compensateFold) * this.lineHeight + this.lineHeight) * this.heightRatio)); + const height = y2 - y1; + if (height < this.minDecorationHeight) { + let yCenter = ((y1 + y2) / 2) | 0; + if (yCenter < this.halfMinDecorationHeight) { + yCenter = this.halfMinDecorationHeight; + } + else if (yCenter + this.halfMinDecorationHeight > this.canvasHeight) { + yCenter = this.canvasHeight - this.halfMinDecorationHeight; + } + y1 = Math.round(yCenter - this.halfMinDecorationHeight); + y2 = Math.round(yCenter + this.halfMinDecorationHeight); + } + + ctx.fillStyle = colors[annotations[i].type] || null; + ctx.fillRect(0, currentY, this.canvasWidth, y2 - y1); + } + } + var cursor = this.renderer.session.selection.getCursor(); + if (cursor) { + let compensateFold = this.compensateFoldRows(cursor.row, foldData); + let currentY = Math.round((cursor.row - compensateFold) * this.lineHeight * this.heightRatio); + ctx.fillStyle = "rgba(0, 0, 0, 0.5)"; + ctx.fillRect(0, currentY, this.canvasWidth, 2); + } + + }; + + this.compensateFoldRows = function (row, foldData) { + let compensateFold = 0; + if (foldData && foldData.length > 0) { + for (let j = 0; j < foldData.length; j++) { + if (row > foldData[j].start.row && row < foldData[j].end.row) { + compensateFold += row - foldData[j].start.row; + } + else if (row >= foldData[j].end.row) { + compensateFold += foldData[j].end.row - foldData[j].start.row; + } + } + } + return compensateFold; + }; +}.call(Decorator.prototype)); + +exports.Decorator = Decorator; \ No newline at end of file diff --git a/src/scrollbar.js b/src/scrollbar.js index 377660d7f22..ca7c4224a0d 100644 --- a/src/scrollbar.js +++ b/src/scrollbar.js @@ -234,7 +234,7 @@ oop.inherits(HScrollBar, ScrollBar); /** * Sets the scroll left of the scroll bar. - * @param {Number} scrollTop The new scroll left + * @param {Number} scrollLeft The new scroll left **/ this.setScrollLeft = function(scrollLeft) { // on chrome 17+ for small zoom levels after calling this function diff --git a/src/scrollbar_custom.js b/src/scrollbar_custom.js new file mode 100644 index 00000000000..b6150f9edf3 --- /dev/null +++ b/src/scrollbar_custom.js @@ -0,0 +1,355 @@ +"use strict"; + +var oop = require("./lib/oop"); +var dom = require("./lib/dom"); +var event = require("./lib/event"); +var EventEmitter = require("./lib/event_emitter").EventEmitter; + +dom.importCssString('.ace_editor>.ace_sb-v div, .ace_editor>.ace_sb-h div{\n' + ' position: absolute;\n' + + ' background: rgba(128, 128, 128, 0.6);\n' + ' -moz-box-sizing: border-box;\n' + ' box-sizing: border-box;\n' + + ' border: 1px solid #bbb;\n' + ' border-radius: 2px;\n' + ' z-index: 8;\n' + '}\n' + + '.ace_editor>.ace_sb-v, .ace_editor>.ace_sb-h {\n' + ' position: absolute;\n' + ' z-index: 6;\n' + + ' background: none;' + ' overflow: hidden!important;\n' + '}\n' + '.ace_editor>.ace_sb-v {\n' + + ' z-index: 6;\n' + ' right: 0;\n' + ' top: 0;\n' + ' width: 12px;\n' + '}' + '.ace_editor>.ace_sb-v div {\n' + + ' z-index: 8;\n' + ' right: 0;\n' + ' width: 100%;\n' + '}' + '.ace_editor>.ace_sb-h {\n' + ' bottom: 0;\n' + + ' left: 0;\n' + ' height: 12px;\n' + '}' + '.ace_editor>.ace_sb-h div {\n' + ' bottom: 0;\n' + + ' height: 100%;\n' + '}' + '.ace_editor>.ace_sb_grabbed {\n' + ' z-index: 8;\n' + ' background: #000;\n' + + '}'); + +/** + * An abstract class representing a native scrollbar control. + * @class ScrollBar + **/ + +/** + * Creates a new `ScrollBar`. `parent` is the owner of the scroll bar. + * @param {Element} parent A DOM element + * + * @constructor + **/ + +var ScrollBar = function (parent) { + this.element = dom.createElement("div"); + this.element.className = "ace_sb" + this.classSuffix; + this.inner = dom.createElement("div"); + this.inner.className = ""; + this.element.appendChild(this.inner); + this.VScrollWidth = 12; + this.HScrollHeight = 12; + + parent.appendChild(this.element); + this.setVisible(false); + this.skipEvent = false; + + event.addMultiMouseDownListener(this.element, [500, 300, 300], this, "onMouseDown"); +}; + +(function () { + oop.implement(this, EventEmitter); + + this.setVisible = function (isVisible) { + this.element.style.display = isVisible ? "" : "none"; + this.isVisible = isVisible; + this.coeff = 1; + }; + + +}).call(ScrollBar.prototype); + +/** + * Represents a vertical scroll bar. + * @class VScrollBar + **/ + +/** + * Creates a new `VScrollBar`. `parent` is the owner of the scroll bar. + * @param {Element} parent A DOM element + * @param {Object} renderer An editor renderer + * + * @constructor + **/ + +var VScrollBar = function (parent, renderer) { + ScrollBar.call(this, parent); + this.scrollTop = 0; + this.scrollHeight = 0; + this.parent = parent; + this.width = this.VScrollWidth; + this.renderer = renderer; + this.inner.style.width = this.element.style.width = (this.width || 15) + "px"; + this.$minWidth = 0; +}; + +oop.inherits(VScrollBar, ScrollBar); + +(function () { + this.classSuffix = '-v'; + + oop.implement(this, EventEmitter); + + /** + * Emitted when the scroll thumb dragged or scrollbar canvas clicked. + **/ + this.onMouseDown = function (eType, e) { + if (eType !== "mousedown") return; + + if (event.getButton(e) !== 0 || e.detail === 2) { + return; + } + + if (e.target === this.inner) { + var self = this; + var mousePageY = e.clientY; + + var onMouseMove = function (e) { + mousePageY = e.clientY; + }; + + var onMouseUp = function () { + clearInterval(timerId); + }; + var startY = e.clientY; + var startTop = this.thumbTop; + + var onScrollInterval = function () { + if (mousePageY === undefined) return; + var scrollTop = self.scrollTopFromThumbTop(startTop + mousePageY - startY); + if (scrollTop === self.scrollTop) return; + self._emit("scroll", {data: scrollTop}); + }; + + event.capture(this.inner, onMouseMove, onMouseUp); + var timerId = setInterval(onScrollInterval, 20); + return event.preventDefault(e); + } + var top = e.clientY - this.element.getBoundingClientRect().top - this.thumbHeight / 2; + this._emit("scroll", {data: this.scrollTopFromThumbTop(top)}); + return event.preventDefault(e); + }; + + this.getHeight = function () { + return this.height; + }; + + /** + * Returns new top for scroll thumb + * @param {Number}thumbTop + * @returns {Number} + **/ + this.scrollTopFromThumbTop = function (thumbTop) { + var scrollTop = thumbTop * (this.pageHeight - this.viewHeight) / (this.slideHeight - this.thumbHeight); + scrollTop = scrollTop >> 0; + if (scrollTop < 0) { + scrollTop = 0; + } + else if (scrollTop > this.pageHeight - this.viewHeight) { + scrollTop = this.pageHeight - this.viewHeight; + } + return scrollTop; + }; + + /** + * Returns the width of the scroll bar. + * @returns {Number} + **/ + this.getWidth = function () { + return Math.max(this.isVisible ? this.width : 0, this.$minWidth || 0); + }; + + /** + * Sets the height of the scroll bar, in pixels. + * @param {Number} height The new height + **/ + this.setHeight = function (height) { + this.height = Math.max(0, height); + this.slideHeight = this.height; + this.viewHeight = this.height; + + this.setScrollHeight(this.pageHeight, true); + }; + + /** + * Sets the inner and scroll height of the scroll bar, in pixels. + * @param {Number} height The new inner height + * + * @param {boolean} force Forcely update height + **/ + this.setInnerHeight = this.setScrollHeight = function (height, force) { + if (this.pageHeight === height && !force) return; + this.pageHeight = height; + this.thumbHeight = this.slideHeight * this.viewHeight / this.pageHeight; + + if (this.thumbHeight > this.slideHeight) this.thumbHeight = this.slideHeight; + if (this.thumbHeight < 15) this.thumbHeight = 15; + + this.inner.style.height = this.thumbHeight + "px"; + + if (this.scrollTop > (this.pageHeight - this.viewHeight)) { + this.scrollTop = (this.pageHeight - this.viewHeight); + if (this.scrollTop < 0) this.scrollTop = 0; + this._emit("scroll", {data: this.scrollTop}); + } + }; + + /** + * Sets the scroll top of the scroll bar. + * @param {Number} scrollTop The new scroll top + **/ + this.setScrollTop = function (scrollTop) { + this.scrollTop = scrollTop; + if (scrollTop < 0) scrollTop = 0; + this.thumbTop = scrollTop * (this.slideHeight - this.thumbHeight) / (this.pageHeight - this.viewHeight); + this.inner.style.top = this.thumbTop + "px"; + }; + +}).call(VScrollBar.prototype); + +/** + * Represents a horizontal scroll bar. + * @class HScrollBar + **/ + +/** + * Creates a new `HScrollBar`. `parent` is the owner of the scroll bar. + * @param {Element} parent A DOM element + * @param {Object} renderer An editor renderer + * + * @constructor + **/ +var HScrollBar = function (parent, renderer) { + ScrollBar.call(this, parent); + this.scrollLeft = 0; + this.scrollWidth = 0; + this.height = this.HScrollHeight; + this.inner.style.height = this.element.style.height = (this.height || 12) + "px"; + this.renderer = renderer; +}; + +oop.inherits(HScrollBar, ScrollBar); + +(function () { + + this.classSuffix = '-h'; + + oop.implement(this, EventEmitter); + + /** + * Emitted when the scroll thumb dragged or scrollbar canvas clicked. + **/ + this.onMouseDown = function (eType, e) { + if (eType !== "mousedown") return; + + if (event.getButton(e) !== 0 || e.detail === 2) { + return; + } + + + if (e.target === this.inner) { + var self = this; + var mousePageX = e.clientX; + + var onMouseMove = function (e) { + mousePageX = e.clientX; + }; + + var onMouseUp = function () { + clearInterval(timerId); + }; + var startX = e.clientX; + var startLeft = this.thumbLeft; + + var onScrollInterval = function () { + if (mousePageX === undefined) return; + var scrollLeft = self.scrollLeftFromThumbLeft(startLeft + mousePageX - startX); + if (scrollLeft === self.scrollLeft) return; + self._emit("scroll", {data: scrollLeft}); + }; + + event.capture(this.inner, onMouseMove, onMouseUp); + var timerId = setInterval(onScrollInterval, 20); + return event.preventDefault(e); + } + + var left = e.clientX - this.element.getBoundingClientRect().left - this.thumbWidth / 2; + this._emit("scroll", {data: this.scrollLeftFromThumbLeft(left)}); + return event.preventDefault(e); + }; + + /** + * Returns the height of the scroll bar. + * @returns {Number} + **/ + this.getHeight = function () { + return this.isVisible ? this.height : 0; + }; + + /** + * Returns new left for scroll thumb + * @param {Number} thumbLeft + * @returns {Number} + **/ + this.scrollLeftFromThumbLeft = function (thumbLeft) { + var scrollLeft = thumbLeft * (this.pageWidth - this.viewWidth) / (this.slideWidth - this.thumbWidth); + scrollLeft = scrollLeft >> 0; + if (scrollLeft < 0) { + scrollLeft = 0; + } + else if (scrollLeft > this.pageWidth - this.viewWidth) { + scrollLeft = this.pageWidth - this.viewWidth; + } + return scrollLeft; + }; + + /** + * Sets the width of the scroll bar, in pixels. + * @param {Number} width The new width + **/ + this.setWidth = function (width) { + this.width = Math.max(0, width); + this.element.style.width = this.width + "px"; + this.slideWidth = this.width; + this.viewWidth = this.width; + + this.setScrollWidth(this.pageWidth, true); + }; + + /** + * Sets the inner and scroll width of the scroll bar, in pixels. + * @param {Number} width The new inner width + * @param {boolean} force Forcely update width + **/ + this.setInnerWidth = this.setScrollWidth = function (width, force) { + if (this.pageWidth === width && !force) return; + this.pageWidth = width; + this.thumbWidth = this.slideWidth * this.viewWidth / this.pageWidth; + + if (this.thumbWidth > this.slideWidth) this.thumbWidth = this.slideWidth; + if (this.thumbWidth < 15) this.thumbWidth = 15; + this.inner.style.width = this.thumbWidth + "px"; + + if (this.scrollLeft > (this.pageWidth - this.viewWidth)) { + this.scrollLeft = (this.pageWidth - this.viewWidth); + if (this.scrollLeft < 0) this.scrollLeft = 0; + this._emit("scroll", {data: this.scrollLeft}); + } + }; + + /** + * Sets the scroll left of the scroll bar. + * @param {Number} scrollLeft The new scroll left + **/ + this.setScrollLeft = function (scrollLeft) { + this.scrollLeft = scrollLeft; + if (scrollLeft < 0) scrollLeft = 0; + this.thumbLeft = scrollLeft * (this.slideWidth - this.thumbWidth) / (this.pageWidth - this.viewWidth); + this.inner.style.left = (this.thumbLeft) + "px"; + }; + +}).call(HScrollBar.prototype); + +exports.ScrollBar = VScrollBar; // backward compatibility +exports.ScrollBarV = VScrollBar; // backward compatibility +exports.ScrollBarH = HScrollBar; // backward compatibility + +exports.VScrollBar = VScrollBar; +exports.HScrollBar = HScrollBar; diff --git a/src/scrollbar_test.js b/src/scrollbar_test.js new file mode 100644 index 00000000000..6f317bc1ae5 --- /dev/null +++ b/src/scrollbar_test.js @@ -0,0 +1,140 @@ +if (typeof process !== "undefined") { + require("amd-loader"); + require("./test/mockdom"); +} + +"use strict"; + +var assert = require("./test/assertions"); +var VirtualRenderer = require("./virtual_renderer").VirtualRenderer; +var Editor = require("./editor").Editor; +var MouseEvent = function (type, opts) { + var e = document.createEvent("MouseEvents"); + e.initMouseEvent(/click|wheel/.test(type) ? type : "mouse" + type, true, true, window, opts.detail, opts.x, opts.y, + opts.x, opts.y, opts.ctrl, opts.alt, opts.shift, opts.meta, opts.button || 0, opts.relatedTarget + ); + return e; +}; +var WheelEvent = function (opts) { + var e = new MouseEvent("wheel", opts); + e.DOM_DELTA_PIXEL = 0; + e.DOM_DELTA_LINE = 1; + e.DOM_DELTA_PAGE = 2; + e.deltaMode = e["DOM_DELTA_" + opts.mode.toUpperCase()]; + e.deltaX = opts.x || 0; + e.deltaY = opts.y || 0; + return e; +}; +var editor = null; +var renderer = null; +module.exports = { + name: "ACE scrollbar_custom.js", + setUp: function () { + if (editor) editor.destroy(); + var el = document.createElement("div"); + + el.style.left = "20px"; + el.style.top = "30px"; + el.style.width = "300px"; + el.style.height = "100px"; + document.body.appendChild(el); + renderer = new VirtualRenderer(el); + renderer.scrollHeight = 50; + renderer.layerConfig.maxHeight = 200; + renderer.layerConfig.lineHeight = 14; + editor = new Editor(renderer); + editor.on("destroy", function () { + document.body.removeChild(el); + }); + editor.setOptions({ + customScrollbar: true + }); + }, + tearDown: function () { + editor && editor.destroy(); + editor = null; + }, + "test: vertical scrolling": function () { + editor.setValue("a" + "\n".repeat(100) + "b" + "\nxxxxxx", -1); + renderer.$loop._flush(); + renderer.scrollBarV.element.dispatchEvent(MouseEvent("down", { + x: 0, + y: 80, + button: 0 + })); + renderer.$loop._flush(); + var thumbTop = renderer.scrollBarV.thumbTop; + assert.ok(thumbTop > 0); + editor.container.dispatchEvent(WheelEvent({ + mode: "line", + y: 50 + })); + renderer.$loop._flush(); + assert.ok(renderer.scrollBarV.thumbTop > thumbTop); + }, + "test: dragging vertical scroll thumb": function (done) { + editor.setValue("a" + "\n".repeat(100) + "b" + "\nxxxxxx", -1); + renderer.$loop._flush(); + + renderer.scrollBarV.inner.dispatchEvent(MouseEvent("down", { + x: 5, + y: 10, + button: 0 + })); + renderer.$loop._flush(); + + renderer.scrollBarV.inner.dispatchEvent(MouseEvent("move", { + x: 5, + y: 80, + button: 0 + })); + + setTimeout(function () { + assert.ok(renderer.scrollBarV.thumbTop > 0); + done(); + }, 200); + }, + "test: horizontal scrolling": function () { + assert.ok(!renderer.scrollBarH.isVisible); + editor.setValue("a".repeat(1000), -1); + + renderer.$loop._flush(); + assert.ok(renderer.scrollBarH.isVisible); + renderer.scrollBarH.element.dispatchEvent(MouseEvent("down", { + x: 80, + y: 0, + button: 0 + })); + renderer.$loop._flush(); + + assert.ok(renderer.scrollBarH.thumbLeft > 0); + }, + "test: dragging horizontal scroll thumb": function (done) { + editor.setValue("a".repeat(1000), -1); + renderer.$loop._flush(); + + renderer.scrollBarH.inner.dispatchEvent(MouseEvent("down", { + x: 5, + y: 5, + button: 0 + })); + renderer.$loop._flush(); + + renderer.scrollBarH.inner.dispatchEvent(MouseEvent("move", { + x: 80, + y: 5, + button: 0 + })); + + setTimeout(function () { + assert.ok(renderer.scrollBarH.thumbLeft > 0); + done(); + }, 200); + } + +}; + + +if (typeof module !== "undefined" && module === require.main) { + require("asyncjs").test.testcase(module.exports).exec(); +} diff --git a/src/test/mockdom.js b/src/test/mockdom.js index d0de6c33aca..b64b79c405b 100644 --- a/src/test/mockdom.js +++ b/src/test/mockdom.js @@ -472,7 +472,54 @@ function Node(name) { if (document.activeElement == this) document.body.focus(); }; - + this.getContext = function (contextId, options) { + if (this.contextMock !== undefined) return this.contextMock; + this.contextMock = { + points: [], + fillStyle: "#000", + clearRect: function (x, y, w, h) { + for (var i = x; i < w + x; i++) { + for (var j = y; j < h + y; j++) { + var point = this.points.find(el => el.x === i && el.y === j); + if (point) { + point.fillStyle = "rgba(0, 0, 0, 0)"; + } + else { + this.points.push({ + x: i, + y: j, + fillStyle: "rgba(0, 0, 0, 0)" + }); + } + } + } + }, + fillRect: function (x, y, w, h) { + for (var i = x; i < w + x; i++) { + for (var j = y; j < h + y; j++) { + var point = this.points.find(el => el.x === i && el.y === j); + if (point) { + point.fillStyle = this.fillStyle; + } + else { + this.points.push({ + x: i, + y: j, + fillStyle: this.fillStyle + }); + } + } + } + }, + getImageData: function (sx, sy, sw, sh) { + return { + "data": this.points.filter((el) => el.x >= sx && el.x <= sx + sw && el.y >= sy && el.y <= sy + sh) + }; + } + }; + return this.contextMock; + }; + function removeAllChildren(node) { node.children.forEach(function(node) { node.parentNode = null; diff --git a/src/virtual_renderer.js b/src/virtual_renderer.js index 7aac26854a1..0410051ba5d 100644 --- a/src/virtual_renderer.js +++ b/src/virtual_renderer.js @@ -9,10 +9,13 @@ var TextLayer = require("./layer/text").Text; var CursorLayer = require("./layer/cursor").Cursor; var HScrollBar = require("./scrollbar").HScrollBar; var VScrollBar = require("./scrollbar").VScrollBar; +var HScrollBarCustom = require("./scrollbar_custom").HScrollBar; +var VScrollBarCustom = require("./scrollbar_custom").VScrollBar; var RenderLoop = require("./renderloop").RenderLoop; var FontMetrics = require("./layer/font_metrics").FontMetrics; var EventEmitter = require("./lib/event_emitter").EventEmitter; var editorCss = require("./css/editor.css"); +var Decorator = require("./layer/decorators").Decorator; var useragent = require("./lib/useragent"); var HIDE_TEXTAREA = useragent.isIE; @@ -358,6 +361,9 @@ var VirtualRenderer = function(container, theme) { // reset cached values on scrollbars, needs to be removed when switching to non-native scrollbars // see https://github.com/ajaxorg/ace/issues/2195 this.scrollBarH.scrollLeft = this.scrollBarV.scrollTop = null; + if (this.$customScrollbar) { + this.$updateCustomScrollbar(true); + } }; this.$updateCachedSize = function(force, gutterWidth, width, height) { @@ -378,7 +384,7 @@ var VirtualRenderer = function(container, theme) { if (this.$horizScroll) size.scrollerHeight -= this.scrollBarH.getHeight(); - // this.scrollBarV.setHeight(size.scrollerHeight); + this.scrollBarV.setHeight(size.scrollerHeight); this.scrollBarV.element.style.bottom = this.scrollBarH.getHeight() + "px"; changes = changes | this.CHANGE_SCROLL; @@ -403,7 +409,7 @@ var VirtualRenderer = function(container, theme) { dom.setStyle(this.scroller.style, "right", right); dom.setStyle(this.scroller.style, "bottom", this.scrollBarH.getHeight()); - // this.scrollBarH.element.style.setWidth(size.scrollerWidth); + this.scrollBarH.setWidth(size.scrollerWidth); if (this.session && this.session.getUseWrapMode() && this.adjustWrapLimit() || force) { changes |= this.CHANGE_FULL; @@ -872,6 +878,9 @@ var VirtualRenderer = function(container, theme) { this.$textLayer.update(config); if (this.$showGutter) this.$gutterLayer.update(config); + if (this.$customScrollbar) { + this.$scrollDecorator.$updateDecorators(config); + } this.$markerBack.update(config); this.$markerFront.update(config); this.$cursorLayer.update(config); @@ -894,6 +903,9 @@ var VirtualRenderer = function(container, theme) { else this.$gutterLayer.scrollLines(config); } + if (this.$customScrollbar) { + this.$scrollDecorator.$updateDecorators(config); + } this.$markerBack.update(config); this.$markerFront.update(config); this.$cursorLayer.update(config); @@ -907,18 +919,30 @@ var VirtualRenderer = function(container, theme) { this.$textLayer.update(config); if (this.$showGutter) this.$gutterLayer.update(config); + if (this.$customScrollbar) { + this.$scrollDecorator.$updateDecorators(config); + } } else if (changes & this.CHANGE_LINES) { if (this.$updateLines() || (changes & this.CHANGE_GUTTER) && this.$showGutter) this.$gutterLayer.update(config); + if (this.$customScrollbar) { + this.$scrollDecorator.$updateDecorators(config); + } } else if (changes & this.CHANGE_TEXT || changes & this.CHANGE_GUTTER) { if (this.$showGutter) this.$gutterLayer.update(config); + if (this.$customScrollbar) { + this.$scrollDecorator.$updateDecorators(config); + } } else if (changes & this.CHANGE_CURSOR) { if (this.$highlightGutterLine) this.$gutterLayer.updateLineHighlight(config); + if (this.$customScrollbar) { + this.$scrollDecorator.$updateDecorators(config); + } } if (changes & this.CHANGE_CURSOR) { @@ -1715,6 +1739,41 @@ var VirtualRenderer = function(container, theme) { this.container.textContent = ""; }; + this.$updateCustomScrollbar = function (val) { + var _self = this; + this.$horizScroll = this.$vScroll = null; + this.scrollBarV.element.remove(); + this.scrollBarH.element.remove(); + if (this.$scrollDecorator) { + delete this.$scrollDecorator; + } + if (val === true) { + this.scrollBarV = new VScrollBarCustom(this.container, this); + this.scrollBarH = new HScrollBarCustom(this.container, this); + this.scrollBarV.setHeight(this.$size.scrollerHeight); + this.scrollBarH.setWidth(this.$size.scrollerWidth); + + this.scrollBarV.addEventListener("scroll", function (e) { + if (!_self.$scrollAnimation) _self.session.setScrollTop(e.data - _self.scrollMargin.top); + }); + this.scrollBarH.addEventListener("scroll", function (e) { + if (!_self.$scrollAnimation) _self.session.setScrollLeft(e.data - _self.scrollMargin.left); + }); + this.$scrollDecorator = new Decorator(this.scrollBarV, this); + this.$scrollDecorator.$updateDecorators(); + } + else { + this.scrollBarV = new VScrollBar(this.container, this); + this.scrollBarH = new HScrollBar(this.container, this); + this.scrollBarV.addEventListener("scroll", function (e) { + if (!_self.$scrollAnimation) _self.session.setScrollTop(e.data - _self.scrollMargin.top); + }); + this.scrollBarH.addEventListener("scroll", function (e) { + if (!_self.$scrollAnimation) _self.session.setScrollLeft(e.data - _self.scrollMargin.left); + }); + } + }; + }).call(VirtualRenderer.prototype); @@ -1856,6 +1915,12 @@ config.defineOptions(VirtualRenderer.prototype, "renderer", { this.$loop.schedule(this.CHANGE_GUTTER); } }, + customScrollbar: { + set: function(val) { + this.$updateCustomScrollbar(val); + }, + initialValue: false + }, theme: { set: function(val) { this.setTheme(val); }, get: function() { return this.$themeId || this.theme; }, diff --git a/src/virtual_renderer_test.js b/src/virtual_renderer_test.js index 6ca26c5b95d..24b484c0557 100644 --- a/src/virtual_renderer_test.js +++ b/src/virtual_renderer_test.js @@ -214,6 +214,90 @@ module.exports = { editor.session.selection.$setSelection(1, 15, 1, 15); editor.resize(true); assertIndentGuides( 0); + }, + "test annotation marks": function() { + function findPointFillStyle(points, x, y) { + var point = points.find(el => el.x === x && el.y === y); + if (point === undefined) return; + + return point.fillStyle; + } + + function assertCoordsColor(expected, points) { + for (var el of expected) { + assert.equal(findPointFillStyle(points, el.x, el.y), el.color); + } + } + + var renderer = editor.renderer; + renderer.container.scrollHeight = 100; + renderer.layerConfig.maxHeight = 200; + renderer.layerConfig.lineHeight = 14; + + editor.setOptions({ + customScrollbar: true + }); + editor.setValue("a" + "\n".repeat(100) + "b" + "\nxxxxxx", -1); + editor.session.setAnnotations([ + { + row: 1, + column: 2, + type: "error" + }, { + row: 4, + column: 1, + type: "warning" + }, { + row: 20, + column: 1, + type: "info" + } + ]); + renderer.$loop._flush(); + var context = renderer.$scrollDecorator.canvas.getContext(); + var imageData = context.getImageData(0, 0, 50, 50); + var scrollDecoratorColors = renderer.$scrollDecorator.colors.light; + var values = [ + // reflects cursor position on canvas + {x: 0, y: 0, color: "rgba(0, 0, 0, 0.5)"}, + {x: 1, y: 1, color: "rgba(0, 0, 0, 0.5)"}, + // reflects error annotation mark on canvas overlapped by cursor + {x: 2, y: 2, color: scrollDecoratorColors.error}, + // default value + {x: 3, y: 3, color: "rgba(0, 0, 0, 0)"}, + // reflects warning annotation mark on canvas + {x: 4, y: 4, color: scrollDecoratorColors.warning}, + {x: 5, y: 5, color: scrollDecoratorColors.warning}, + {x: 6, y: 6, color: "rgba(0, 0, 0, 0)"}, + {x: 7, y: 20, color: scrollDecoratorColors.info}, + {x: 8, y: 21, color: scrollDecoratorColors.info} + ]; + assertCoordsColor(values, imageData.data); + editor.moveCursorTo(5, 6); + renderer.$loop._flush(); + values = [ + {x: 0, y: 0, color: "rgba(0, 0, 0, 0)"}, + {x: 1, y: 1, color: scrollDecoratorColors.error}, + {x: 2, y: 2, color: scrollDecoratorColors.error}, + {x: 3, y: 3, color: "rgba(0, 0, 0, 0)"}, + {x: 4, y: 4, color: scrollDecoratorColors.warning}, + {x: 5, y: 5, color: "rgba(0, 0, 0, 0.5)"}, + {x: 6, y: 6, color: "rgba(0, 0, 0, 0.5)"} + ]; + assertCoordsColor(values, imageData.data); + renderer.session.addFold("...", new Range(0, 0, 3, 2)); + editor.moveCursorTo(10, 0); + renderer.$loop._flush(); + values = [ + {x: 0, y: 0, color: scrollDecoratorColors.error}, + {x: 1, y: 1, color: scrollDecoratorColors.error}, + {x: 2, y: 2, color: scrollDecoratorColors.warning}, + {x: 3, y: 3, color: "rgba(0, 0, 0, 0)"}, + {x: 4, y: 4, color: "rgba(0, 0, 0, 0)"}, + {x: 5, y: 5, color: "rgba(0, 0, 0, 0)"}, + {x: 6, y: 6, color: "rgba(0, 0, 0, 0)"} + ]; + assertCoordsColor(values, imageData.data); } // change tab size after setDocument (for text layer)