diff --git a/jssrc/term_screen.js b/jssrc/term_screen.js index 1a4f0ed..c798e4a 100644 --- a/jssrc/term_screen.js +++ b/jssrc/term_screen.js @@ -82,16 +82,12 @@ window.TermScreen = class TermScreen { this.cursor = { x: 0, y: 0, - fg: 7, - bg: 0, - attrs: 0, blinkOn: false, blinking: true, visible: true, hanging: false, style: 'block', - blinkEnable: true, - blinkInterval: 0 + blinkInterval: null } this._palette = null @@ -131,15 +127,18 @@ window.TermScreen = class TermScreen { // though alt can be held to override it selectable: true, + // selection start and end (x, y) tuples start: [0, 0], end: [0, 0] } + // mouse features this.mouseMode = { clicks: false, movement: false } // event listeners this._listeners = {} + // make writing to window update size and draw const self = this this.window = new Proxy(this._window, { set (target, key, value, receiver) { @@ -157,13 +156,15 @@ window.TermScreen = class TermScreen { this.screenBG = [] 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.drawnScreenFG = [] this.drawnScreenBG = [] this.drawnScreenAttrs = [] this.drawnCursor = [-1, -1, ''] + // start blink timers this.resetBlink() this.resetCursorBlink() @@ -190,6 +191,8 @@ window.TermScreen = class TermScreen { Object.assign(this.selection, this.getNormalizedSelection()) } + // bind event listeners + this.canvas.addEventListener('mousedown', e => { if ((this.selection.selectable || e.altKey) && e.button === 0) { selectStart(e.offsetX, e.offsetY) @@ -207,6 +210,8 @@ window.TermScreen = class TermScreen { selectEnd(e.offsetX, e.offsetY) }) + // touch event listeners + let touchPosition = null let touchDownTime = 0 let touchSelectMinTime = 500 @@ -247,6 +252,7 @@ window.TermScreen = class TermScreen { e.preventDefault() selectEnd(...touchPosition) + // selection ended; show touch select menu let touchSelectMenu = qs('#touch-select-menu') touchSelectMenu.classList.add('open') 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) { if (!this._listeners[event]) this._listeners[event] = [] 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) { if (!this._listeners[event]) this._listeners[event] = [] 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) { let listeners = this._listeners[event] 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) { let listeners = this._listeners[event] if (listeners) { @@ -376,6 +402,10 @@ window.TermScreen = class TermScreen { } } + /** + * The color palette. Should define 16 colors in an array. + * @type {number[]} + */ get palette () { 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) { // return palette color if it exists if (this.palette[i]) return this.palette[i] @@ -411,24 +449,44 @@ window.TermScreen = class TermScreen { return 'rgba(0, 0, 0, 0)' } - // schedule a size update in the next tick + /** + * Schedule a size update in the next millisecond + */ scheduleSizeUpdate () { clearTimeout(this._scheduledSizeUpdate) 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) { clearTimeout(this._scheduledDraw) 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 = {}) { let fontStyle = modifiers.style || 'normal' let fontWeight = modifiers.weight || 'normal' 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 () { 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 () { let charSize = this.getCharSize() @@ -447,6 +509,9 @@ window.TermScreen = class TermScreen { } } + /** + * Updates the canvas size if it changed + */ updateSize () { 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 () { this.cursor.blinkOn = true clearInterval(this.cursor.blinkInterval) @@ -524,6 +592,9 @@ window.TermScreen = class TermScreen { }, 500) } + /** + * Resets the blink style to on and restarts the timer + */ resetBlink () { this.window.blinkStyleOn = true clearInterval(this.window.blinkInterval) @@ -542,6 +613,11 @@ window.TermScreen = class TermScreen { }, 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 () { let { start, end } = this.selection // 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 } } + /** + * 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) { let { start, end } = this.getNormalizedSelection() let colAfterStart = start[0] <= col @@ -565,6 +647,10 @@ window.TermScreen = class TermScreen { else return start[1] < line && line < end[1] } + /** + * Sweeps for selected cells and joins them in a multiline string. + * @returns {string} the selection + */ getSelectedText () { const screenLength = this.window.width * this.window.height let lines = [] @@ -586,6 +672,9 @@ window.TermScreen = class TermScreen { return lines.join('\n') } + /** + * Copies the selection to clipboard and creates a notification balloon. + */ copySelectionToClipboard () { let selectedText = this.getSelectedText() // don't copy anything if nothing is selected @@ -602,6 +691,12 @@ window.TermScreen = class TermScreen { 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) { 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) { 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 }) { const ctx = this.ctx 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)) } + /** + * 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 }) { if (!text) return @@ -671,6 +795,12 @@ window.TermScreen = class TermScreen { 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) { const { width, height } = this.window const screenLength = width * height @@ -687,6 +817,10 @@ window.TermScreen = class TermScreen { return cells.filter(cell => cell >= 0 && cell < screenLength) } + /** + * Updates the screen. + * @param {string} why - the draw reason (for debugging) + */ draw (why) { const ctx = this.ctx const { @@ -726,7 +860,8 @@ window.TermScreen = class TermScreen { let isCursor = !this.cursor.hanging && this.cursor.x === x && this.cursor.y === y && - this.cursor.blinkOn + this.cursor.blinkOn && + this.cursor.visible 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() } + /** + * Parses the content of an `S` message and schedules a draw + * @param {string} str - the message content + */ loadContent (str) { // current index let i = 0 @@ -1075,7 +1214,11 @@ window.TermScreen = class TermScreen { 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) { let pieces = str.split('\x01') 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) { console.info(`Notification: ${text}`) 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) { const content = str.substr(1) if (theme >= 0 && theme < themes.length) { @@ -1133,6 +1285,9 @@ window.TermScreen = class TermScreen { } } + /** + * Creates a beep sound. + */ beep () { const audioCtx = this.audioCtx if (!audioCtx) return @@ -1166,6 +1321,11 @@ window.TermScreen = class TermScreen { 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) { if (character >= 'a' && character <= 'z') { character = String.fromCodePoint(0x1d51e - 0x61 + character.charCodeAt(0))