diff --git a/src/bootstrap/galaxy.ts b/src/bootstrap/galaxy.ts index 59cde13c..9303150e 100644 --- a/src/bootstrap/galaxy.ts +++ b/src/bootstrap/galaxy.ts @@ -88,4 +88,7 @@ dispatch.sub('window.change', () => { }) // TODO(smolck): Put this somewhere else? -api.onAction('update-nameplates', () => (windows.refresh(), console.log('refresh'))) +api.onAction( + 'update-nameplates', + () => (windows.refresh(), console.log('refresh')) +) diff --git a/src/bootstrap/index.html b/src/bootstrap/index.html index f07f6a57..6f3e8c43 100644 --- a/src/bootstrap/index.html +++ b/src/bootstrap/index.html @@ -166,8 +166,6 @@
-
-
diff --git a/src/components/extensions/grep.tsx b/src/components/extensions/grep.tsx index 98cb546a..fcef0e57 100644 --- a/src/components/extensions/grep.tsx +++ b/src/components/extensions/grep.tsx @@ -1,7 +1,7 @@ import { RowNormal, RowHeader } from '../row-container' import { PluginRight } from '../plugin-container' import { vimBlur, vimFocus } from '../../ui/uikit' -import { showCursorline } from '../../core/cursor' +// TODO(smolck): import { showCursorline } from '../../core/cursor' import Input from '../text-input' import { badgeStyle } from '../../ui/styles' import { render } from 'inferno' @@ -67,7 +67,7 @@ const selectResult = (results: Result[], ix: number, subix: number) => { const [path, items] = results[ix] const { line, column } = items[subix] api.nvim.jumpTo({ path, line, column }) - showCursorline() + // TODO(smolck): showCursorline() } const highlightPattern = ( diff --git a/src/components/extensions/lsp-references.tsx b/src/components/extensions/lsp-references.tsx index 20f75c17..1f557f9c 100644 --- a/src/components/extensions/lsp-references.tsx +++ b/src/components/extensions/lsp-references.tsx @@ -2,7 +2,7 @@ import { RowNormal, RowHeader } from '../row-container' import { PluginRight } from '../plugin-container' import { vimBlur, vimFocus } from '../../ui/uikit' import { simplifyPath } from '../../support/utils' -import { showCursorline } from '../../core/cursor' +// TODO(smolck): import { showCursorline } from '../../core/cursor' import { badgeStyle } from '../../ui/styles' import { render } from 'inferno' import Input from '../text-input' @@ -82,7 +82,7 @@ const selectResult = (references: Refs[], ix: number, subix: number) => { line: lineNum - 1, column: column - 1, }) - showCursorline() + // TODO(smolck): showCursorline() } const highlightPattern = ( diff --git a/src/components/text-input.tsx b/src/components/text-input.tsx index 0f574b1f..df2803e4 100644 --- a/src/components/text-input.tsx +++ b/src/components/text-input.tsx @@ -1,6 +1,5 @@ import Loading from './loading' import { paddingVH, cvar } from '../ui/css' -import { xfrmUp } from '../core/input' import { FormEvent } from 'inferno' import Icon from './icon' @@ -59,25 +58,6 @@ const nopMaybe = (obj: object) => get: (_, key) => Reflect.get(obj, key) || (() => {}), }) as Props -const keToStr = (e: KeyboardEvent) => - [ - e.key, - (e.ctrlKey as any) | 0, - (e.metaKey as any) | 0, - (e.altKey as any) | 0, - (e.shiftKey as any) | 0, - ].join('') - -// TODO: could be better? it's global so will be shared between different -// inputs. however only one input will have focus at a time, so perhaps -// it's not a big deal -// -// the reason this has to live outside the function is because the view -// function will be triggered on re-render. pressing keys will potentially -// trigger re-renders, thus reseting the value of lastDown when inside -// the function. -let lastDown = '' - // TODO(smolck): But why though? Has to be another way to get access to // `onComponentDidMount` with normal stuff like const WhyInput = (props: any) => @@ -167,24 +147,10 @@ const textInput = ( setFocus(e.currentTarget, focus) setPosition(e.currentTarget, position) }} - onKeyUp={(e: KeyboardEvent) => { - const prevKeyAndThisOne = lastDown + keToStr(e) - - if (xfrmUp.has(prevKeyAndThisOne)) { - const { key } = xfrmUp.get(prevKeyAndThisOne)!(e) - if (key.toLowerCase() === '') { - lastDown = '' - ;(e.target as HTMLInputElement).blur() - return $.hide() - } - } - }} onKeyDown={(e: KeyboardEvent) => { const { ctrlKey: ctrl, metaKey: meta, key } = e const cm = ctrl || meta - lastDown = keToStr(e) - if (key === 'Tab') { e.preventDefault() return $.tab() diff --git a/src/core/cursor.ts b/src/core/cursor.ts index a68e2a14..2660825a 100644 --- a/src/core/cursor.ts +++ b/src/core/cursor.ts @@ -1,93 +1,39 @@ import * as windows from '../windows/window-manager' -import { partialFill, translate } from '../ui/css' -import { paddingX } from '../windows/window' -import { cell } from '../core/workspace' +import { hexToRGB } from '../ui/css' export enum CursorShape { - block, - line, - underline, + block = 0, + line = 1, + underline = 2, } export const cursor = { + visible: false, row: 0, col: 0, - color: '#fff', + color: [0, 0, 0], shape: CursorShape.block, + size: 20, } -const cursorEl = document.getElementById('cursor') as HTMLElement -const cursorChar = document.createElement('span') -const cursorline = document.getElementById('cursorline') as HTMLElement -export const debugline = document.getElementById('debugline') as HTMLElement +let cursorEnabled = false let cursorRequestedToBeHidden = false -let cursorEnabled = true -let cursorCharVisible = true - -Object.assign(cursorline.style, { - background: 'rgba(var(--background-alpha), 0.2)', - position: 'absolute', - 'mix-blend-mode': 'screen', - height: `${cell.height}px`, - 'z-index': 60, -}) - -Object.assign(debugline.style, { - display: 'none', - position: 'absolute', - 'mix-blend-mode': 'screen', - height: `${cell.height}px`, - 'z-index': 60, -}) - -Object.assign(cursorEl.style, { - 'z-index': 70, - position: 'absolute', - display: 'none', - 'justify-content': 'center', - 'align-items': 'center', -}) - -Object.assign(cursorChar.style, { - filter: 'invert(1) grayscale(1)', - 'font-family': 'var(--font)', - 'font-size': 'calc(var(--font-size) * 1px)', -}) - -cursorEl.appendChild(cursorChar) - -export const getCursorBoundingClientRect = () => - cursorline.getBoundingClientRect() export const setCursorShape = (shape: CursorShape, size = 20) => { cursor.shape = shape + cursor.size = size - if (shape === CursorShape.block) - Object.assign(cursorEl.style, { - background: cursor.color, - height: `${cell.height}px`, - width: `${cell.width}px`, - }) - - if (shape === CursorShape.line) - Object.assign(cursorEl.style, { - background: cursor.color, - height: `${cell.height}px`, - width: `${(cell.width * (size / 100)).toFixed(2)}px`, - }) - - if (shape === CursorShape.underline) - Object.assign(cursorEl.style, { - background: partialFill('horizontal', cursor.color, size), - height: `${cell.height}px`, - width: `${cell.width}px`, - }) + windows.webgl.updateCursorShape(shape) } export const setCursorColor = (color: string) => { - cursorChar.style.color = color - cursor.color = color - cursorEl.style.background = color + let [r, g, b] = hexToRGB(color) + r /= 255 + g /= 255 + b /= 255 + cursor.color = [r, g, b] + + windows.webgl.updateCursorColor(r, g, b) } export const enableCursor = () => (cursorEnabled = true) @@ -95,69 +41,28 @@ export const disableCursor = () => (cursorEnabled = false) export const hideCursor = () => { if (!cursorEnabled) return - cursorRequestedToBeHidden = true - cursorEl.style.display = 'none' - cursorline.style.display = 'none' + + windows.webgl.showCursor(false) + Object.assign(cursor, { visible: false }) } export const showCursor = () => { if (!cursorEnabled) return - cursorRequestedToBeHidden = false - cursorEl.style.display = 'flex' - cursorline.style.display = 'none' -} - -export const showCursorline = () => (cursorline.style.display = '') -export const updateCursorChar = () => { - cursorChar.innerText = - cursor.shape === CursorShape.block - ? windows.getActive().editor.getChar(cursor.row, cursor.col) - : '' - - if (cursor.shape === CursorShape.block && !cursorCharVisible) - cursorChar.style.display = '' + windows.webgl.showCursor(true) + Object.assign(cursor, { visible: true }) } -const updateCursorCharInternal = (gridId: number, row: number, col: number) => { - if (cursor.shape !== CursorShape.block) { - cursorChar.style.display = 'none' - cursorCharVisible = false - cursorChar.innerText = '' - return - } - - const char = windows.get(gridId).editor.getChar(row, col) - cursorChar.innerText = char - cursorChar.style.display = '' - cursorCharVisible = true -} +// TODO(smolck): export const showCursorline = () => (cursorline.style.display = '') -export const moveCursor = (gridId: number, row: number, col: number) => { +export const moveCursor = (row: number, col: number) => { Object.assign(cursor, { row, col }) - // even if cursor(line) is hidden, we still need to update the positions. - // once the cursor elements are re-activated, the position updated while - // hidden must be accurate. (e.g. using jumpTo() in grep/references/etc) - const win = windows.get(gridId) - const cursorPos = win.positionToWorkspacePixels(row, col) - const linePos = win.positionToWorkspacePixels(row, 0) - const { width } = win.getWindowSize() - - cursorEl.style.transform = translate(cursorPos.x, cursorPos.y) - - Object.assign(cursorline.style, { - transform: translate(linePos.x - paddingX, linePos.y), - width: `${width}px`, - height: `${cell.height}px`, - }) - - updateCursorCharInternal(gridId, row, col) - if (cursorRequestedToBeHidden) return showCursor() + windows.webgl.updateCursorPosition(row, col) } setCursorShape(CursorShape.block) diff --git a/src/core/input.ts b/src/core/input.ts index 5b480e48..8cebe9d5 100644 --- a/src/core/input.ts +++ b/src/core/input.ts @@ -1,40 +1,18 @@ -import { normalizeVimMode } from '../support/neovim-utils' import { input } from '../core/master-control' import { VimMode } from '../neovim/types' -import { $, is } from '../support/utils' +import { $ } from '../support/utils' import api from '../core/instance-api' import { remote } from 'electron' -import { Script } from 'vm' export enum InputType { Down = 'down', Up = 'up', } -interface RemapModifer { - from: string - to: string -} - -interface KeyShape extends KeyboardEvent { - mode?: VimMode -} - -interface KeyTransform { - mode: string - event: 'hold' | 'up' | 'down' - match: KeyboardEvent - transform: string -} - type OnKeyFn = (inputKeys: string, inputType: InputType) => void const modifiers = ['Alt', 'Shift', 'Meta', 'Control'] -const remaps = new Map() let isCapturing = true -let holding = '' -let xformed = false -let lastDown = '' let windowHasFocus = true let lastEscapeTimestamp = 0 let shouldClearEscapeOnNextAppFocus = false @@ -85,102 +63,19 @@ const wrapKey = (key: string): string => key.length > 1 && isUpper(key[0]) ? `<${key}>` : key const combineModsWithKey = (mods: string, key: string) => mods.length ? `${mods}-${key}` : key -const userModRemaps = (mods: string[]) => mods.map((m) => remaps.get(m) || m) const joinModsWithDash = (mods: string[]) => mods.join('-') -const mapMods = $(handleMods, userModRemaps, joinModsWithDash) +const mapMods = $(handleMods, joinModsWithDash) const mapKey = $(bypassEmptyMod, toVimKey) const formatInput = $(combineModsWithKey, wrapKey) const shortcuts = new Map() const globalShortcuts = new Map void>() -const resetInputState = () => { - xformed = false - lastDown = '' - holding = '' -} - export const focus = () => { isCapturing = true - resetInputState() } export const blur = () => { isCapturing = false - resetInputState() -} - -export const setupRemapModifiers = (mappings: RemapModifer[]) => { - if (!mappings) return - remaps.clear() - mappings.forEach((mapping) => remapModifier(mapping.from, mapping.to)) -} - -const vimscriptObjectToECMA = (obj: any) => - Object.entries(obj).reduce((res, [key, val]) => { - if (val === 'true') Reflect.set(res, key, true) - else if (val === 'false') Reflect.set(res, key, false) - else Reflect.set(res, key, val) - return res - }, {}) - -const setupTransforms = (transforms: KeyTransform[]) => { - if (!transforms) return - xfrmHold.clear() - xfrmDown.clear() - xfrmUp.clear() - - transforms.forEach(({ event, mode, match, transform }) => { - const nvimMode = normalizeVimMode(mode) - const fn = Reflect.get(addTransform, event) - if (!fn) return console.error('can not add key-transform for event:', event) - - const transformFn = new Script(transform).runInThisContext() - const matchObj = - nvimMode !== VimMode.SomeModeThatIProbablyDontCareAbout - ? Object.assign(vimscriptObjectToECMA(match), { mode: nvimMode }) - : vimscriptObjectToECMA(match) - - if (is.function(fn) && is.function(transformFn)) fn(matchObj, transformFn) - }) -} - -const remapModifier = (from: string, to: string) => remaps.set(from, to) - -type Transformer = (input: KeyboardEvent) => KeyboardEvent -export const xfrmHold = new Map() -export const xfrmDown = new Map() -export const xfrmUp = new Map() - -const keToStr = (e: KeyShape) => - [ - e.key, - (e.ctrlKey) | 0, - (e.metaKey) | 0, - (e.altKey) | 0, - (e.shiftKey) | 0, - ].join('') - -const defkey = { - ...new KeyboardEvent('keydown'), - key: '', - ctrlKey: false, - metaKey: false, - altKey: false, - shiftKey: false, -} - -const addTransform = { - hold: (e: any, fn: Transformer) => - xfrmHold.set(keToStr({ ...defkey, ...e }), (e) => ({ ...e, ...fn(e) })), - - down: (e: any, fn: Transformer) => - xfrmDown.set(keToStr({ ...defkey, ...e }), (e) => ({ ...e, ...fn(e) })), - - up: (e: any, fn: Transformer) => { - const before = keToStr({ ...defkey, ...e }) - const now = keToStr({ ...defkey, key: e.key }) - xfrmUp.set(before + now, (e) => ({ ...e, ...fn(e) })) - }, } export const stealInput = (onKeyFn: OnKeyFn) => { @@ -244,54 +139,9 @@ const sendKeys = async (e: KeyboardEvent, inputType: InputType) => { const keydownHandler = (e: KeyboardEvent) => { if (!windowHasFocus || !isCapturing) return - const es = keToStr(e) - lastDown = es - - if (xfrmDown.has(es)) { - const remapped = xfrmDown.get(holding)!(e) - sendKeys(remapped, InputType.Down) - return - } - - if (xfrmHold.has(es)) { - holding = es - return - } - - if (xfrmHold.has(holding)) { - const remapped = xfrmHold.get(holding)!(e) - sendKeys(remapped, InputType.Down) - xformed = true - return - } - sendKeys(e, InputType.Down) } -const keyupHandler = (e: KeyboardEvent) => { - if (!windowHasFocus || !isCapturing) return - - // one of the observed ways in which we can have a 'keyup' event without a - // 'keydown' event is when the window receives focus while a key is already - // pressed. this will happen with key combos like cmd+tab or alt+tab to - // switch applications in mac/windows. there is probably no good reason to - // send the keyup event key to neovim. in fact, this causes issues if we have - // a xform mapping of cmd -> escape, as it sends an 'esc' key to neovim - // terminal, thus swallowing the first key after app focus - if (!lastDown) return - const es = keToStr(e) - - const prevKeyAndThisOne = lastDown + es - if (xfrmUp.has(prevKeyAndThisOne)) - return sendKeys(xfrmUp.get(prevKeyAndThisOne)!(e), InputType.Up) - - if (holding === es) { - if (!xformed) sendKeys(e, InputType.Up) - xformed = false - holding = '' - } -} - // Need to handle key events from window for GUI elements like the external // cmdline, so if the key composition textarea isn't focused (which it won't // be when those elements are in use), handle the event from the window. @@ -301,19 +151,10 @@ window.addEventListener('keydown', (e) => { keydownHandler(e) }) -// Same as above, just for `keyup` event. -window.addEventListener('keyup', (e) => { - if (textarea) if (textarea === document.activeElement) return - - keyupHandler(e) -}) - textarea?.addEventListener('keydown', keydownHandler) -textarea?.addEventListener('keyup', keyupHandler) remote.getCurrentWindow().on('focus', () => { windowHasFocus = true - resetInputState() if (shouldClearEscapeOnNextAppFocus) { // so if i remap 'cmd' down+up -> 'esc' and then hit cmd+tab to switch apps // while in a terminal buffer, the application captures the 'cmd' (xform to @@ -335,13 +176,9 @@ remote.getCurrentWindow().on('focus', () => { remote.getCurrentWindow().on('blur', async () => { windowHasFocus = false - resetInputState() const lastEscapeFromNow = Date.now() - lastEscapeTimestamp const isTerminalMode = api.nvim.state.mode === VimMode.Terminal const fixTermEscape = isTerminalMode && lastEscapeFromNow < 25 if (fixTermEscape) shouldClearEscapeOnNextAppFocus = true }) - -api.onConfig.inputRemapModifiersDidChange(setupRemapModifiers) -api.onConfig.inputKeyTransformsDidChange(setupTransforms) diff --git a/src/core/instance-api.ts b/src/core/instance-api.ts index 3bf1e045..d362ed8e 100644 --- a/src/core/instance-api.ts +++ b/src/core/instance-api.ts @@ -91,11 +91,6 @@ onSwitchVim(async () => { ee.emit('git.status', gitInfo.status) ee.emit('git.branch', gitInfo.branch) ee.emit('nvim.load', true) - - const mappings = await instance.request.nvimGetVar('veonim_remap_modifiers') - ee.emit('input.remap.modifiers', mappings) - const transforms = await instance.request.nvimGetVar('veonim_key_transforms') - ee.emit('input.key.transforms', transforms) }) const getBufferInfo = (): Promise => diff --git a/src/neovim/startup.ts b/src/neovim/startup.ts index a79c5de5..48fe9532 100644 --- a/src/neovim/startup.ts +++ b/src/neovim/startup.ts @@ -83,7 +83,7 @@ startup.defineFunc.UivonimCreateHighlights` hi! link uvnFunction Function hi! link uvnBuiltin Constant hi! link uvnKeyword Keyword - hi! link uvnCursor CursorNormal + hi! link uvnCursor Cursor ` // autocmds in a separate function because chaining autocmds with "|" is bad diff --git a/src/render/redraw.ts b/src/render/redraw.ts index 55a40ba5..13dbbcf0 100644 --- a/src/render/redraw.ts +++ b/src/render/redraw.ts @@ -7,12 +7,7 @@ import { getCharIndex, getUpdatedFontAtlasMaybe, } from '../render/font-texture-atlas' -import { - moveCursor, - hideCursor, - showCursor, - updateCursorChar, -} from '../core/cursor' +import { hideCursor, showCursor, moveCursor } from '../core/cursor' import * as windows from '../windows/window-manager' import * as dispatch from '../messaging/dispatch' import { onRedraw, resizeGrid } from '../core/master-control' @@ -92,7 +87,7 @@ const grid_cursor_goto = ([, [gridId, row, col]]: any) => { state_cursorVisible = gridId !== 1 if (gridId === 1) return windows.setActiveGrid(gridId) - moveCursor(gridId, row, col) + moveCursor(row, col) } const grid_scroll = ([ @@ -307,21 +302,6 @@ const win_float_pos = (e: any) => { } } -let layoutTimeout: NodeJS.Timeout | undefined - -const refreshOrStartLayoutTimer = (winUpdates: boolean) => { - layoutTimeout = setTimeout(() => { - renderEvents.messageClearPromptsMaybeHack(state_cursorVisible) - state_cursorVisible ? showCursor() : hideCursor() - if (state_cursorVisible) updateCursorChar() - dispatch.pub('redraw') - if (!winUpdates) return - - windows.disposeInvalidWindows() - windows.layout() - }, 10) -} - onRedraw((redrawEvents) => { // because of circular logic/infinite loop. cmdline_show updates UI, UI makes // a change in the cmdline, nvim sends redraw again. we cut that stuff out @@ -386,6 +366,11 @@ onRedraw((redrawEvents) => { else if (e === 'msg_ruler') renderEvents.msg_ruler(ev) } - if (layoutTimeout) clearTimeout(layoutTimeout) - refreshOrStartLayoutTimer(winUpdates) + renderEvents.messageClearPromptsMaybeHack(state_cursorVisible) + state_cursorVisible ? showCursor() : hideCursor() + dispatch.pub('redraw') + if (!winUpdates) return + + windows.disposeInvalidWindows() + windows.layout() }) diff --git a/src/render/webgl-text-bg.ts b/src/render/webgl-text-bg.ts index 6e8b0acc..693b4ed7 100644 --- a/src/render/webgl-text-bg.ts +++ b/src/render/webgl-text-bg.ts @@ -2,12 +2,16 @@ import { getColorAtlas, colors } from '../render/highlight-attributes' import { WebGL, VarKind } from '../render/webgl-utils' import { cell } from '../core/workspace' import { hexToRGB } from '../ui/css' +import { CursorShape } from '../core/cursor' export default (webgl: WebGL) => { const viewport = { x: 0, y: 0, width: 0, height: 0 } + let shouldShowCursor = true + let cursorShape = 0 /* CursorShape.block */ const program = webgl.setupProgram({ quadVertex: VarKind.Attribute, + isCursorTri: VarKind.Attribute, cellPosition: VarKind.Attribute, hlid: VarKind.Attribute, hlidType: VarKind.Uniform, @@ -15,23 +19,35 @@ export default (webgl: WebGL) => { colorAtlasResolution: VarKind.Uniform, colorAtlasTextureId: VarKind.Uniform, cellSize: VarKind.Uniform, + cursorPosition: VarKind.Uniform, + cursorColor: VarKind.Uniform, + cursorShape: VarKind.Uniform, + shouldShowCursor: VarKind.Uniform, }) program.setVertexShader( (v) => `#version 300 es in vec2 ${v.quadVertex}; in vec2 ${v.cellPosition}; + in float ${v.isCursorTri}; in float ${v.hlid}; + uniform vec2 ${v.cursorPosition}; uniform vec2 ${v.canvasResolution}; uniform vec2 ${v.colorAtlasResolution}; uniform vec2 ${v.cellSize}; + uniform vec4 ${v.cursorColor}; + uniform bool ${v.shouldShowCursor}; + uniform int ${v.cursorShape}; uniform float ${v.hlidType}; uniform sampler2D ${v.colorAtlasTextureId}; - out vec4 o_color; out vec2 o_colorPosition; void main() { + bool isCursorCell = ${v.cursorPosition} == ${v.cellPosition} && ${ + v.shouldShowCursor + }; + vec2 absolutePixelPosition = ${v.cellPosition} * ${v.cellSize}; vec2 vertexPosition = absolutePixelPosition + ${v.quadVertex}; vec2 posFloat = vertexPosition / ${v.canvasResolution}; @@ -44,7 +60,26 @@ export default (webgl: WebGL) => { float color_y = ${v.hlidType} * texelSize + 1.0; vec2 colorPosition = vec2(color_x, color_y) / ${v.colorAtlasResolution}; - o_color = texture(${v.colorAtlasTextureId}, colorPosition); + bool condition; + ${ + /* + TODO(smolck): I'm almost certain there's a way to do this + condition all in one without extra if statements, but my brain is + not finding it right now. + */ '' + } + if (${v.cursorShape} == 1) { + condition = isCursorCell && isCursorTri == 1.0; + } else { + condition = isCursorCell; + } + + if (condition) { + o_color = cursorColor; + } else { + vec4 textureColor = texture(${v.colorAtlasTextureId}, colorPosition); + o_color = textureColor; + } } ` ) @@ -75,6 +110,10 @@ export default (webgl: WebGL) => { colorAtlas.width, colorAtlas.height ) + webgl.gl.uniform2f(program.vars.cursorPosition, 0, 0) + webgl.gl.uniform4fv(program.vars.cursorColor, [1, 1, 1, 1]) + // @ts-ignore + webgl.gl.uniform1i(program.vars.shouldShowCursor, shouldShowCursor) // total size of all pointers. chunk size that goes to shader const wrenderStride = 4 * Float32Array.BYTES_PER_ELEMENT @@ -98,41 +137,104 @@ export default (webgl: WebGL) => { }, ]) - const quadBuffer = program.setupData({ - pointer: program.vars.quadVertex, - type: webgl.gl.FLOAT, - size: 2, - }) + const quadBuffer = program.setupData([ + { + pointer: program.vars.quadVertex, + type: webgl.gl.FLOAT, + size: 2, + offset: 0, + }, + { + pointer: program.vars.isCursorTri, + type: webgl.gl.FLOAT, + size: 1, + offset: Float32Array.BYTES_PER_ELEMENT * 2 * 12, + }, + ]) + + const updateCellSize = (initial = false, cursorSize = 20) => { + const w = cell.width + const h = cell.height + const smallerW = w * (cursorSize / 100.0) + const percentH = h * (cursorSize / 100.0) - const updateCellSize = (initial = false) => { const next = { boxes: new Float32Array([ 0, 0, - cell.width, - cell.height, + smallerW, + h, 0, - cell.height, - cell.width, + h, + + smallerW, + 0, + smallerW, + h, 0, - cell.width, - cell.height, 0, + + smallerW, + 0, + w, + h, + smallerW, + h, + + w, 0, + w, + h, + smallerW, + 0, + + // TODO(smolck): Better way of doing this? Also, note that the 1's + // specify which triangles of the above to color in for the cursor, and the zeroes + // which triangles not to color in, *if* the cursor is a line shape. If + // it isn't a line shape (atm a block shape), these are ignored. + ...Array(6).fill(1), + ...Array(6).fill(0), ]), + // TODO(smolck): Don't draw double the tris for underliens too, maybe use + // a separate buffer for quadVertex somehow or something? lines: new Float32Array([ + /* Previous values (for future ref): + * 0, cell.height - 1, + * cell.width, cell.height, + * 0, cell.height, + * + * cell.width, cell.height - 1, + * cell.width, cell.height, + * 0, cell.height - 1, */ 0, - cell.height - 1, - cell.width, - cell.height, + h - 1, + smallerW, + percentH, 0, - cell.height, - cell.width, - cell.height - 1, - cell.width, - cell.height, + percentH, + + smallerW, + h - 1, + smallerW, + percentH, 0, - cell.height - 1, + h - 1, + + smallerW, + h - 1, + w, + percentH, + smallerW, + percentH, + + w, + h - 1, + w, + percentH, + smallerW, + h - 1, + + ...Array(12).fill(0), ]), } @@ -183,15 +285,47 @@ export default (webgl: WebGL) => { readjustViewportMaybe(x, y, width, height) wrenderBuffer.setData(buffer) + if (shouldShowCursor && cursorShape == 2) + webgl.gl.uniform1i(program.vars.shouldShowCursor, 0 /* false */) // background quadBuffer.setData(quads.boxes) webgl.gl.uniform1f(program.vars.hlidType, 0) - webgl.gl.drawArraysInstanced(webgl.gl.TRIANGLES, 0, 6, buffer.length / 4) + webgl.gl.drawArraysInstanced(webgl.gl.TRIANGLES, 0, 12, buffer.length / 4) + + webgl.gl.uniform1i(program.vars.shouldShowCursor, shouldShowCursor ? 1 : 0) // underlines quadBuffer.setData(quads.lines) + + // Just want to ignore the cursor logic in the vertex shader for underlines, + // so set shouldShowCursor to false, then back to it's previous value after + // the draw call. + if (shouldShowCursor && cursorShape != 2 /* CursorShape.underline */) + webgl.gl.uniform1i(program.vars.shouldShowCursor, 0 /* false */) + webgl.gl.uniform1f(program.vars.hlidType, 2) - webgl.gl.drawArraysInstanced(webgl.gl.TRIANGLES, 0, 6, buffer.length / 4) + webgl.gl.drawArraysInstanced(webgl.gl.TRIANGLES, 0, 12, buffer.length / 4) + + webgl.gl.uniform1i(program.vars.shouldShowCursor, shouldShowCursor ? 1 : 0) + } + + const showCursor = (enable: boolean) => ( + (shouldShowCursor = enable), + // @ts-ignore + webgl.gl.uniform1i(program.vars.shouldShowCursor, enable) + ) + + const updateCursorColor = (color: [number, number, number]) => { + webgl.gl.uniform4fv(program.vars.cursorColor, [...color, 1]) + } + + const updateCursorShape = (shape: CursorShape) => { + cursorShape = shape + webgl.gl.uniform1i(program.vars.cursorShape, shape) + } + + const updateCursorPosition = (row: number, col: number) => { + webgl.gl.uniform2f(program.vars.cursorPosition, col, row) } const updateColorAtlas = (colorAtlas: HTMLCanvasElement) => { @@ -222,5 +356,16 @@ export default (webgl: WebGL) => { webgl.gl.clear(webgl.gl.COLOR_BUFFER_BIT) } - return { clear, clearAll, render, resize, updateColorAtlas, updateCellSize } + return { + clear, + clearAll, + render, + resize, + updateColorAtlas, + updateCellSize, + showCursor, + updateCursorPosition, + updateCursorShape, + updateCursorColor, + } } diff --git a/src/render/webgl-text-fg.ts b/src/render/webgl-text-fg.ts index fe79b017..643c4f9b 100644 --- a/src/render/webgl-text-fg.ts +++ b/src/render/webgl-text-fg.ts @@ -2,6 +2,7 @@ import { getColorAtlas } from '../render/highlight-attributes' import generateFontAtlas from '../render/font-texture-atlas' import { WebGL, VarKind } from '../render/webgl-utils' import { cell } from '../core/workspace' +import { CursorShape } from '../core/cursor' export default (webgl: WebGL) => { const viewport = { x: 0, y: 0, width: 0, height: 0 } @@ -18,6 +19,11 @@ export default (webgl: WebGL) => { colorAtlasTextureId: VarKind.Uniform, cellSize: VarKind.Uniform, cellPadding: VarKind.Uniform, + + shouldShowCursor: VarKind.Uniform, + cursorPosition: VarKind.Uniform, + cursorShape: VarKind.Uniform, + cursorColor: VarKind.Uniform, }) program.setVertexShader( @@ -33,10 +39,17 @@ export default (webgl: WebGL) => { uniform vec2 ${v.cellPadding}; uniform sampler2D ${v.colorAtlasTextureId}; + uniform vec4 ${v.cursorColor}; + uniform vec2 ${v.cursorPosition}; + uniform bool ${v.shouldShowCursor}; + uniform int ${v.cursorShape}; + out vec2 o_glyphPosition; out vec4 o_color; void main() { + bool isCursorCell = ${v.cursorPosition} == ${v.cellPosition} && ${v.shouldShowCursor}; + vec2 absolutePixelPosition = ${v.cellPosition} * ${v.cellSize}; vec2 vertexPosition = absolutePixelPosition + ${v.quadVertex} + ${v.cellPadding}; vec2 posFloat = vertexPosition / ${v.canvasResolution}; @@ -53,7 +66,13 @@ export default (webgl: WebGL) => { float color_y = 1.0 * texelSize + 1.0; vec2 colorPosition = vec2(color_x, color_y) / ${v.colorAtlasResolution}; - o_color = texture(${v.colorAtlasTextureId}, colorPosition); + vec4 textureColor = texture(${v.colorAtlasTextureId}, colorPosition); + + if (isCursorCell && cursorShape == 0) { + o_color = ${v.cursorColor}; + } else { + o_color = textureColor; + } } ` ) @@ -104,6 +123,12 @@ export default (webgl: WebGL) => { colorAtlas.height ) + webgl.gl.uniform4fv(program.vars.cursorColor, [0, 0, 0, 1]) + webgl.gl.uniform2f(program.vars.cursorPosition, 0, 0) + webgl.gl.uniform1i(program.vars.cursorShape, 0) // CursorShape.block = 0 + // @ts-ignore + webgl.gl.uniform1i(program.vars.shouldShowCursor, true) + // total size of all pointers. chunk size that goes to shader const wrenderStride = 4 * Float32Array.BYTES_PER_ELEMENT @@ -231,6 +256,22 @@ export default (webgl: WebGL) => { webgl.gl.uniform2f(program.vars.cellPadding, 0, cell.padding) } + const showCursor = (enable: boolean) => + // @ts-ignore + webgl.gl.uniform1i(program.vars.shouldShowCursor, enable) + + const updateCursorColor = (color: [number, number, number]) => { + webgl.gl.uniform4fv(program.vars.cursorColor, [...color, 1]) + } + + const updateCursorShape = (shape: CursorShape) => { + webgl.gl.uniform1i(program.vars.cursorShape, shape) + } + + const updateCursorPosition = (row: number, col: number) => { + webgl.gl.uniform2f(program.vars.cursorPosition, col, row) + } + const updateColorAtlas = (colorAtlas: HTMLCanvasElement) => { webgl.loadCanvasTexture(colorAtlas, webgl.gl.TEXTURE1) webgl.gl.uniform2f( @@ -263,5 +304,9 @@ export default (webgl: WebGL) => { updateFontAtlas, updateColorAtlas, updateCellSize, + updateCursorPosition, + updateCursorShape, + updateCursorColor, + showCursor, } } diff --git a/src/render/webgl.ts b/src/render/webgl.ts index 41eb3a71..d689c655 100644 --- a/src/render/webgl.ts +++ b/src/render/webgl.ts @@ -3,6 +3,8 @@ import CreateWebGL from '../render/webgl-utils' import { cell } from '../core/workspace' import TextFG from '../render/webgl-text-fg' import TextBG from '../render/webgl-text-bg' +import { cursor as cursorState, CursorShape } from '../core/cursor' +import { getActiveGridId } from '../windows/window-manager' export interface WebGLView { resize: (rows: number, cols: number) => void @@ -17,6 +19,7 @@ export interface WebGLView { getGridLine: (row: number) => Float32Array getGridBuffer: () => Float32Array getBuffer: () => Float32Array + updateGridId: (gridId: number) => void } const nutella = () => { @@ -31,6 +34,29 @@ const nutella = () => { textFGRenderer.resize(width, height) } + const showCursor = (enable: boolean) => { + textBGRenderer.showCursor(enable) + textFGRenderer.showCursor(enable) + } + + const updateCursorShape = (shape: CursorShape) => { + textBGRenderer.updateCursorShape(shape) + // TODO(smolck): If cursor size changes need to update cells . . . + textBGRenderer.updateCellSize(false, cursorState.size) + + textFGRenderer.updateCursorShape(shape) + } + + const updateCursorColor = (r: number, g: number, b: number) => { + textBGRenderer.updateCursorColor([r, g, b]) + textFGRenderer.updateCursorColor([1.0 - r, 1.0 - g, 1.0 - b]) + } + + const updateCursorPosition = (row: number, col: number) => { + textBGRenderer.updateCursorPosition(row, col) + textFGRenderer.updateCursorPosition(row, col) + } + const updateFontAtlas = (fontAtlas: HTMLCanvasElement) => { textFGRenderer.updateFontAtlas(fontAtlas) } @@ -50,12 +76,15 @@ const nutella = () => { textFGRenderer.clearAll() } - const createView = (): WebGLView => { + const createView = (initialGridId: number): WebGLView => { + let gridId = initialGridId const viewport = { x: 0, y: 0, width: 0, height: 0 } const gridSize = { rows: 0, cols: 0 } const gridBuffer = CreateWebGLBuffer() let dataBuffer = new Float32Array() + const updateGridId = (newGridId: number) => (gridId = newGridId) + const resize = (rows: number, cols: number) => { const width = cols * cell.width const height = rows * cell.height @@ -85,15 +114,23 @@ const nutella = () => { const render = (elements: number) => { const buffer = dataBuffer.subarray(0, elements) const { x, y, width, height } = viewport + + const doHacks = gridId !== getActiveGridId() && cursorState.visible + if (doHacks) showCursor(false) textBGRenderer.render(buffer, x, y, width, height) textFGRenderer.render(buffer, x, y, width, height) + if (doHacks) showCursor(true) } const renderGridBuffer = () => { const { x, y, width, height } = viewport const buffer = gridBuffer.getBuffer() + + const doHacks = gridId !== getActiveGridId() && cursorState.visible + if (doHacks) showCursor(false) textBGRenderer.render(buffer, x, y, width, height) textFGRenderer.render(buffer, x, y, width, height) + if (doHacks) showCursor(true) } const clear = () => { @@ -129,6 +166,7 @@ const nutella = () => { moveRegionDown, clearGridBuffer, renderGridBuffer, + updateGridId, getGridCell: gridBuffer.getCell, getGridLine: gridBuffer.getLine, getGridBuffer: gridBuffer.getBuffer, @@ -143,6 +181,10 @@ const nutella = () => { updateCellSize, updateFontAtlas, updateColorAtlas, + updateCursorShape, + updateCursorPosition, + updateCursorColor, + showCursor, foregroundElement: foregroundGL.canvasElement, backgroundElement: backgroundGL.canvasElement, } diff --git a/src/windows/window-manager.ts b/src/windows/window-manager.ts index be87fe99..036ff991 100644 --- a/src/windows/window-manager.ts +++ b/src/windows/window-manager.ts @@ -74,7 +74,9 @@ export const calculateGlobalOffset = (anchorWin: Window, float: Window) => { } } -export const createWebGLView = () => webgl.createView() +export const createWebGLView = (gridId: number) => webgl.createView(gridId) + +export const getActiveGridId = () => state.activeInstanceGrid export const setActiveGrid = (id: number) => Object.assign(state, { @@ -114,6 +116,7 @@ export const set = ( visible: true, id: wid, gridId: gid, + gridIdNumber: gridId, }) if (!windows.has(gid)) windows.set(gid, win) @@ -203,7 +206,7 @@ export const layout = () => { state.activeGrid && requestAnimationFrame(() => { if (!windows.has(state.activeGrid)) return - moveCursor(state.activeInstanceGrid, cursor.row, cursor.col) + moveCursor(cursor.row, cursor.col) }) } diff --git a/src/windows/window.ts b/src/windows/window.ts index 0649a1d2..d738d70b 100644 --- a/src/windows/window.ts +++ b/src/windows/window.ts @@ -14,6 +14,7 @@ import { makel } from '../ui/vanilla' export interface WindowInfo { id: string gridId: string + gridIdNumber: number row: number col: number width: number @@ -116,6 +117,7 @@ export default () => { const wininfo: WindowInfo = { id: '0', gridId: '0', + gridIdNumber: 0, row: 0, col: 0, width: 0, @@ -125,7 +127,7 @@ export default () => { anchor: '', } const layout = { x: 0, y: 0, width: 0, height: 0 } - const webgl = createWebGLView() + const webgl = createWebGLView(0) const container = makel({ flexFlow: 'column', @@ -229,6 +231,7 @@ export default () => { container.id = `${info.id}` container.setAttribute('gridid', info.gridId) + webgl.updateGridId(info.gridIdNumber) Object.assign(wininfo, info) }