Document TermScreen

cpsdqs/unified-input
cpsdqs 7 years ago
parent f74e689196
commit 3e743ef397
Signed by untrusted user: cpsdqs
GPG Key ID: 3F59586BB7448DD1
  1. 182
      jssrc/term_screen.js

@ -82,16 +82,12 @@ window.TermScreen = class TermScreen {
this.cursor = { this.cursor = {
x: 0, x: 0,
y: 0, y: 0,
fg: 7,
bg: 0,
attrs: 0,
blinkOn: false, blinkOn: false,
blinking: true, blinking: true,
visible: true, visible: true,
hanging: false, hanging: false,
style: 'block', style: 'block',
blinkEnable: true, blinkInterval: null
blinkInterval: 0
} }
this._palette = null this._palette = null
@ -131,15 +127,18 @@ window.TermScreen = class TermScreen {
// though alt can be held to override it // though alt can be held to override it
selectable: true, selectable: true,
// selection start and end (x, y) tuples
start: [0, 0], start: [0, 0],
end: [0, 0] end: [0, 0]
} }
// mouse features
this.mouseMode = { clicks: false, movement: false } this.mouseMode = { clicks: false, movement: false }
// event listeners // event listeners
this._listeners = {} this._listeners = {}
// make writing to window update size and draw
const self = this const self = this
this.window = new Proxy(this._window, { this.window = new Proxy(this._window, {
set (target, key, value, receiver) { set (target, key, value, receiver) {
@ -157,13 +156,15 @@ window.TermScreen = class TermScreen {
this.screenBG = [] this.screenBG = []
this.screenAttrs = [] this.screenAttrs = []
// used to determine if a cell should be redrawn // used to determine if a cell should be redrawn; storing the current state
// as it is on screen
this.drawnScreen = [] this.drawnScreen = []
this.drawnScreenFG = [] this.drawnScreenFG = []
this.drawnScreenBG = [] this.drawnScreenBG = []
this.drawnScreenAttrs = [] this.drawnScreenAttrs = []
this.drawnCursor = [-1, -1, ''] this.drawnCursor = [-1, -1, '']
// start blink timers
this.resetBlink() this.resetBlink()
this.resetCursorBlink() this.resetCursorBlink()
@ -190,6 +191,8 @@ window.TermScreen = class TermScreen {
Object.assign(this.selection, this.getNormalizedSelection()) Object.assign(this.selection, this.getNormalizedSelection())
} }
// bind event listeners
this.canvas.addEventListener('mousedown', e => { this.canvas.addEventListener('mousedown', e => {
if ((this.selection.selectable || e.altKey) && e.button === 0) { if ((this.selection.selectable || e.altKey) && e.button === 0) {
selectStart(e.offsetX, e.offsetY) selectStart(e.offsetX, e.offsetY)
@ -207,6 +210,8 @@ window.TermScreen = class TermScreen {
selectEnd(e.offsetX, e.offsetY) selectEnd(e.offsetX, e.offsetY)
}) })
// touch event listeners
let touchPosition = null let touchPosition = null
let touchDownTime = 0 let touchDownTime = 0
let touchSelectMinTime = 500 let touchSelectMinTime = 500
@ -247,6 +252,7 @@ window.TermScreen = class TermScreen {
e.preventDefault() e.preventDefault()
selectEnd(...touchPosition) selectEnd(...touchPosition)
// selection ended; show touch select menu
let touchSelectMenu = qs('#touch-select-menu') let touchSelectMenu = qs('#touch-select-menu')
touchSelectMenu.classList.add('open') touchSelectMenu.classList.add('open')
let rect = touchSelectMenu.getBoundingClientRect() let rect = touchSelectMenu.getBoundingClientRect()
@ -333,16 +339,31 @@ window.TermScreen = class TermScreen {
}) })
} }
/**
* Bind an event listener to an event
* @param {string} event - the event name
* @param {Function} listener - the event listener
*/
on (event, listener) { on (event, listener) {
if (!this._listeners[event]) this._listeners[event] = [] if (!this._listeners[event]) this._listeners[event] = []
this._listeners[event].push({ listener }) this._listeners[event].push({ listener })
} }
/**
* Bind an event listener to be run only once the next time the event fires
* @param {string} event - the event name
* @param {Function} listener - the event listener
*/
once (event, listener) { once (event, listener) {
if (!this._listeners[event]) this._listeners[event] = [] if (!this._listeners[event]) this._listeners[event] = []
this._listeners[event].push({ listener, once: true }) this._listeners[event].push({ listener, once: true })
} }
/**
* Remove an event listener
* @param {string} event - the event name
* @param {Function} listener - the event listener
*/
off (event, listener) { off (event, listener) {
let listeners = this._listeners[event] let listeners = this._listeners[event]
if (listeners) { if (listeners) {
@ -355,6 +376,11 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* Emits an event
* @param {string} event - the event name
* @param {...any} args - arguments passed to all listeners
*/
emit (event, ...args) { emit (event, ...args) {
let listeners = this._listeners[event] let listeners = this._listeners[event]
if (listeners) { if (listeners) {
@ -376,6 +402,10 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* The color palette. Should define 16 colors in an array.
* @type {number[]}
*/
get palette () { get palette () {
return this._palette || themes[0] return this._palette || themes[0]
} }
@ -387,6 +417,14 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* Returns the specified color. If `i` is in the palette, it will return the
* palette color. If `i` is between 16 and 255, it will return the 256color
* value. If `i` is larger than 255, it will return an RGB color value. If `i`
* is -1 (foreground) or -2 (background), it will return the selection colors.
* @param {number} i - the color
* @returns {string} the CSS color
*/
getColor (i) { getColor (i) {
// return palette color if it exists // return palette color if it exists
if (this.palette[i]) return this.palette[i] if (this.palette[i]) return this.palette[i]
@ -411,24 +449,44 @@ window.TermScreen = class TermScreen {
return 'rgba(0, 0, 0, 0)' return 'rgba(0, 0, 0, 0)'
} }
// schedule a size update in the next tick /**
* Schedule a size update in the next millisecond
*/
scheduleSizeUpdate () { scheduleSizeUpdate () {
clearTimeout(this._scheduledSizeUpdate) clearTimeout(this._scheduledSizeUpdate)
this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1) this._scheduledSizeUpdate = setTimeout(() => this.updateSize(), 1)
} }
// schedule a draw in the next tick /**
* Schedule a draw in the next millisecond
* @param {string} why - the reason why the draw occured (for debugging)
* @param {number} [aggregateTime] - time to wait for more scheduleDraw calls
* to occur. 1 ms by default.
*/
scheduleDraw (why, aggregateTime = 1) { scheduleDraw (why, aggregateTime = 1) {
clearTimeout(this._scheduledDraw) clearTimeout(this._scheduledDraw)
this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime) this._scheduledDraw = setTimeout(() => this.draw(why), aggregateTime)
} }
/**
* Returns a CSS font string with this TermScreen's font settings and the
* font modifiers.
* @param {Object} modifiers
* @param {string} [modifiers.style] - the font style
* @param {string} [modifiers.weight] - the font weight
* @returns {string} a CSS font string
*/
getFont (modifiers = {}) { getFont (modifiers = {}) {
let fontStyle = modifiers.style || 'normal' let fontStyle = modifiers.style || 'normal'
let fontWeight = modifiers.weight || 'normal' let fontWeight = modifiers.weight || 'normal'
return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${this.window.fontFamily}` return `${fontStyle} normal ${fontWeight} ${this.window.fontSize}px ${this.window.fontFamily}`
} }
/**
* The character size, used for calculating the cell size. The space character
* is used for measuring.
* @returns {Object} the character size with `width` and `height` in pixels
*/
getCharSize () { getCharSize () {
this.ctx.font = this.getFont() this.ctx.font = this.getFont()
@ -438,6 +496,10 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* The cell size, which is the character size multiplied by the grid scale.
* @returns {Object} the cell size with `width` and `height` in pixels
*/
getCellSize () { getCellSize () {
let charSize = this.getCharSize() let charSize = this.getCharSize()
@ -447,6 +509,9 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* Updates the canvas size if it changed
*/
updateSize () { updateSize () {
this._window.devicePixelRatio = window.devicePixelRatio || 1 this._window.devicePixelRatio = window.devicePixelRatio || 1
@ -513,6 +578,9 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* Resets the cursor blink to on and restarts the timer
*/
resetCursorBlink () { resetCursorBlink () {
this.cursor.blinkOn = true this.cursor.blinkOn = true
clearInterval(this.cursor.blinkInterval) clearInterval(this.cursor.blinkInterval)
@ -524,6 +592,9 @@ window.TermScreen = class TermScreen {
}, 500) }, 500)
} }
/**
* Resets the blink style to on and restarts the timer
*/
resetBlink () { resetBlink () {
this.window.blinkStyleOn = true this.window.blinkStyleOn = true
clearInterval(this.window.blinkInterval) clearInterval(this.window.blinkInterval)
@ -542,6 +613,11 @@ window.TermScreen = class TermScreen {
}, 200) }, 200)
} }
/**
* Returns a normalized version of the current selection, such that `start`
* is always before `end`.
* @returns {Object} the normalized selection, with `start` and `end`
*/
getNormalizedSelection () { getNormalizedSelection () {
let { start, end } = this.selection let { start, end } = this.selection
// if the start line is after the end line, or if they're both on the same // if the start line is after the end line, or if they're both on the same
@ -552,6 +628,12 @@ window.TermScreen = class TermScreen {
return { start, end } return { start, end }
} }
/**
* Returns whether or not a given cell is in the current selection.
* @param {number} col - the column (x)
* @param {number} line - the line (y)
* @returns {boolean}
*/
isInSelection (col, line) { isInSelection (col, line) {
let { start, end } = this.getNormalizedSelection() let { start, end } = this.getNormalizedSelection()
let colAfterStart = start[0] <= col let colAfterStart = start[0] <= col
@ -565,6 +647,10 @@ window.TermScreen = class TermScreen {
else return start[1] < line && line < end[1] else return start[1] < line && line < end[1]
} }
/**
* Sweeps for selected cells and joins them in a multiline string.
* @returns {string} the selection
*/
getSelectedText () { getSelectedText () {
const screenLength = this.window.width * this.window.height const screenLength = this.window.width * this.window.height
let lines = [] let lines = []
@ -586,6 +672,9 @@ window.TermScreen = class TermScreen {
return lines.join('\n') return lines.join('\n')
} }
/**
* Copies the selection to clipboard and creates a notification balloon.
*/
copySelectionToClipboard () { copySelectionToClipboard () {
let selectedText = this.getSelectedText() let selectedText = this.getSelectedText()
// don't copy anything if nothing is selected // don't copy anything if nothing is selected
@ -602,6 +691,12 @@ window.TermScreen = class TermScreen {
document.body.removeChild(textarea) document.body.removeChild(textarea)
} }
/**
* Converts screen coordinates to grid coordinates.
* @param {number} x - x in pixels
* @param {number} y - y in pixels
* @returns {number[]} a tuple of (x, y) in cells
*/
screenToGrid (x, y) { screenToGrid (x, y) {
let cellSize = this.getCellSize() let cellSize = this.getCellSize()
@ -611,12 +706,27 @@ window.TermScreen = class TermScreen {
] ]
} }
/**
* Converts grid coordinates to screen coordinates.
* @param {number} x - x in cells
* @param {number} y - y in cells
* @returns {number[]} a tuple of (x, y) in pixels
*/
gridToScreen (x, y) { gridToScreen (x, y) {
let cellSize = this.getCellSize() let cellSize = this.getCellSize()
return [ x * cellSize.width, y * cellSize.height ] return [x * cellSize.width, y * cellSize.height]
} }
/**
* Draws a cell's background with the given parameters.
* @param {Object} options
* @param {number} options.x - x in cells
* @param {number} options.y - y in cells
* @param {number} options.cellWidth - cell width in pixels
* @param {number} options.cellHeight - cell height in pixels
* @param {number} options.bg - the background color
*/
drawCellBackground ({ x, y, cellWidth, cellHeight, bg }) { drawCellBackground ({ x, y, cellWidth, cellHeight, bg }) {
const ctx = this.ctx const ctx = this.ctx
ctx.fillStyle = this.getColor(bg) ctx.fillStyle = this.getColor(bg)
@ -624,6 +734,20 @@ window.TermScreen = class TermScreen {
ctx.fillRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight)) ctx.fillRect(x * cellWidth, y * cellHeight, Math.ceil(cellWidth), Math.ceil(cellHeight))
} }
/**
* Draws a cell's character with the given parameters. Won't do anything if
* text is an empty string.
* @param {Object} options
* @param {number} options.x - x in cells
* @param {number} options.y - y in cells
* @param {Object} options.charSize - the character size, an object with
* `width` and `height` in pixels
* @param {number} options.cellWidth - cell width in pixels
* @param {number} options.cellHeight - cell height in pixels
* @param {string} options.text - the cell content
* @param {number} options.fg - the foreground color
* @param {number} options.attrs - the cell's attributes
*/
drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) { drawCell ({ x, y, charSize, cellWidth, cellHeight, text, fg, attrs }) {
if (!text) return if (!text) return
@ -671,6 +795,12 @@ window.TermScreen = class TermScreen {
ctx.globalAlpha = 1 ctx.globalAlpha = 1
} }
/**
* Returns all adjacent cell indices given a radius.
* @param {number} cell - the center cell index
* @param {number} [radius] - the radius. 1 by default
* @returns {number[]} an array of cell indices
*/
getAdjacentCells (cell, radius = 1) { getAdjacentCells (cell, radius = 1) {
const { width, height } = this.window const { width, height } = this.window
const screenLength = width * height const screenLength = width * height
@ -687,6 +817,10 @@ window.TermScreen = class TermScreen {
return cells.filter(cell => cell >= 0 && cell < screenLength) return cells.filter(cell => cell >= 0 && cell < screenLength)
} }
/**
* Updates the screen.
* @param {string} why - the draw reason (for debugging)
*/
draw (why) { draw (why) {
const ctx = this.ctx const ctx = this.ctx
const { const {
@ -726,7 +860,8 @@ window.TermScreen = class TermScreen {
let isCursor = !this.cursor.hanging && let isCursor = !this.cursor.hanging &&
this.cursor.x === x && this.cursor.x === x &&
this.cursor.y === y && this.cursor.y === y &&
this.cursor.blinkOn this.cursor.blinkOn &&
this.cursor.visible
let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1] let wasCursor = x === this.drawnCursor[0] && y === this.drawnCursor[1]
@ -899,6 +1034,10 @@ window.TermScreen = class TermScreen {
if (this.window.debug && this._debug) this._debug.drawEnd() if (this.window.debug && this._debug) this._debug.drawEnd()
} }
/**
* Parses the content of an `S` message and schedules a draw
* @param {string} str - the message content
*/
loadContent (str) { loadContent (str) {
// current index // current index
let i = 0 let i = 0
@ -1075,7 +1214,11 @@ window.TermScreen = class TermScreen {
this.emit('load') this.emit('load')
} }
/** Apply labels to buttons and screen title (leading T removed already) */ /**
* Parses the content of a `T` message and updates the screen title and button
* labels.
* @param {string} str - the message content
*/
loadLabels (str) { loadLabels (str) {
let pieces = str.split('\x01') let pieces = str.split('\x01')
qs('h1').textContent = pieces[0] qs('h1').textContent = pieces[0]
@ -1088,6 +1231,10 @@ window.TermScreen = class TermScreen {
}) })
} }
/**
* Shows an actual notification (if possible) or a notification balloon.
* @param {string} text - the notification content
*/
showNotification (text) { showNotification (text) {
console.info(`Notification: ${text}`) console.info(`Notification: ${text}`)
if (Notification && Notification.permission === 'granted') { if (Notification && Notification.permission === 'granted') {
@ -1105,6 +1252,11 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* Loads a message from the server, and optionally a theme.
* @param {string} str - the message
* @param {number} [theme] - the new theme index
*/
load (str, theme = -1) { load (str, theme = -1) {
const content = str.substr(1) const content = str.substr(1)
if (theme >= 0 && theme < themes.length) { if (theme >= 0 && theme < themes.length) {
@ -1133,6 +1285,9 @@ window.TermScreen = class TermScreen {
} }
} }
/**
* Creates a beep sound.
*/
beep () { beep () {
const audioCtx = this.audioCtx const audioCtx = this.audioCtx
if (!audioCtx) return if (!audioCtx) return
@ -1166,6 +1321,11 @@ window.TermScreen = class TermScreen {
osc.stop(audioCtx.currentTime + 0.08) osc.stop(audioCtx.currentTime + 0.08)
} }
/**
* Converts an alphabetic character to its fraktur variant.
* @param {string} character - the character
* @returns {string} the converted character
*/
static alphaToFraktur (character) { static alphaToFraktur (character) {
if (character >= 'a' && character <= 'z') { if (character >= 'a' && character <= 'z') {
character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0)) character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0))

Loading…
Cancel
Save