Browse Source

Merge branch 'fancy-debug' into work

cpsdqs 2 years ago
parent
commit
78d32f7fdc
5 changed files with 469 additions and 300 deletions
  1. 390 290
      js/term/debug.js
  2. 2 0
      js/term/screen.js
  3. 2 0
      js/term/screen_layout.js
  4. 2 2
      js/term/screen_renderer.js
  5. 73 8
      sass/pages/_term.scss

+ 390 - 290
js/term/debug.js View File

@@ -1,397 +1,497 @@
1
-const { mk } = require('../utils')
1
+const { getColor } = require('./themes')
2
+const {
3
+  ATTR_FG,
4
+  ATTR_BG,
5
+  ATTR_BOLD,
6
+  ATTR_UNDERLINE,
7
+  ATTR_BLINK,
8
+  ATTR_ITALIC,
9
+  ATTR_STRIKE,
10
+  ATTR_OVERLINE,
11
+  ATTR_FAINT,
12
+  ATTR_FRAKTUR
13
+} = require('./screen_attr_bits')
2 14
 
3 15
 module.exports = function attachDebugger (screen, connection) {
4
-  const debugCanvas = mk('canvas')
16
+  const debugCanvas = document.createElement('canvas')
17
+  debugCanvas.classList.add('debug-canvas')
5 18
   const ctx = debugCanvas.getContext('2d')
6 19
 
7
-  debugCanvas.classList.add('debug-canvas')
20
+  const toolbar = document.createElement('div')
21
+  toolbar.classList.add('debug-toolbar')
8 22
 
9
-  let mouseHoverCell = null
23
+  const tooltip = document.createElement('div')
24
+  tooltip.classList.add('debug-tooltip')
25
+  tooltip.classList.add('hidden')
26
+
27
+  let updateTooltip
10 28
   let updateToolbar
11 29
 
12
-  let onMouseMove = e => {
13
-    mouseHoverCell = screen.layout.screenToGrid(e.offsetX, e.offsetY)
14
-    startDrawing()
15
-    updateToolbar()
30
+  let selectedCell = null
31
+  let mousePosition = null
32
+  const onMouseMove = (e) => {
33
+    if (e.target !== screen.layout.canvas) {
34
+      selectedCell = null
35
+      return
36
+    }
37
+    selectedCell = screen.layout.screenToGrid(e.offsetX, e.offsetY)
38
+    mousePosition = [e.offsetX, e.offsetY]
39
+    updateTooltip()
40
+  }
41
+  const onMouseOut = (e) => {
42
+    selectedCell = null
43
+    tooltip.classList.add('hidden')
44
+  }
45
+
46
+  const updateCanvasSize = function () {
47
+    let { width, height, devicePixelRatio } = screen.layout.window
48
+    let cellSize = screen.layout.getCellSize()
49
+    let padding = Math.round(screen.layout._padding)
50
+    debugCanvas.width = (width * cellSize.width + 2 * padding) * devicePixelRatio
51
+    debugCanvas.height = (height * cellSize.height + 2 * padding) * devicePixelRatio
52
+    debugCanvas.style.width = `${width * cellSize.width + 2 * screen.layout._padding}px`
53
+    debugCanvas.style.height = `${height * cellSize.height + 2 * screen.layout._padding}px`
16 54
   }
17
-  let onMouseOut = () => (mouseHoverCell = null)
18 55
 
19
-  let addCanvas = function () {
20
-    if (!debugCanvas.parentNode) {
56
+  let startDrawLoop
57
+  let screenAttached = false
58
+  let eventNode
59
+  const setScreenAttached = function (attached) {
60
+    if (attached && !debugCanvas.parentNode) {
21 61
       screen.layout.canvas.parentNode.appendChild(debugCanvas)
22
-      screen.layout.canvas.addEventListener('mousemove', onMouseMove)
23
-      screen.layout.canvas.addEventListener('mouseout', onMouseOut)
24
-    }
25
-  }
26
-  let removeCanvas = function () {
27
-    if (debugCanvas.parentNode) {
62
+      eventNode = debugCanvas.parentNode
63
+      eventNode.addEventListener('mousemove', onMouseMove)
64
+      eventNode.addEventListener('mouseout', onMouseOut)
65
+      screen.layout.on('size-update', updateCanvasSize)
66
+      updateCanvasSize()
67
+      screenAttached = true
68
+      startDrawLoop()
69
+    } else if (!attached && debugCanvas.parentNode) {
28 70
       debugCanvas.parentNode.removeChild(debugCanvas)
29
-      screen.layout.canvas.removeEventListener('mousemove', onMouseMove)
30
-      screen.layout.canvas.removeEventListener('mouseout', onMouseOut)
31
-      onMouseOut()
71
+      eventNode.removeEventListener('mousemove', onMouseMove)
72
+      eventNode.removeEventListener('mouseout', onMouseOut)
73
+      screen.layout.removeListener('size-update', updateCanvasSize)
74
+      screenAttached = false
32 75
     }
33 76
   }
34
-  let updateCanvasSize = function () {
35
-    let { width, height, devicePixelRatio } = screen.layout.window
36
-    let cellSize = screen.layout.getCellSize()
37
-    debugCanvas.width = width * cellSize.width * devicePixelRatio
38
-    debugCanvas.height = height * cellSize.height * devicePixelRatio
39
-    debugCanvas.style.width = `${width * cellSize.width}px`
40
-    debugCanvas.style.height = `${height * cellSize.height}px`
77
+
78
+  const setToolbarAttached = function (attached) {
79
+    if (attached && !toolbar.parentNode) {
80
+      screen.layout.canvas.parentNode.appendChild(toolbar)
81
+      screen.layout.canvas.parentNode.appendChild(tooltip)
82
+      updateToolbar()
83
+    } else if (!attached && toolbar.parentNode) {
84
+      screen.layout.canvas.parentNode.removeChild(toolbar)
85
+      screen.layout.canvas.parentNode.removeChild(tooltip)
86
+    }
41 87
   }
42 88
 
43
-  let drawInfo = mk('div')
44
-  drawInfo.classList.add('draw-info')
89
+  screen.on('update-window:debug', enabled => {
90
+    setToolbarAttached(enabled)
91
+  })
45 92
 
46
-  let startTime, endTime, lastReason
47
-  let cells = new Map()
48
-  let clippedRects = []
49
-  let updateFrames = []
93
+  screen.layout.on('update-window:debug', enabled => {
94
+    setScreenAttached(enabled)
95
+  })
50 96
 
51
-  let startDrawing
97
+  let drawData = {
98
+    reason: '',
99
+    showUpdates: false,
100
+    startTime: 0,
101
+    endTime: 0,
102
+    clipped: [],
103
+    frames: [],
104
+    cells: new Map(),
105
+    scrollRegion: null
106
+  }
52 107
 
53 108
   screen._debug = screen.layout.renderer._debug = {
54 109
     drawStart (reason) {
55
-      lastReason = reason
56
-      startTime = Date.now()
57
-      clippedRects = []
110
+      drawData.reason = reason
111
+      drawData.startTime = window.performance.now()
58 112
     },
59 113
     drawEnd () {
60
-      endTime = Date.now()
61
-      drawInfo.textContent = `Draw: ${lastReason} (${(endTime - startTime)} ms), fancy gfx=${screen.layout.renderer.graphics}`
62
-      startDrawing()
114
+      drawData.endTime = window.performance.now()
63 115
     },
64 116
     setCell (cell, flags) {
65
-      cells.set(cell, [flags, Date.now()])
66
-    },
67
-    clipRect (...args) {
68
-      clippedRects.push(args)
117
+      drawData.cells.set(cell, [flags, window.performance.now()])
69 118
     },
70 119
     pushFrame (frame) {
71
-      frame.push(Date.now())
72
-      updateFrames.push(frame)
73
-      startDrawing()
120
+      drawData.frames.push([...frame, window.performance.now()])
74 121
     }
75 122
   }
76 123
 
77
-  let clipPattern
78
-  {
79
-    let patternCanvas = document.createElement('canvas')
80
-    patternCanvas.width = patternCanvas.height = 12
81
-    let pctx = patternCanvas.getContext('2d')
82
-    pctx.lineWidth = 1
83
-    pctx.strokeStyle = '#00f'
84
-    pctx.beginPath()
85
-    pctx.moveTo(0, 0)
86
-    pctx.lineTo(0 - 4, 12)
87
-    pctx.moveTo(4, 0)
88
-    pctx.lineTo(4 - 4, 12)
89
-    pctx.moveTo(8, 0)
90
-    pctx.lineTo(8 - 4, 12)
91
-    pctx.moveTo(12, 0)
92
-    pctx.lineTo(12 - 4, 12)
93
-    pctx.moveTo(16, 0)
94
-    pctx.lineTo(16 - 4, 12)
95
-    pctx.stroke()
96
-    clipPattern = ctx.createPattern(patternCanvas, 'repeat')
97
-  }
98
-
99 124
   let isDrawing = false
100
-  let lastDrawTime = 0
101
-  let t = 0
102
-
103 125
   let drawLoop = function () {
104
-    if (isDrawing) window.requestAnimationFrame(drawLoop)
126
+    if (screenAttached) window.requestAnimationFrame(drawLoop)
127
+    else isDrawing = false
105 128
 
106
-    let dt = (Date.now() - lastDrawTime) / 1000
107
-    lastDrawTime = Date.now()
108
-    t += dt
129
+    let now = window.performance.now()
109 130
 
110
-    let { devicePixelRatio, width, height } = screen.layout.window
111
-    let { width: cellWidth, height: cellHeight } = screen.layout.getCellSize()
112
-    let screenLength = width * height
113
-    let now = Date.now()
131
+    let { width, height, devicePixelRatio } = screen.layout.window
132
+    let padding = Math.round(screen.layout._padding)
133
+    let cellSize = screen.layout.getCellSize()
114 134
 
115 135
     ctx.setTransform(devicePixelRatio, 0, 0, devicePixelRatio, 0, 0)
116
-    ctx.clearRect(0, 0, width * cellWidth, height * cellHeight)
136
+    ctx.clearRect(0, 0, width * cellSize.width + 2 * padding, height * cellSize.height + 2 * padding)
137
+    ctx.translate(padding, padding)
117 138
 
118
-    let activeCells = 0
119
-    for (let cell = 0; cell < screenLength; cell++) {
120
-      if (!cells.has(cell) || cells.get(cell)[0] === 0) continue
139
+    ctx.lineWidth = 2
140
+    ctx.lineJoin = 'round'
121 141
 
122
-      let [flags, timestamp] = cells.get(cell)
123
-      let elapsedTime = (now - timestamp) / 1000
142
+    if (drawData.showUpdates) {
143
+      const cells = drawData.cells
144
+      for (let cell = 0; cell < width * height; cell++) {
145
+        // cell does not exist or has no flags set
146
+        if (!cells.has(cell) || cells.get(cell)[0] === 0) continue
124 147
 
125
-      if (elapsedTime > 1) continue
148
+        const [flags, timestamp] = cells.get(cell)
149
+        let elapsedTime = (now - timestamp) / 1000
126 150
 
127
-      activeCells++
128
-      ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime)
151
+        if (elapsedTime > 1) {
152
+          cells.delete(cell)
153
+          continue
154
+        }
129 155
 
130
-      let x = cell % width
131
-      let y = Math.floor(cell / width)
156
+        ctx.globalAlpha = 0.5 * Math.max(0, 1 - elapsedTime)
132 157
 
133
-      if (flags & 1) {
134
-        // redrawn
135
-        ctx.fillStyle = '#f0f'
136
-      }
137
-      if (flags & 2) {
138
-        // updated
139
-        ctx.fillStyle = '#0f0'
140
-      }
158
+        let x = cell % width
159
+        let y = Math.floor(cell / width)
141 160
 
142
-      ctx.fillRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight)
161
+        if (flags & 2) {
162
+          // updated
163
+          ctx.fillStyle = '#0f0'
164
+        } else if (flags & 1) {
165
+          // redrawn
166
+          ctx.fillStyle = '#f0f'
167
+        }
143 168
 
144
-      if (flags & 4) {
145
-        // wide cell
146
-        ctx.lineWidth = 2
147
-        ctx.strokeStyle = '#f00'
148
-        ctx.strokeRect(x * cellWidth, y * cellHeight, cellWidth, cellHeight)
149
-      }
150
-    }
169
+        if (!(flags & 4)) {
170
+          // outside a clipped region
171
+          ctx.fillStyle = '#0ff'
172
+        }
151 173
 
152
-    if (clippedRects.length) {
153
-      ctx.globalAlpha = 0.5
154
-      ctx.beginPath()
174
+        ctx.fillRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
155 175
 
156
-      for (let rect of clippedRects) {
157
-        ctx.rect(...rect)
176
+        if (flags & 8) {
177
+          // wide cell
178
+          ctx.strokeStyle = '#f00'
179
+          ctx.beginPath()
180
+          ctx.moveTo(x * cellSize.width, (y + 1) * cellSize.height)
181
+          ctx.lineTo((x + 1) * cellSize.width, (y + 1) * cellSize.height)
182
+          ctx.stroke()
183
+        }
158 184
       }
159 185
 
160
-      ctx.fillStyle = clipPattern
161
-      ctx.fill()
162
-    }
163
-
164
-    let didDrawUpdateFrames = false
165
-    if (updateFrames.length) {
166 186
       let framesToDelete = []
167
-      for (let frame of updateFrames) {
168
-        let time = frame[4]
169
-        let elapsed = Date.now() - time
170
-        if (elapsed > 1000) framesToDelete.push(frame)
187
+      for (let frame of drawData.frames) {
188
+        let timestamp = frame[4]
189
+        let elapsedTime = (now - timestamp) / 1000
190
+        if (elapsedTime > 1) framesToDelete.push(frame)
171 191
         else {
172
-          didDrawUpdateFrames = true
173
-          ctx.globalAlpha = 1 - elapsed / 1000
192
+          ctx.globalAlpha = 1 - elapsedTime
174 193
           ctx.strokeStyle = '#ff0'
175
-          ctx.lineWidth = 2
176
-          ctx.strokeRect(frame[0] * cellWidth, frame[1] * cellHeight, frame[2] * cellWidth, frame[3] * cellHeight)
194
+          ctx.strokeRect(frame[0] * cellSize.width, frame[1] * cellSize.height,
195
+            frame[2] * cellSize.width, frame[3] * cellSize.height)
177 196
         }
178 197
       }
179 198
       for (let frame of framesToDelete) {
180
-        updateFrames.splice(updateFrames.indexOf(frame), 1)
199
+        drawData.frames.splice(drawData.frames.indexOf(frame), 1)
181 200
       }
182 201
     }
183 202
 
184
-    if (mouseHoverCell) {
203
+    if (selectedCell !== null) {
204
+      let [x, y] = selectedCell
205
+
185 206
       ctx.save()
207
+      ctx.globalAlpha = 0.5
208
+      ctx.lineWidth = 1
209
+
210
+      // draw X line
211
+      ctx.beginPath()
212
+      ctx.moveTo(0, y * cellSize.height)
213
+      ctx.lineTo(x * cellSize.width, y * cellSize.height)
214
+      ctx.strokeStyle = '#f00'
215
+      ctx.setLineDash([cellSize.width])
216
+      ctx.stroke()
217
+
218
+      // draw Y line
219
+      ctx.beginPath()
220
+      ctx.moveTo(x * cellSize.width, 0)
221
+      ctx.lineTo(x * cellSize.width, y * cellSize.height)
222
+      ctx.strokeStyle = '#0f0'
223
+      ctx.setLineDash([cellSize.height])
224
+      ctx.stroke()
225
+
186 226
       ctx.globalAlpha = 1
187
-      ctx.lineWidth = 1 + 0.5 * Math.sin(t * 10)
227
+      ctx.lineWidth = 1 + 0.5 * Math.sin((now / 1000) * 10)
188 228
       ctx.strokeStyle = '#fff'
189 229
       ctx.lineJoin = 'round'
190 230
       ctx.setLineDash([2, 2])
191
-      ctx.lineDashOffset = t * 10
192
-      ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight)
231
+      ctx.lineDashOffset = (now / 1000) * 10
232
+      ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
193 233
       ctx.lineDashOffset += 2
194 234
       ctx.strokeStyle = '#000'
195
-      ctx.strokeRect(mouseHoverCell[0] * cellWidth, mouseHoverCell[1] * cellHeight, cellWidth, cellHeight)
235
+      ctx.strokeRect(x * cellSize.width, y * cellSize.height, cellSize.width, cellSize.height)
196 236
       ctx.restore()
197 237
     }
198 238
 
199
-    if (activeCells === 0 && !mouseHoverCell && !didDrawUpdateFrames) {
200
-      isDrawing = false
201
-      removeCanvas()
239
+    if (drawData.scrollRegion !== null) {
240
+      let [start, end] = drawData.scrollRegion
241
+
242
+      ctx.save()
243
+      ctx.globalAlpha = 1
244
+      ctx.strokeStyle = '#00f'
245
+      ctx.lineWidth = 2
246
+      ctx.setLineDash([2, 2])
247
+
248
+      ctx.beginPath()
249
+      ctx.moveTo(0, start * cellSize.height)
250
+      ctx.lineTo(width * cellSize.width, start * cellSize.height)
251
+      ctx.stroke()
252
+
253
+      ctx.beginPath()
254
+      ctx.moveTo(0, (end + 1) * cellSize.height)
255
+      ctx.lineTo(width * cellSize.width, (end + 1) * cellSize.height)
256
+      ctx.stroke()
257
+
258
+      ctx.restore()
202 259
     }
203 260
   }
204
-
205
-  startDrawing = function () {
261
+  startDrawLoop = function () {
206 262
     if (isDrawing) return
207
-    addCanvas()
208
-    updateCanvasSize()
209 263
     isDrawing = true
210
-    lastDrawTime = Date.now()
211 264
     drawLoop()
212 265
   }
213 266
 
214
-  // debug toolbar
215
-  const toolbar = mk('div')
216
-  toolbar.classList.add('debug-toolbar')
217
-  let toolbarAttached = false
267
+  let pad2 = i => ('00' + i.toString()).substr(-2)
268
+  let formatColor = color => color < 256
269
+    ? color.toString()
270
+    : '#' + pad2(color >> 16) + pad2((color >> 8) & 0xFF) + pad2(color & 0xFF)
218 271
 
219
-  const heartbeat = mk('div')
220
-  heartbeat.classList.add('heartbeat')
221
-  heartbeat.textContent = '❤'
222
-  toolbar.appendChild(heartbeat)
272
+  let makeSpan = (text, styles) => {
273
+    let span = document.createElement('span')
274
+    span.textContent = text
275
+    Object.assign(span.style, styles || {})
276
+    return span
277
+  }
278
+  let formatAttributes = (target, attrs) => {
279
+    if (attrs & ATTR_FG) target.appendChild(makeSpan('HasFG'))
280
+    if (attrs & ATTR_BG) target.appendChild(makeSpan('HasBG'))
281
+    if (attrs & ATTR_BOLD) target.appendChild(makeSpan('Bold', { fontWeight: 'bold' }))
282
+    if (attrs & ATTR_UNDERLINE) target.appendChild(makeSpan('Uline', { textDecoration: 'underline' }))
283
+    if (attrs & ATTR_BLINK) target.appendChild(makeSpan('Blink'))
284
+    if (attrs & ATTR_ITALIC) target.appendChild(makeSpan('Italic', { fontStyle: 'italic' }))
285
+    if (attrs & ATTR_STRIKE) target.appendChild(makeSpan('Strike', { textDecoration: 'line-through' }))
286
+    if (attrs & ATTR_OVERLINE) target.appendChild(makeSpan('Oline', { textDecoration: 'overline' }))
287
+    if (attrs & ATTR_FAINT) target.appendChild(makeSpan('Faint', { opacity: 0.5 }))
288
+    if (attrs & ATTR_FRAKTUR) target.appendChild(makeSpan('Fraktur'))
289
+  }
223 290
 
224
-  const dataDisplay = mk('div')
225
-  dataDisplay.classList.add('data-display')
226
-  toolbar.appendChild(dataDisplay)
291
+  // tooltip
292
+  updateTooltip = function () {
293
+    tooltip.classList.remove('hidden')
294
+    tooltip.innerHTML = ''
295
+    let cell = selectedCell[1] * screen.window.width + selectedCell[0]
296
+    if (!screen.screen[cell]) return
297
+
298
+    let foreground = document.createElement('span')
299
+    foreground.textContent = formatColor(screen.screenFG[cell])
300
+    let preview = document.createElement('span')
301
+    preview.textContent = ' ●'
302
+    preview.style.color = getColor(screen.screenFG[cell], screen.layout.renderer.palette)
303
+    foreground.appendChild(preview)
304
+
305
+    let background = document.createElement('span')
306
+    background.textContent = formatColor(screen.screenBG[cell])
307
+    let bgPreview = document.createElement('span')
308
+    bgPreview.textContent = ' ●'
309
+    bgPreview.style.color = getColor(screen.screenBG[cell], screen.layout.renderer.palette)
310
+    background.appendChild(bgPreview)
311
+
312
+    let character = screen.screen[cell]
313
+    let codePoint = character.codePointAt(0)
314
+    let formattedCodePoint = codePoint.toString(16).length <= 4
315
+      ? `0000${codePoint.toString(16)}`.substr(-4)
316
+      : codePoint.toString(16)
317
+
318
+    let attributes = document.createElement('span')
319
+    attributes.classList.add('attributes')
320
+    formatAttributes(attributes, screen.screenAttrs[cell])
321
+
322
+    let data = {
323
+      Cell: `col ${selectedCell[0] + 1}, ln ${selectedCell[1] + 1} (${cell})`,
324
+      Foreground: foreground,
325
+      Background: background,
326
+      Character: `U+${formattedCodePoint}`,
327
+      Attributes: attributes
328
+    }
227 329
 
228
-  const internalDisplay = mk('div')
229
-  internalDisplay.classList.add('internal-display')
230
-  toolbar.appendChild(internalDisplay)
330
+    let table = document.createElement('table')
231 331
 
232
-  toolbar.appendChild(drawInfo)
332
+    for (let name in data) {
333
+      let row = document.createElement('tr')
334
+      let label = document.createElement('td')
335
+      label.appendChild(new window.Text(name))
336
+      label.classList.add('label')
233 337
 
234
-  const buttons = mk('div')
235
-  buttons.classList.add('toolbar-buttons')
236
-  toolbar.appendChild(buttons)
338
+      let value = document.createElement('td')
339
+      value.appendChild(typeof data[name] === 'string' ? new window.Text(data[name]) : data[name])
340
+      value.classList.add('value')
237 341
 
238
-  // heartbeat
239
-  connection.on('heartbeat', () => {
240
-    heartbeat.classList.remove('beat')
241
-    window.requestAnimationFrame(() => {
242
-      heartbeat.classList.add('beat')
243
-    })
244
-  })
342
+      row.appendChild(label)
343
+      row.appendChild(value)
344
+      table.appendChild(row)
345
+    }
245 346
 
246
-  {
247
-    const redraw = mk('button')
248
-    redraw.textContent = 'Redraw'
249
-    redraw.addEventListener('click', e => {
250
-      screen.layout.renderer.resetDrawn()
251
-      screen.layout.renderer.draw('debug-redraw')
252
-    })
253
-    buttons.appendChild(redraw)
347
+    tooltip.appendChild(table)
254 348
 
255
-    const fancyGraphics = mk('button')
256
-    fancyGraphics.textContent = 'Toggle Fancy Graphics'
257
-    fancyGraphics.addEventListener('click', e => {
258
-      screen.layout.renderer.graphics = +!screen.layout.renderer.graphics
259
-      screen.layout.renderer.draw('set-graphics')
260
-    })
261
-    buttons.appendChild(fancyGraphics)
349
+    let cellSize = screen.layout.getCellSize()
350
+    // add 3 to the position because for some reason the corner is off
351
+    let posX = (selectedCell[0] + 1) * cellSize.width + 3
352
+    let posY = (selectedCell[1] + 1) * cellSize.height + 3
353
+    tooltip.style.transform = `translate(${posX}px, ${posY}px)`
262 354
   }
263 355
 
264
-  const attachToolbar = function () {
265
-    screen.layout.canvas.parentNode.appendChild(toolbar)
266
-  }
267
-  const detachToolbar = function () {
268
-    toolbar.parentNode.removeChild(toolbar)
269
-  }
356
+  let toolbarData = null
357
+  let toolbarNodes = {}
358
+  const initToolbar = function () {
359
+    if (toolbarData) return
270 360
 
271
-  screen.on('update-window:debug', debug => {
272
-    if (debug !== toolbarAttached) {
273
-      toolbarAttached = debug
274
-      if (debug) attachToolbar()
275
-      else {
276
-        detachToolbar()
277
-        removeCanvas()
278
-      }
279
-    }
280
-  })
361
+    let showUpdates = document.createElement('input')
362
+    showUpdates.type = 'checkbox'
363
+    showUpdates.addEventListener('change', e => {
364
+      drawData.showUpdates = showUpdates.checked
365
+    })
281 366
 
282
-  const displayCellAttrs = attrs => {
283
-    let result = attrs.toString(16)
284
-    if (attrs & 1 || attrs & 2) {
285
-      result += ':has('
286
-      if (attrs & 1) result += 'fg'
287
-      if (attrs & 2) result += (attrs & 1 ? ',' : '') + 'bg'
288
-      result += ')'
367
+    let fancyGraphics = document.createElement('input')
368
+    fancyGraphics.type = 'checkbox'
369
+    fancyGraphics.value = !!screen.layout.window.graphics
370
+    fancyGraphics.addEventListener('change', e => {
371
+      screen.layout.window.graphics = +fancyGraphics.checked
372
+    })
373
+
374
+    toolbarData = {
375
+      cursor: {
376
+        title: 'Cursor',
377
+        Position: '',
378
+        Style: '',
379
+        Visible: true,
380
+        Hanging: false
381
+      },
382
+      internal: {
383
+        Flags: '',
384
+        'Cursor Attributes': '',
385
+        'Code Page': '',
386
+        Heap: 0,
387
+        Clients: 0
388
+      },
389
+      drawing: {
390
+        title: 'Drawing',
391
+        'Last Update': '',
392
+        'Show Updates': showUpdates,
393
+        'Fancy Graphics': fancyGraphics,
394
+        'Redraw Screen': () => {
395
+          screen.layout.renderer.resetDrawn()
396
+          screen.layout.renderer.draw('debug-redraw')
397
+        }
398
+      }
289 399
     }
290
-    let attributes = []
291
-    if (attrs & (1 << 2)) attributes.push('\\[bold]bold\\()')
292
-    if (attrs & (1 << 3)) attributes.push('\\[underline]underln\\()')
293
-    if (attrs & (1 << 4)) attributes.push('\\[invert]invert\\()')
294
-    if (attrs & (1 << 5)) attributes.push('blink')
295
-    if (attrs & (1 << 6)) attributes.push('\\[italic]italic\\()')
296
-    if (attrs & (1 << 7)) attributes.push('\\[strike]strike\\()')
297
-    if (attrs & (1 << 8)) attributes.push('\\[overline]overln\\()')
298
-    if (attrs & (1 << 9)) attributes.push('\\[faint]faint\\()')
299
-    if (attrs & (1 << 10)) attributes.push('fraktur')
300
-    if (attributes.length) result += ':' + attributes.join()
301
-    return result.trim()
302
-  }
303 400
 
304
-  const formatColor = color => color < 256 ? color : `#${`000000${(color - 256).toString(16)}`.substr(-6)}`
305
-  const getCellData = cell => {
306
-    if (cell < 0 || cell > screen.screen.length) return '(-)'
307
-    let cellAttrs = screen.layout.renderer.drawnScreenAttrs[cell] | 0
308
-    let cellFG = screen.layout.renderer.drawnScreenFG[cell] | 0
309
-    let cellBG = screen.layout.renderer.drawnScreenBG[cell] | 0
310
-    let fgText = formatColor(cellFG)
311
-    let bgText = formatColor(cellBG)
312
-    fgText += `\\[color=${screen.layout.renderer.getColor(cellFG).replace(/ /g, '')}]●\\[]`
313
-    bgText += `\\[color=${screen.layout.renderer.getColor(cellBG).replace(/ /g, '')}]●\\[]`
314
-    let cellCode = (screen.layout.renderer.drawnScreen[cell] || '').codePointAt(0) | 0
315
-    let hexcode = cellCode.toString(16).toUpperCase()
316
-    if (hexcode.length < 4) hexcode = `0000${hexcode}`.substr(-4)
317
-    hexcode = `U+${hexcode}`
318
-    let x = cell % screen.window.width
319
-    let y = Math.floor(cell / screen.window.width)
320
-    return `((${y},${x})=${cell}:\\[bold]${hexcode}\\[]:F${fgText}:B${bgText}:A(${displayCellAttrs(cellAttrs)}))`
321
-  }
401
+    for (let i in toolbarData) {
402
+      let group = toolbarData[i]
403
+      let table = document.createElement('table')
404
+      table.classList.add('toolbar-group')
322 405
 
323
-  const setFormattedText = (node, text) => {
324
-    node.innerHTML = ''
406
+      toolbarNodes[i] = {}
325 407
 
326
-    let match
327
-    let attrs = {}
408
+      for (let key in group) {
409
+        let item = document.createElement('tr')
410
+        let name = document.createElement('td')
411
+        name.classList.add('name')
412
+        let value = document.createElement('td')
413
+        value.classList.add('value')
328 414
 
329
-    let pushSpan = content => {
330
-      let span = mk('span')
331
-      node.appendChild(span)
332
-      span.textContent = content
333
-      for (let key in attrs) span[key] = attrs[key]
334
-    }
415
+        toolbarNodes[i][key] = { name, value }
335 416
 
336
-    while ((match = text.match(/\\\[(.*?)\]/))) {
337
-      if (match.index > 0) pushSpan(text.substr(0, match.index))
338
-
339
-      attrs = { style: '' }
340
-      let data = match[1].split(' ')
341
-      for (let attr of data) {
342
-        if (!attr) continue
343
-        let key, value
344
-        if (attr.indexOf('=') > -1) {
345
-          key = attr.substr(0, attr.indexOf('='))
346
-          value = attr.substr(attr.indexOf('=') + 1)
417
+        if (key === 'title') {
418
+          name.textContent = group[key]
419
+          name.classList.add('title')
347 420
         } else {
348
-          key = attr
349
-          value = true
421
+          name.textContent = key
422
+          if (group[key] instanceof Function) {
423
+            name.textContent = ''
424
+            let button = document.createElement('button')
425
+            name.classList.add('has-button')
426
+            name.appendChild(button)
427
+            button.textContent = key
428
+            button.addEventListener('click', e => group[key](e))
429
+          } else if (group[key] instanceof window.Node) value.appendChild(group[key])
430
+          else value.textContent = group[key]
350 431
         }
351 432
 
352
-        if (key === 'bold') attrs.style += 'font-weight:bold;'
353
-        if (key === 'italic') attrs.style += 'font-style:italic;'
354
-        if (key === 'underline') attrs.style += 'text-decoration:underline;'
355
-        if (key === 'invert') attrs.style += 'background:#000;filter:invert(1);'
356
-        if (key === 'strike') attrs.style += 'text-decoration:line-through;'
357
-        if (key === 'overline') attrs.style += 'text-decoration:overline;'
358
-        if (key === 'faint') attrs.style += 'opacity:0.5;'
359
-        else if (key === 'color') attrs.style += `color:${value};`
360
-        else attrs[key] = value
433
+        item.appendChild(name)
434
+        item.appendChild(value)
435
+        table.appendChild(item)
361 436
       }
362 437
 
363
-      text = text.substr(match.index + match[0].length)
438
+      toolbar.appendChild(table)
364 439
     }
365 440
 
366
-    if (text) pushSpan(text)
441
+    let heartbeat = toolbarNodes.heartbeat = document.createElement('div')
442
+    heartbeat.classList.add('heartbeat')
443
+    heartbeat.textContent = '❤'
444
+    toolbar.appendChild(heartbeat)
367 445
   }
368 446
 
369
-  let internalInfo = {}
370
-
371
-  updateToolbar = () => {
372
-    if (!toolbarAttached) return
373
-    let text = `C((${screen.cursor.y},${screen.cursor.x}),hang:${screen.cursor.hanging},vis:${screen.cursor.visible})`
374
-    if (mouseHoverCell) {
375
-      text += ' m' + getCellData(mouseHoverCell[1] * screen.window.width + mouseHoverCell[0])
447
+  connection.on('heartbeat', () => {
448
+    if (screenAttached && toolbarNodes.heartbeat) {
449
+      toolbarNodes.heartbeat.classList.remove('beat')
450
+      window.requestAnimationFrame(() => {
451
+        toolbarNodes.heartbeat.classList.add('beat')
452
+      })
376 453
     }
377
-    setFormattedText(dataDisplay, text)
378
-
379
-    if ('flags' in internalInfo) {
380
-      // we got ourselves some internal data
381
-      let text = ' '
382
-      text += ` flags:${internalInfo.flags.toString(2)}`
383
-      text += ` curAttrs:${internalInfo.cursorAttrs.toString(2)}`
384
-      text += ` Region:${internalInfo.regionStart}->${internalInfo.regionEnd}`
385
-      text += ` Charset:${internalInfo.charsetGx} (0:${internalInfo.charsetG0},1:${internalInfo.charsetG1})`
386
-      text += ` Heap:${internalInfo.freeHeap}`
387
-      text += ` Clients:${internalInfo.clientCount}`
388
-      setFormattedText(internalDisplay, text)
454
+  })
455
+
456
+  updateToolbar = function () {
457
+    initToolbar()
458
+
459
+    Object.assign(toolbarData.cursor, {
460
+      Position: `col ${screen.cursor.x + 1}, ln ${screen.cursor.y + 1}`,
461
+      Style: screen.cursor.style + (screen.cursor.blinking ? ', blink' : ''),
462
+      Visible: screen.cursor.visible,
463
+      Hanging: screen.cursor.hanging
464
+    })
465
+
466
+    let drawTime = Math.round((drawData.endTime - drawData.startTime) * 100) / 100
467
+    toolbarData.drawing['Last Update'] = `${drawData.reason} (${drawTime}ms)`
468
+    toolbarData.drawing['Fancy Graphics'].checked = !!screen.layout.window.graphics
469
+
470
+    for (let i in toolbarData) {
471
+      let group = toolbarData[i]
472
+      let nodes = toolbarNodes[i]
473
+      for (let key in group) {
474
+        if (key === 'title') continue
475
+        let value = nodes[key].value
476
+        if (!(group[key] instanceof window.Node) && !(group[key] instanceof Function)) {
477
+          value.textContent = group[key]
478
+        }
479
+      }
389 480
     }
390 481
   }
391 482
 
392
-  screen.on('draw', updateToolbar)
483
+  screen.on('update', updateToolbar)
393 484
   screen.on('internal', data => {
394
-    internalInfo = data
395
-    updateToolbar()
485
+    if (screenAttached && toolbarData) {
486
+      Object.assign(toolbarData.internal, {
487
+        Flags: data.flags.toString(2),
488
+        'Cursor Attributes': data.cursorAttrs.toString(2),
489
+        'Code Page': `${data.charsetGx} (${data.charsetG0}, ${data.charsetG1})`,
490
+        Heap: data.freeHeap,
491
+        Clients: data.clientCount
492
+      })
493
+      drawData.scrollRegion = [data.regionStart, data.regionEnd]
494
+      updateToolbar()
495
+    }
396 496
   })
397 497
 }

+ 2 - 0
js/term/screen.js View File

@@ -549,5 +549,7 @@ module.exports = class TermScreen extends EventEmitter {
549 549
           console.warn('Unhandled update', update)
550 550
       }
551 551
     }
552
+
553
+    this.emit('update')
552 554
   }
553 555
 }

+ 2 - 0
js/term/screen_layout.js View File

@@ -247,6 +247,8 @@ module.exports = class ScreenLayout extends EventEmitter {
247 247
       this.renderer.resetDrawn()
248 248
 
249 249
       this.renderer.render('update-size', this.serializeRenderData())
250
+
251
+      this.emit('size-update')
250 252
     }
251 253
   }
252 254
 

+ 2 - 2
js/term/screen_renderer.js View File

@@ -639,7 +639,6 @@ module.exports = class CanvasRenderer extends EventEmitter {
639 639
         if (y === height - 1) rectHeight += padding
640 640
 
641 641
         ctx.rect(rectX, rectY, rectWidth, rectHeight)
642
-        if (this.debug && this._debug) this._debug.clipRect(rectX, rectY, rectWidth, rectHeight)
643 642
       }
644 643
 
645 644
       ctx.save()
@@ -674,7 +673,8 @@ module.exports = class CanvasRenderer extends EventEmitter {
674 673
             // set cell flags
675 674
             let flags = (+redrawMap.get(cell))
676 675
             flags |= (+updateMap.get(cell)) << 1
677
-            flags |= (+isTextWide(text)) << 2
676
+            flags |= (+maskedCells.get(cell)) << 2
677
+            flags |= (+isTextWide(text)) << 3
678 678
             this._debug.setCell(cell, flags)
679 679
           }
680 680
         }

+ 73 - 8
sass/pages/_term.scss View File

@@ -106,22 +106,87 @@ body.term {
106 106
 
107 107
 	.debug-canvas {
108 108
 		position: absolute;
109
-		top: 6px;
110
-		left: 6px;
109
+		top: 0;
110
+		left: 0;
111 111
 		pointer-events: none;
112 112
 	}
113 113
 
114
+	.debug-tooltip {
115
+		position: absolute;
116
+		top: 0;
117
+		left: 0;
118
+		pointer-events: none;
119
+		background: #fff;
120
+		color: #000;
121
+		box-shadow: 0 4px 10px rgba(0, 0, 0, 0.3);
122
+		border-radius: 6px;
123
+		padding: 6px 10px;
124
+		font-size: 12px;
125
+		line-height: 1;
126
+
127
+		table {
128
+			tr {
129
+				.label {
130
+					font-weight: bold;
131
+					text-align: right;
132
+					opacity: 0.5;
133
+				}
134
+
135
+				.value {
136
+					text-align: left;
137
+
138
+					.attributes {
139
+						&:empty::before {
140
+							content: 'None'
141
+						}
142
+
143
+						span:not(:last-of-type)::after {
144
+							content: ', '
145
+						}
146
+					}
147
+				}
148
+			}
149
+		}
150
+	}
151
+
114 152
 	.debug-toolbar {
115
-		line-height: 1.5;
153
+		line-height: 1.2;
116 154
 		text-align: left;
117
-		padding: 6px 12px 12px 12px;
118
-		font-family: $screen-stack;
155
+		margin: 6px 12px 12px 12px;
156
+		padding: 6px;
157
+		background: #fff;
158
+		color: #000;
159
+		border-radius: 6px;
119 160
 		font-size: 12px;
120 161
 		white-space: normal;
121 162
 
122
-		.toolbar-buttons {
123
-			button {
124
-				margin-right: 5px;
163
+		.toolbar-group {
164
+			display: inline-block;
165
+			vertical-align: top;
166
+			margin: 0 1em;
167
+
168
+			tr {
169
+				.name {
170
+					font-weight: bold;
171
+					text-align: right;
172
+					opacity: 0.5;
173
+
174
+					&.title, &.has-button {
175
+						opacity: 1;
176
+					}
177
+
178
+					button {
179
+						background: none;
180
+						font: inherit;
181
+						text-shadow: none;
182
+						box-shadow: none;
183
+						color: #2ea1f9;
184
+						font-weight: bold;
185
+						text-align: right;
186
+						padding: 0;
187
+						margin: 0;
188
+					}
189
+				}
125 190
 			}
126 191
 		}
127 192