From 6d7bd89f0bb89ae9f9438d12fa5726e2a4d22e40 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Mon, 9 Oct 2017 21:45:52 +0200 Subject: [PATCH 1/7] Start the fancy-debug rewrite --- js/term/debug.js | 457 +++++++++++-------------------------- js/term/screen_layout.js | 2 + js/term/screen_renderer.js | 4 +- sass/pages/_term.scss | 4 +- 4 files changed, 140 insertions(+), 327 deletions(-) diff --git a/js/term/debug.js b/js/term/debug.js index 294da29..9d4b062 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -1,397 +1,208 @@ -const { mk } = require('../utils') - module.exports = function attachDebugger (screen, connection) { - const debugCanvas = mk('canvas') + const debugCanvas = document.createElement('canvas') + debugCanvas.classList.add('debug-canvas') const ctx = debugCanvas.getContext('2d') - debugCanvas.classList.add('debug-canvas') + const toolbar = document.createElement('div') + toolbar.classList.add('debug-toolbar') - let mouseHoverCell = null - let updateToolbar + let selectedCell = null + const onMouseMove = (e) => { + selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) + } + const onMouseOut = (e) => { + selectedCell = null + } - let onMouseMove = e => { - mouseHoverCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) - startDrawing() - updateToolbar() + const updateCanvasSize = function () { + let { width, height, devicePixelRatio } = screen.layout.window + let cellSize = screen.layout.getCellSize() + let padding = Math.round(screen.layout._padding) + debugCanvas.width = (width * cellSize.width + 2 * padding) * devicePixelRatio + debugCanvas.height = (height * cellSize.height + 2 * padding) * devicePixelRatio + debugCanvas.style.width = `${width * cellSize.width + 2 * screen.layout._padding}px` + debugCanvas.style.height = `${height * cellSize.height + 2 * screen.layout._padding}px` } - let onMouseOut = () => (mouseHoverCell = null) - let addCanvas = function () { - if (!debugCanvas.parentNode) { + let startDrawLoop + let screenAttached = false + let eventNode + const setScreenAttached = function (attached) { + if (attached && !debugCanvas.parentNode) { screen.layout.canvas.parentNode.appendChild(debugCanvas) - screen.layout.canvas.addEventListener('mousemove', onMouseMove) - screen.layout.canvas.addEventListener('mouseout', onMouseOut) - } - } - let removeCanvas = function () { - if (debugCanvas.parentNode) { + eventNode = debugCanvas.parentNode + eventNode.addEventListener('mousemove', onMouseMove) + eventNode.addEventListener('mouseout', onMouseOut) + screen.layout.on('size-update', updateCanvasSize) + updateCanvasSize() + screenAttached = true + startDrawLoop() + } else if (!attached && debugCanvas.parentNode) { debugCanvas.parentNode.removeChild(debugCanvas) - screen.layout.canvas.removeEventListener('mousemove', onMouseMove) - screen.layout.canvas.removeEventListener('mouseout', onMouseOut) - onMouseOut() + eventNode.removeEventListener('mousemove', onMouseMove) + eventNode.removeEventListener('mouseout', onMouseOut) + screen.layout.removeListener('size-update', updateCanvasSize) + screenAttached = false } } - let updateCanvasSize = function () { - let { width, height, devicePixelRatio } = screen.layout.window - let cellSize = screen.layout.getCellSize() - debugCanvas.width = width * cellSize.width * devicePixelRatio - debugCanvas.height = height * cellSize.height * devicePixelRatio - debugCanvas.style.width = `${width * cellSize.width}px` - debugCanvas.style.height = `${height * cellSize.height}px` + + const setToolbarAttached = function (attached) { + if (attached && !toolbar.parentNode) { + screen.layout.canvas.parentNode.appendChild(toolbar) + } else if (!attached && toolbar.parentNode) { + screen.layout.canvas.parentNode.removeChild(toolbar) + } } - let drawInfo = mk('div') - drawInfo.classList.add('draw-info') + screen.on('update-window:debug', enabled => { + setToolbarAttached(enabled) + }) - let startTime, endTime, lastReason - let cells = new Map() - let clippedRects = [] - let updateFrames = [] + screen.layout.on('update-window:debug', enabled => { + setScreenAttached(enabled) + }) - let startDrawing + let drawData = { + reason: '', + startTime: 0, + endTime: 0, + clipped: [], + frames: [], + cells: new Map() + } screen._debug = screen.layout.renderer._debug = { drawStart (reason) { - lastReason = reason - startTime = Date.now() - clippedRects = [] + drawData.reason = reason + drawData.startTime = window.performance.now() }, drawEnd () { - endTime = Date.now() - drawInfo.textContent = `Draw: ${lastReason} (${(endTime - startTime)} ms), fancy gfx=${screen.layout.renderer.graphics}` - startDrawing() + drawData.endTime = window.performance.now() }, setCell (cell, flags) { - cells.set(cell, [flags, Date.now()]) - }, - clipRect (...args) { - clippedRects.push(args) + drawData.cells.set(cell, [flags, window.performance.now()]) }, pushFrame (frame) { - frame.push(Date.now()) - updateFrames.push(frame) - startDrawing() + drawData.frames.push([...frame, window.performance.now()]) } } - let clipPattern - { - let patternCanvas = document.createElement('canvas') - patternCanvas.width = patternCanvas.height = 12 - let pctx = patternCanvas.getContext('2d') - pctx.lineWidth = 1 - pctx.strokeStyle = '#00f' - pctx.beginPath() - pctx.moveTo(0, 0) - pctx.lineTo(0 - 4, 12) - pctx.moveTo(4, 0) - pctx.lineTo(4 - 4, 12) - pctx.moveTo(8, 0) - pctx.lineTo(8 - 4, 12) - pctx.moveTo(12, 0) - pctx.lineTo(12 - 4, 12) - pctx.moveTo(16, 0) - pctx.lineTo(16 - 4, 12) - pctx.stroke() - clipPattern = ctx.createPattern(patternCanvas, 'repeat') - } - let isDrawing = false - let lastDrawTime = 0 - let t = 0 - let drawLoop = function () { - if (isDrawing) window.requestAnimationFrame(drawLoop) + if (screenAttached) window.requestAnimationFrame(drawLoop) + else isDrawing = false - let dt = (Date.now() - lastDrawTime) / 1000 - lastDrawTime = Date.now() - t += dt + let now = window.performance.now() - let { devicePixelRatio, width, height } = screen.layout.window - let { width: cellWidth, height: cellHeight } = screen.layout.getCellSize() - let screenLength = width * height - let now = Date.now() + let { width, height, devicePixelRatio } = screen.layout.window + let padding = Math.round(screen.layout._padding) + let cellSize = screen.layout.getCellSize() ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0) - ctx.clearRect(0, 0, width * cellWidth, height * cellHeight) + ctx.clearRect(0, 0, width * cellSize.width + 2 * padding, height * cellSize.height + 2 * padding) + ctx.translate(padding, padding) + + ctx.lineWidth = 2 + ctx.lineJoin = 'round' - let activeCells = 0 - for (let cell = 0; cell < screenLength; cell++) { + const cells = drawData.cells + for (let cell = 0; cell < width * height; cell++) { + // cell does not exist or has no flags set if (!cells.has(cell) || cells.get(cell)[0] === 0) continue - let [flags, timestamp] = cells.get(cell) + const [flags, timestamp] = cells.get(cell) let elapsedTime = (now - timestamp) / 1000 - if (elapsedTime > 1) continue + if (elapsedTime > 1) { + cells.delete(cell) + continue + } - activeCells++ ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) let x = cell % width let y = Math.floor(cell / width) - if (flags & 1) { - // redrawn - ctx.fillStyle = '#f0f' - } if (flags & 2) { // updated ctx.fillStyle = '#0f0' + } else if (flags & 1) { + // redrawn + ctx.fillStyle = '#f0f' } - ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + if (!(flags & 4)) { + // outside a clipped region + ctx.fillStyle = '#0ff' + } + + ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) - if (flags & 4) { + if (flags & 8) { // wide cell - ctx.lineWidth = 2 ctx.strokeStyle = '#f00' - ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight) + ctx.beginPath() + ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height) + ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height) + ctx.stroke() } } - if (clippedRects.length) { - ctx.globalAlpha = 0.5 - ctx.beginPath() - - for (let rect of clippedRects) { - ctx.rect(...rect) + let framesToDelete = [] + for (let frame of drawData.frames) { + let timestamp = frame[4] + let elapsedTime = (now - timestamp) / 1000 + if (elapsedTime > 1) framesToDelete.push(frame) + else { + ctx.globalAlpha = 1 - elapsedTime + ctx.strokeStyle = '#ff0' + ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height, + frame[2] * cellSize.width, frame[3] * cellSize.height) } - - ctx.fillStyle = clipPattern - ctx.fill() } - - let didDrawUpdateFrames = false - if (updateFrames.length) { - let framesToDelete = [] - for (let frame of updateFrames) { - let time = frame[4] - let elapsed = Date.now() - time - if (elapsed > 1000) framesToDelete.push(frame) - else { - didDrawUpdateFrames = true - ctx.globalAlpha = 1 - elapsed / 1000 - ctx.strokeStyle = '#ff0' - ctx.lineWidth = 2 - ctx.strokeRect(frame[0] * cellWidth, frame[1] * cellHeight, frame[2] * cellWidth, frame[3] * cellHeight) - } - } - for (let frame of framesToDelete) { - updateFrames.splice(updateFrames.indexOf(frame), 1) - } + for (let frame of framesToDelete) { + drawData.frames.splice(drawData.frames.indexOf(frame), 1) } - if (mouseHoverCell) { + if (selectedCell !== null) { + let [x, y] = selectedCell + ctx.save() + ctx.globalAlpha = 0.5 + ctx.lineWidth = 1 + + // draw X line + ctx.beginPath() + ctx.moveTo(0, y * cellSize.height) + ctx.lineTo(x * cellSize.width, y * cellSize.height) + ctx.strokeStyle = '#f00' + ctx.setLineDash([cellSize.width]) + ctx.stroke() + + // draw Y line + ctx.beginPath() + ctx.moveTo(x * cellSize.width, 0) + ctx.lineTo(x * cellSize.width, y * cellSize.height) + ctx.strokeStyle = '#0f0' + ctx.setLineDash([cellSize.height]) + ctx.stroke() + ctx.globalAlpha = 1 - ctx.lineWidth = 1 + 0.5 * Math.sin(t * 10) + ctx.lineWidth = 1 + 0.5 * Math.sin((now / 1000) * 10) ctx.strokeStyle = '#fff' ctx.lineJoin = 'round' ctx.setLineDash([2, 2]) - ctx.lineDashOffset = t * 10 - ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight) + ctx.lineDashOffset = (now / 1000) * 10 + ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) ctx.lineDashOffset += 2 ctx.strokeStyle = '#000' - ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight) + ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) ctx.restore() } - - if (activeCells === 0 && !mouseHoverCell && !didDrawUpdateFrames) { - isDrawing = false - removeCanvas() - } } - - startDrawing = function () { + startDrawLoop = function () { if (isDrawing) return - addCanvas() - updateCanvasSize() isDrawing = true - lastDrawTime = Date.now() drawLoop() } - - // debug toolbar - const toolbar = mk('div') - toolbar.classList.add('debug-toolbar') - let toolbarAttached = false - - const heartbeat = mk('div') - heartbeat.classList.add('heartbeat') - heartbeat.textContent = '❤' - toolbar.appendChild(heartbeat) - - const dataDisplay = mk('div') - dataDisplay.classList.add('data-display') - toolbar.appendChild(dataDisplay) - - const internalDisplay = mk('div') - internalDisplay.classList.add('internal-display') - toolbar.appendChild(internalDisplay) - - toolbar.appendChild(drawInfo) - - const buttons = mk('div') - buttons.classList.add('toolbar-buttons') - toolbar.appendChild(buttons) - - // heartbeat - connection.on('heartbeat', () => { - heartbeat.classList.remove('beat') - window.requestAnimationFrame(() => { - heartbeat.classList.add('beat') - }) - }) - - { - const redraw = mk('button') - redraw.textContent = 'Redraw' - redraw.addEventListener('click', e => { - screen.layout.renderer.resetDrawn() - screen.layout.renderer.draw('debug-redraw') - }) - buttons.appendChild(redraw) - - const fancyGraphics = mk('button') - fancyGraphics.textContent = 'Toggle Fancy Graphics' - fancyGraphics.addEventListener('click', e => { - screen.layout.renderer.graphics = +!screen.layout.renderer.graphics - screen.layout.renderer.draw('set-graphics') - }) - buttons.appendChild(fancyGraphics) - } - - const attachToolbar = function () { - screen.layout.canvas.parentNode.appendChild(toolbar) - } - const detachToolbar = function () { - toolbar.parentNode.removeChild(toolbar) - } - - screen.on('update-window:debug', debug => { - if (debug !== toolbarAttached) { - toolbarAttached = debug - if (debug) attachToolbar() - else { - detachToolbar() - removeCanvas() - } - } - }) - - const displayCellAttrs = attrs => { - let result = attrs.toString(16) - if (attrs & 1 || attrs & 2) { - result += ':has(' - if (attrs & 1) result += 'fg' - if (attrs & 2) result += (attrs & 1 ? ',' : '') + 'bg' - result += ')' - } - let attributes = [] - if (attrs & (1 << 2)) attributes.push('\\[bold]bold\\()') - if (attrs & (1 << 3)) attributes.push('\\[underline]underln\\()') - if (attrs & (1 << 4)) attributes.push('\\[invert]invert\\()') - if (attrs & (1 << 5)) attributes.push('blink') - if (attrs & (1 << 6)) attributes.push('\\[italic]italic\\()') - if (attrs & (1 << 7)) attributes.push('\\[strike]strike\\()') - if (attrs & (1 << 8)) attributes.push('\\[overline]overln\\()') - if (attrs & (1 << 9)) attributes.push('\\[faint]faint\\()') - if (attrs & (1 << 10)) attributes.push('fraktur') - if (attributes.length) result += ':' + attributes.join() - return result.trim() - } - - const formatColor = color => color < 256 ? color : `#${`000000${(color - 256).toString(16)}`.substr(-6)}` - const getCellData = cell => { - if (cell < 0 || cell > screen.screen.length) return '(-)' - let cellAttrs = screen.layout.renderer.drawnScreenAttrs[cell] | 0 - let cellFG = screen.layout.renderer.drawnScreenFG[cell] | 0 - let cellBG = screen.layout.renderer.drawnScreenBG[cell] | 0 - let fgText = formatColor(cellFG) - let bgText = formatColor(cellBG) - fgText += `\\[color=${screen.layout.renderer.getColor(cellFG).replace(/ /g, '')}]●\\[]` - bgText += `\\[color=${screen.layout.renderer.getColor(cellBG).replace(/ /g, '')}]●\\[]` - let cellCode = (screen.layout.renderer.drawnScreen[cell] || '').codePointAt(0) | 0 - let hexcode = cellCode.toString(16).toUpperCase() - if (hexcode.length < 4) hexcode = `0000${hexcode}`.substr(-4) - hexcode = `U+${hexcode}` - let x = cell % screen.window.width - let y = Math.floor(cell / screen.window.width) - return `((${y},${x})=${cell}:\\[bold]${hexcode}\\[]:F${fgText}:B${bgText}:A(${displayCellAttrs(cellAttrs)}))` - } - - const setFormattedText = (node, text) => { - node.innerHTML = '' - - let match - let attrs = {} - - let pushSpan = content => { - let span = mk('span') - node.appendChild(span) - span.textContent = content - for (let key in attrs) span[key] = attrs[key] - } - - while ((match = text.match(/\\\[(.*?)\]/))) { - if (match.index > 0) pushSpan(text.substr(0, match.index)) - - attrs = { style: '' } - let data = match[1].split(' ') - for (let attr of data) { - if (!attr) continue - let key, value - if (attr.indexOf('=') > -1) { - key = attr.substr(0, attr.indexOf('=')) - value = attr.substr(attr.indexOf('=') + 1) - } else { - key = attr - value = true - } - - if (key === 'bold') attrs.style += 'font-weight:bold;' - if (key === 'italic') attrs.style += 'font-style:italic;' - if (key === 'underline') attrs.style += 'text-decoration:underline;' - if (key === 'invert') attrs.style += 'background:#000;filter:invert(1);' - if (key === 'strike') attrs.style += 'text-decoration:line-through;' - if (key === 'overline') attrs.style += 'text-decoration:overline;' - if (key === 'faint') attrs.style += 'opacity:0.5;' - else if (key === 'color') attrs.style += `color:${value};` - else attrs[key] = value - } - - text = text.substr(match.index + match[0].length) - } - - if (text) pushSpan(text) - } - - let internalInfo = {} - - updateToolbar = () => { - if (!toolbarAttached) return - let text = `C((${screen.cursor.y},${screen.cursor.x}),hang:${screen.cursor.hanging},vis:${screen.cursor.visible})` - if (mouseHoverCell) { - text += ' m' + getCellData(mouseHoverCell[1] * screen.window.width + mouseHoverCell[0]) - } - setFormattedText(dataDisplay, text) - - if ('flags' in internalInfo) { - // we got ourselves some internal data - let text = ' ' - text += ` flags:${internalInfo.flags.toString(2)}` - text += ` curAttrs:${internalInfo.cursorAttrs.toString(2)}` - text += ` Region:${internalInfo.regionStart}->${internalInfo.regionEnd}` - text += ` Charset:${internalInfo.charsetGx} (0:${internalInfo.charsetG0},1:${internalInfo.charsetG1})` - text += ` Heap:${internalInfo.freeHeap}` - text += ` Clients:${internalInfo.clientCount}` - setFormattedText(internalDisplay, text) - } - } - - screen.on('draw', updateToolbar) - screen.on('internal', data => { - internalInfo = data - updateToolbar() - }) } diff --git a/js/term/screen_layout.js b/js/term/screen_layout.js index 357f670..1260a7a 100644 --- a/js/term/screen_layout.js +++ b/js/term/screen_layout.js @@ -247,6 +247,8 @@ module.exports = class ScreenLayout extends EventEmitter { this.renderer.resetDrawn() this.renderer.render('update-size', this.serializeRenderData()) + + this.emit('size-update') } } diff --git a/js/term/screen_renderer.js b/js/term/screen_renderer.js index 22af143..6c4910c 100644 --- a/js/term/screen_renderer.js +++ b/js/term/screen_renderer.js @@ -638,7 +638,6 @@ module.exports = class CanvasRenderer extends EventEmitter { if (y === height - 1) rectHeight += padding ctx.rect(rectX, rectY, rectWidth, rectHeight) - if (this.debug && this._debug) this._debug.clipRect(rectX, rectY, rectWidth, rectHeight) } ctx.save() @@ -673,7 +672,8 @@ module.exports = class CanvasRenderer extends EventEmitter { // set cell flags let flags = (+redrawMap.get(cell)) flags |= (+updateMap.get(cell)) << 1 - flags |= (+isTextWide(text)) << 2 + flags |= (+maskedCells.get(cell)) << 2 + flags |= (+isTextWide(text)) << 3 this._debug.setCell(cell, flags) } } diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index 3b2f54e..c9a8e44 100644 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -106,8 +106,8 @@ body.term { .debug-canvas { position: absolute; - top: 6px; - left: 6px; + top: 0; + left: 0; pointer-events: none; } From e81b2d409ce69a2e494557c13e329019ca956f99 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Tue, 10 Oct 2017 18:18:49 +0200 Subject: [PATCH 2/7] Add rudimentary and inefficient tooltip --- js/term/debug.js | 70 +++++++++++++++++++++++++++++++++++++++++++ sass/pages/_term.scss | 26 ++++++++++++++++ 2 files changed, 96 insertions(+) diff --git a/js/term/debug.js b/js/term/debug.js index 9d4b062..dc6e87c 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -1,3 +1,5 @@ +const { getColor } = require('./themes') + module.exports = function attachDebugger (screen, connection) { const debugCanvas = document.createElement('canvas') debugCanvas.classList.add('debug-canvas') @@ -6,9 +8,21 @@ module.exports = function attachDebugger (screen, connection) { const toolbar = document.createElement('div') toolbar.classList.add('debug-toolbar') + const tooltip = document.createElement('div') + tooltip.classList.add('debug-tooltip') + + let updateTooltip + let selectedCell = null + let mousePosition = null const onMouseMove = (e) => { + if (e.target !== screen.layout.canvas) { + selectedCell = null + return + } selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY) + mousePosition = [e.offsetX, e.offsetY] + updateTooltip() } const onMouseOut = (e) => { selectedCell = null @@ -49,8 +63,10 @@ module.exports = function attachDebugger (screen, connection) { const setToolbarAttached = function (attached) { if (attached && !toolbar.parentNode) { screen.layout.canvas.parentNode.appendChild(toolbar) + screen.layout.canvas.parentNode.appendChild(tooltip) } else if (!attached && toolbar.parentNode) { screen.layout.canvas.parentNode.removeChild(toolbar) + screen.layout.canvas.parentNode.removeChild(tooltip) } } @@ -205,4 +221,58 @@ module.exports = function attachDebugger (screen, connection) { isDrawing = true drawLoop() } + + // tooltip + updateTooltip = function () { + tooltip.innerHTML = '' + let cell = selectedCell[1] * screen.window.width + selectedCell[0] + if (!screen.screen[cell]) return + + let foreground = document.createElement('span') + foreground.textContent = screen.screenFG[cell] + let preview = document.createElement('span') + preview.textContent = ' ●' + preview.style.color = getColor(screen.screenFG[cell], screen.layout.renderer.palette) + foreground.appendChild(preview) + + let background = document.createElement('span') + background.textContent = screen.screenBG[cell] + let bgPreview = document.createElement('span') + bgPreview.textContent = ' ●' + bgPreview.style.color = getColor(screen.screenBG[cell], screen.layout.renderer.palette) + background.appendChild(bgPreview) + + let character = screen.screen[cell] + let codePoint = character.codePointAt(0) + let formattedCodePoint = codePoint.toString(16).length <= 4 + ? `0000${codePoint.toString(16)}`.substr(-4) + : codePoint.toString(16) + + let data = { + Foreground: foreground, + Background: background, + Character: `U+${formattedCodePoint}` + } + + let table = document.createElement('table') + + for (let name in data) { + let row = document.createElement('tr') + let label = document.createElement('td') + label.appendChild(new window.Text(name)) + label.classList.add('label') + + let value = document.createElement('td') + value.appendChild(typeof data[name] === 'string' ? new window.Text(data[name]) : data[name]) + value.classList.add('value') + + row.appendChild(label) + row.appendChild(value) + table.appendChild(row) + } + + tooltip.appendChild(table) + + tooltip.style.transform = `translate(${mousePosition.map(x => x + 'px').join(',')})` + } } diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index c9a8e44..1dd41c0 100644 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -111,6 +111,32 @@ body.term { pointer-events: none; } + .debug-tooltip { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + background: #fff; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); + border-radius: 6px; + padding: 10px 15px; + line-height: 1; + + table { + tr { + .label { + font-weight: bold; + text-align: right; + opacity: 0.5; + } + + .value { + text-align: left; + } + } + } + } + .debug-toolbar { line-height: 1.5; text-align: left; From ef4c8670078e188d9f3202f79ea603982c04b3cc Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 27 Oct 2017 14:00:11 +0200 Subject: [PATCH 3/7] Add fancy debug toolbar --- js/term/debug.js | 311 ++++++++++++++++++++++++++++++++++-------- js/term/screen.js | 2 + sass/pages/_term.scss | 53 ++++++- 3 files changed, 305 insertions(+), 61 deletions(-) diff --git a/js/term/debug.js b/js/term/debug.js index dc6e87c..bd92a9c 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -1,4 +1,16 @@ const { getColor } = require('./themes') +const { + ATTR_FG, + ATTR_BG, + ATTR_BOLD, + ATTR_UNDERLINE, + ATTR_BLINK, + ATTR_ITALIC, + ATTR_STRIKE, + ATTR_OVERLINE, + ATTR_FAINT, + ATTR_FRAKTUR +} = require('./screen_attr_bits') module.exports = function attachDebugger (screen, connection) { const debugCanvas = document.createElement('canvas') @@ -10,8 +22,10 @@ module.exports = function attachDebugger (screen, connection) { const tooltip = document.createElement('div') tooltip.classList.add('debug-tooltip') + tooltip.classList.add('hidden') let updateTooltip + let updateToolbar let selectedCell = null let mousePosition = null @@ -26,6 +40,7 @@ module.exports = function attachDebugger (screen, connection) { } const onMouseOut = (e) => { selectedCell = null + tooltip.classList.add('hidden') } const updateCanvasSize = function () { @@ -64,6 +79,7 @@ module.exports = function attachDebugger (screen, connection) { if (attached && !toolbar.parentNode) { screen.layout.canvas.parentNode.appendChild(toolbar) screen.layout.canvas.parentNode.appendChild(tooltip) + updateToolbar() } else if (!attached && toolbar.parentNode) { screen.layout.canvas.parentNode.removeChild(toolbar) screen.layout.canvas.parentNode.removeChild(tooltip) @@ -80,11 +96,13 @@ module.exports = function attachDebugger (screen, connection) { let drawData = { reason: '', + showUpdates: false, startTime: 0, endTime: 0, clipped: [], frames: [], - cells: new Map() + cells: new Map(), + scrollRegion: null } screen._debug = screen.layout.renderer._debug = { @@ -121,64 +139,66 @@ module.exports = function attachDebugger (screen, connection) { ctx.lineWidth = 2 ctx.lineJoin = 'round' - const cells = drawData.cells - for (let cell = 0; cell < width * height; cell++) { - // cell does not exist or has no flags set - if (!cells.has(cell) || cells.get(cell)[0] === 0) continue - - const [flags, timestamp] = cells.get(cell) - let elapsedTime = (now - timestamp) / 1000 - - if (elapsedTime > 1) { - cells.delete(cell) - continue - } - - ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) - - let x = cell % width - let y = Math.floor(cell / width) - - if (flags & 2) { - // updated - ctx.fillStyle = '#0f0' - } else if (flags & 1) { - // redrawn - ctx.fillStyle = '#f0f' + if (drawData.showUpdates) { + const cells = drawData.cells + for (let cell = 0; cell < width * height; cell++) { + // cell does not exist or has no flags set + if (!cells.has(cell) || cells.get(cell)[0] === 0) continue + + const [flags, timestamp] = cells.get(cell) + let elapsedTime = (now - timestamp) / 1000 + + if (elapsedTime > 1) { + cells.delete(cell) + continue + } + + ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime) + + let x = cell % width + let y = Math.floor(cell / width) + + if (flags & 2) { + // updated + ctx.fillStyle = '#0f0' + } else if (flags & 1) { + // redrawn + ctx.fillStyle = '#f0f' + } + + if (!(flags & 4)) { + // outside a clipped region + ctx.fillStyle = '#0ff' + } + + ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) + + if (flags & 8) { + // wide cell + ctx.strokeStyle = '#f00' + ctx.beginPath() + ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height) + ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height) + ctx.stroke() + } } - if (!(flags & 4)) { - // outside a clipped region - ctx.fillStyle = '#0ff' + let framesToDelete = [] + for (let frame of drawData.frames) { + let timestamp = frame[4] + let elapsedTime = (now - timestamp) / 1000 + if (elapsedTime > 1) framesToDelete.push(frame) + else { + ctx.globalAlpha = 1 - elapsedTime + ctx.strokeStyle = '#ff0' + ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height, + frame[2] * cellSize.width, frame[3] * cellSize.height) + } } - - ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) - - if (flags & 8) { - // wide cell - ctx.strokeStyle = '#f00' - ctx.beginPath() - ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height) - ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height) - ctx.stroke() - } - } - - let framesToDelete = [] - for (let frame of drawData.frames) { - let timestamp = frame[4] - let elapsedTime = (now - timestamp) / 1000 - if (elapsedTime > 1) framesToDelete.push(frame) - else { - ctx.globalAlpha = 1 - elapsedTime - ctx.strokeStyle = '#ff0' - ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height, - frame[2] * cellSize.width, frame[3] * cellSize.height) + for (let frame of framesToDelete) { + drawData.frames.splice(drawData.frames.indexOf(frame), 1) } } - for (let frame of framesToDelete) { - drawData.frames.splice(drawData.frames.indexOf(frame), 1) - } if (selectedCell !== null) { let [x, y] = selectedCell @@ -215,6 +235,28 @@ module.exports = function attachDebugger (screen, connection) { ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height) ctx.restore() } + + if (drawData.scrollRegion !== null) { + let [start, end] = drawData.scrollRegion + + ctx.save() + ctx.globalAlpha = 1 + ctx.strokeStyle = '#00f' + ctx.lineWidth = 2 + ctx.setLineDash([2, 2]) + + ctx.beginPath() + ctx.moveTo(0, start * cellSize.height) + ctx.lineTo(width * cellSize.width, start * cellSize.height) + ctx.stroke() + + ctx.beginPath() + ctx.moveTo(0, end * cellSize.height) + ctx.lineTo(width * cellSize.width, end * cellSize.height) + ctx.stroke() + + ctx.restore() + } } startDrawLoop = function () { if (isDrawing) return @@ -222,21 +264,46 @@ module.exports = function attachDebugger (screen, connection) { drawLoop() } + let pad2 = i => ('00' + i.toString()).substr(-2) + let formatColor = color => color < 256 + ? color.toString() + : '#' + pad2(color >> 16) + pad2((color >> 8) & 0xFF) + pad2(color & 0xFF) + + let makeSpan = (text, styles) => { + let span = document.createElement('span') + span.textContent = text + Object.assign(span.style, styles || {}) + return span + } + let formatAttributes = (target, attrs) => { + if (attrs & ATTR_FG) target.appendChild(makeSpan('HasFG')) + if (attrs & ATTR_BG) target.appendChild(makeSpan('HasBG')) + if (attrs & ATTR_BOLD) target.appendChild(makeSpan('Bold', { fontWeight: 'bold' })) + if (attrs & ATTR_UNDERLINE) target.appendChild(makeSpan('Uline', { textDecoration: 'underline' })) + if (attrs & ATTR_BLINK) target.appendChild(makeSpan('Blink')) + if (attrs & ATTR_ITALIC) target.appendChild(makeSpan('Italic', { fontStyle: 'italic' })) + if (attrs & ATTR_STRIKE) target.appendChild(makeSpan('Strike', { textDecoration: 'line-through' })) + if (attrs & ATTR_OVERLINE) target.appendChild(makeSpan('Oline', { textDecoration: 'overline' })) + if (attrs & ATTR_FAINT) target.appendChild(makeSpan('Faint', { opacity: 0.5 })) + if (attrs & ATTR_FRAKTUR) target.appendChild(makeSpan('Fraktur')) + } + // tooltip updateTooltip = function () { + tooltip.classList.remove('hidden') tooltip.innerHTML = '' let cell = selectedCell[1] * screen.window.width + selectedCell[0] if (!screen.screen[cell]) return let foreground = document.createElement('span') - foreground.textContent = screen.screenFG[cell] + foreground.textContent = formatColor(screen.screenFG[cell]) let preview = document.createElement('span') preview.textContent = ' ●' preview.style.color = getColor(screen.screenFG[cell], screen.layout.renderer.palette) foreground.appendChild(preview) let background = document.createElement('span') - background.textContent = screen.screenBG[cell] + background.textContent = formatColor(screen.screenBG[cell]) let bgPreview = document.createElement('span') bgPreview.textContent = ' ●' bgPreview.style.color = getColor(screen.screenBG[cell], screen.layout.renderer.palette) @@ -248,10 +315,15 @@ module.exports = function attachDebugger (screen, connection) { ? `0000${codePoint.toString(16)}`.substr(-4) : codePoint.toString(16) + let attributes = document.createElement('span') + attributes.classList.add('attributes') + formatAttributes(attributes, screen.screenAttrs[cell]) + let data = { Foreground: foreground, Background: background, - Character: `U+${formattedCodePoint}` + Character: `U+${formattedCodePoint}`, + Attributes: attributes } let table = document.createElement('table') @@ -275,4 +347,129 @@ module.exports = function attachDebugger (screen, connection) { tooltip.style.transform = `translate(${mousePosition.map(x => x + 'px').join(',')})` } + + let toolbarData = null + let toolbarNodes = {} + const initToolbar = function () { + if (toolbarData) return + + let showUpdates = document.createElement('input') + showUpdates.type = 'checkbox' + showUpdates.addEventListener('change', e => { + drawData.showUpdates = showUpdates.checked + }) + + let fancyGraphics = document.createElement('input') + fancyGraphics.type = 'checkbox' + fancyGraphics.value = !!screen.layout.window.graphics + fancyGraphics.addEventListener('change', e => { + screen.layout.window.graphics = +fancyGraphics.checked + }) + + toolbarData = { + cursor: { + title: 'Cursor', + Position: '', + Style: '', + Visible: true, + Hanging: false + }, + internal: { + Flags: '', + 'Cursor Attributes': '', + 'Code Page': '', + Heap: 0, + Clients: 0 + }, + drawing: { + title: 'Drawing', + 'Show Updates': showUpdates, + 'Fancy Graphics': fancyGraphics, + 'Redraw Screen': () => { + screen.layout.renderer.resetDrawn() + screen.layout.renderer.draw('debug-redraw') + } + } + } + + for (let i in toolbarData) { + let group = toolbarData[i] + let table = document.createElement('table') + table.classList.add('toolbar-group') + + toolbarNodes[i] = {} + + for (let key in group) { + let item = document.createElement('tr') + let name = document.createElement('td') + name.classList.add('name') + let value = document.createElement('td') + value.classList.add('value') + + toolbarNodes[i][key] = { name, value } + + if (key === 'title') { + name.textContent = group[key] + name.classList.add('title') + } else { + name.textContent = key + if (group[key] instanceof Function) { + name.textContent = '' + let button = document.createElement('button') + name.classList.add('has-button') + name.appendChild(button) + button.textContent = key + button.addEventListener('click', e => group[key](e)) + } else if (group[key] instanceof window.Node) value.appendChild(group[key]) + else value.textContent = group[key] + } + + item.appendChild(name) + item.appendChild(value) + table.appendChild(item) + } + + toolbar.appendChild(table) + } + } + + updateToolbar = function () { + initToolbar() + + Object.assign(toolbarData.cursor, { + Position: `col ${screen.cursor.x}, ln ${screen.cursor.y}`, + Style: screen.cursor.style + (screen.cursor.blinking ? ', blink' : ''), + Visible: screen.cursor.visible, + Hanging: screen.cursor.hanging + }) + + toolbarData.drawing['Fancy Graphics'].checked = !!screen.layout.window.graphics + + for (let i in toolbarData) { + let group = toolbarData[i] + let nodes = toolbarNodes[i] + for (let key in group) { + if (key === 'title') continue + let value = nodes[key].value + if (!(group[key] instanceof window.Node) && !(group[key] instanceof Function)) { + value.textContent = group[key] + } + } + } + } + + screen.on('update', updateToolbar) + screen.on('internal', data => { + if (screenAttached && toolbarData) { + Object.assign(toolbarData.internal, { + Flags: data.flags.toString(2), + 'Cursor Attributes': data.cursorAttrs.toString(2), + 'Code Page': `${data.charsetGx} (${data.charsetG0}, ${data.charsetG1})`, + Heap: data.freeHeap, + Clients: data.clientCount + }) + drawData.scrollRegion = [data.regionStart, data.regionEnd] + updateToolbar() + } + }) } diff --git a/js/term/screen.js b/js/term/screen.js index 86e67af..a092b72 100644 --- a/js/term/screen.js +++ b/js/term/screen.js @@ -549,5 +549,7 @@ module.exports = class TermScreen extends EventEmitter { console.warn('Unhandled update', update) } } + + this.emit('update') } } diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index 1dd41c0..d14a70e 100644 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -117,9 +117,11 @@ body.term { left: 0; pointer-events: none; background: #fff; + color: #000; box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3); border-radius: 6px; - padding: 10px 15px; + padding: 6px 10px; + font-size: 12px; line-height: 1; table { @@ -132,19 +134,62 @@ body.term { .value { text-align: left; + + .attributes { + &:empty::before { + content: 'None' + } + + span:not(:last-of-type)::after { + content: ', ' + } + } } } } } .debug-toolbar { - line-height: 1.5; + line-height: 1.2; text-align: left; - padding: 6px 12px 12px 12px; - font-family: $screen-stack; + margin: 6px 12px 12px 12px; + padding: 6px; + background: #fff; + color: #000; + border-radius: 6px; font-size: 12px; white-space: normal; + .toolbar-group { + display: inline-block; + vertical-align: top; + margin: 0 1em; + + tr { + .name { + font-weight: bold; + text-align: right; + opacity: 0.5; + + &.title, &.has-button { + opacity: 1; + } + + button { + background: none; + font: inherit; + text-shadow: none; + box-shadow: none; + color: #2ea1f9; + font-weight: bold; + text-align: right; + padding: 0; + margin: 0; + } + } + } + } + .toolbar-buttons { button { margin-right: 5px; From ed59b0cb446e55148e443e67d68a81425b806a00 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 27 Oct 2017 14:13:08 +0200 Subject: [PATCH 4/7] Add (back) heartbeat to debug toolbar --- js/term/debug.js | 14 ++++++++++++++ sass/pages/_term.scss | 6 ------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/js/term/debug.js b/js/term/debug.js index bd92a9c..958381b 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -431,8 +431,22 @@ module.exports = function attachDebugger (screen, connection) { toolbar.appendChild(table) } + + let heartbeat = toolbarNodes.heartbeat = document.createElement('div') + heartbeat.classList.add('heartbeat') + heartbeat.textContent = '❤' + toolbar.appendChild(heartbeat) } + connection.on('heartbeat', () => { + if (screenAttached && toolbarNodes.heartbeat) { + toolbarNodes.heartbeat.classList.remove('beat') + window.requestAnimationFrame(() => { + toolbarNodes.heartbeat.classList.add('beat') + }) + } + }) + updateToolbar = function () { initToolbar() diff --git a/sass/pages/_term.scss b/sass/pages/_term.scss index d14a70e..fe590b3 100644 --- a/sass/pages/_term.scss +++ b/sass/pages/_term.scss @@ -190,12 +190,6 @@ body.term { } } - .toolbar-buttons { - button { - margin-right: 5px; - } - } - .heartbeat { float: right; font-family: $font-stack; From 64108302fc23e0c3e7a649c90edd8eaf71932635 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 27 Oct 2017 14:18:11 +0200 Subject: [PATCH 5/7] Show last screen update in debug toolbar --- js/term/debug.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/js/term/debug.js b/js/term/debug.js index 958381b..a22c75a 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -383,6 +383,7 @@ module.exports = function attachDebugger (screen, connection) { }, drawing: { title: 'Drawing', + 'Last Update': '', 'Show Updates': showUpdates, 'Fancy Graphics': fancyGraphics, 'Redraw Screen': () => { @@ -457,6 +458,8 @@ module.exports = function attachDebugger (screen, connection) { Hanging: screen.cursor.hanging }) + let drawTime = Math.round((drawData.endTime - drawData.startTime) * 100) / 100 + toolbarData.drawing['Last Update'] = `${drawData.reason} (${drawTime}ms)` toolbarData.drawing['Fancy Graphics'].checked = !!screen.layout.window.graphics for (let i in toolbarData) { From 3490e220b5b5cd679639a3500e430a9ce58d4ce1 Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 27 Oct 2017 14:59:39 +0200 Subject: [PATCH 6/7] Round debug tooltip pos, use 1-based coords --- js/term/debug.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/js/term/debug.js b/js/term/debug.js index a22c75a..3ae76a5 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -320,6 +320,7 @@ module.exports = function attachDebugger (screen, connection) { formatAttributes(attributes, screen.screenAttrs[cell]) let data = { + Cell: `col ${selectedCell[0] + 1}, ln ${selectedCell[1] + 1} (${cell})`, Foreground: foreground, Background: background, Character: `U+${formattedCodePoint}`, @@ -345,7 +346,11 @@ module.exports = function attachDebugger (screen, connection) { tooltip.appendChild(table) - tooltip.style.transform = `translate(${mousePosition.map(x => x + 'px').join(',')})` + let cellSize = screen.layout.getCellSize() + // add 3 to the position because for some reason the corner is off + let posX = (selectedCell[0] + 1) * cellSize.width + 3 + let posY = (selectedCell[1] + 1) * cellSize.height + 3 + tooltip.style.transform = `translate(${posX}px, ${posY}px)` } let toolbarData = null @@ -452,7 +457,7 @@ module.exports = function attachDebugger (screen, connection) { initToolbar() Object.assign(toolbarData.cursor, { - Position: `col ${screen.cursor.x}, ln ${screen.cursor.y}`, + Position: `col ${screen.cursor.x + 1}, ln ${screen.cursor.y + 1}`, Style: screen.cursor.style + (screen.cursor.blinking ? ', blink' : ''), Visible: screen.cursor.visible, Hanging: screen.cursor.hanging From a83e4549e8066e8c1b50f2bb0dd5095a10b0abbb Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 27 Oct 2017 15:15:14 +0200 Subject: [PATCH 7/7] Scroll region is inclusive --- js/term/debug.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/term/debug.js b/js/term/debug.js index 3ae76a5..21a9960 100644 --- a/js/term/debug.js +++ b/js/term/debug.js @@ -251,8 +251,8 @@ module.exports = function attachDebugger (screen, connection) { ctx.stroke() ctx.beginPath() - ctx.moveTo(0, end * cellSize.height) - ctx.lineTo(width * cellSize.width, end * cellSize.height) + ctx.moveTo(0, (end + 1) * cellSize.height) + ctx.lineTo(width * cellSize.width, (end + 1) * cellSize.height) ctx.stroke() ctx.restore()