From ef4c8670078e188d9f3202f79ea603982c04b3cc Mon Sep 17 00:00:00 2001 From: cpsdqs Date: Fri, 27 Oct 2017 14:00:11 +0200 Subject: [PATCH] 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;