import { clearScreen, fillRect } from "./window.ts"; import { fontWidth, fontHeight } from "./font.ts"; import { drawText } from "./builtins.ts"; import { COLOR } from "./colors.ts"; import {getSheet, setSheet} from "./sheet.ts"; import { K, getKeyboardString, keyPressed, shiftKeyDown } from "./keyboard.ts"; // deno-lint-ignore prefer-const let tab: "code" | "sprite" | "map" | "sfx" | "music" = "code"; // TODO: Make scrolling work const codeTabState = { scrollX: 0, scrollY: 0, anchor: 0, focus: 0, get focusX() {return indexToGrid(this.code, this.focus).x;}, get focusY() {return indexToGrid(this.code, this.focus).y;}, get anchorX() {return indexToGrid(this.code, this.anchor).x;}, get anchorY() {return indexToGrid(this.code, this.anchor).y;}, isCollapsed() { return this.anchor === this.focus; }, clampInRange(n: number) { return Math.max(0, Math.min(n, this.code.length)) }, setSelection(anchor: number | {x: number, y: number}, focus?: number | {x: number, y: number}) { if (typeof anchor !== "number") { anchor = gridToIndex(this.code, anchor.x, anchor.y); } focus = focus ?? anchor; if (typeof focus !== "number") { focus = gridToIndex(this.code, focus.x, focus.y); } this.anchor = this.clampInRange(anchor), this.focus = this.clampInRange(focus); }, setFocus(focus: number | {x: number, y: number}) { if (typeof focus !== "number") { focus = gridToIndex(this.code, focus.x, focus.y); } this.focus = this.clampInRange(focus); }, insertText(text: string) { const {code, anchor, focus} = this; this.code = code.slice(0, Math.min(anchor, focus)) + text + code.slice(Math.max(anchor, focus)); this.setSelection(Math.min(anchor, focus) + text.length); }, indent(indentString: string) { const lines = this.code.split("\n"); const {focusX, focusY, anchorX, anchorY} = this; const newLines = lines.map((line, i) => { if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY)) { return indentString+line; } else { return line; } }); this.code = newLines.join("\n"); this.setSelection({x: anchorX+1, y: anchorY}, {x: focusX+1, y: focusY}); }, outdent(outdentRegex: RegExp) { const lines = this.code.split("\n"); const {focusX, focusY, anchorX, anchorY} = this; const newLines = lines.map((line, i) => { const match = line.match(outdentRegex); if (i >= Math.min(focusY, anchorY) && i <= Math.max(focusY, anchorY) && match) { return line.slice(match[0].length); } else { return line; } }); this.code = newLines.join("\n"); this.setSelection({x: Math.max(0,anchorX-1), y: anchorY}, {x: Math.max(0,focusX-1), y: focusY}); }, backspace() { const {code, focus} = this; if (this.isCollapsed()) { if (focus > 0) { this.code = code.slice(0, focus-1) + code.slice(focus); this.setSelection(focus-1); } } else { this.insertText(""); } }, delete() { const {code, focus} = this; if (this.isCollapsed()) { if (focus < code.length) { this.code = code.slice(0, focus) + code.slice(1+focus); } } else { this.insertText(""); } }, get code() { return getSheet(0); }, set code(val) { setSheet(0, "code", val); } } const indexToGrid = (str: string, index: number) => { const linesUpTo = str.slice(0,index).split("\n"); return { x: linesUpTo[linesUpTo.length-1].length, y: linesUpTo.length - 1, } } const gridToIndex = (str: string, x: number, y: number) => { const lines = str.split("\n"); if (y < 0) { return 0; } if (y >= lines.length) { return str.length; } return lines.slice(0, y).join("\n").length+Math.min(x, lines[y].length)+1; } const drawCodeField = (code: string, x: number, y: number, w: number, h: number) => { const { scrollX, scrollY, anchor, focus, } = codeTabState; const { x: focusX, y: focusY, } = indexToGrid(code, focus); const { x: anchorX, y: anchorY, } = indexToGrid(code, anchor); fillRect(x, y, w, h, COLOR.DARKBLUE); if (anchor === focus) { fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.RED); } else { // TODO: Draw this selection better fillRect(x+anchorX*fontWidth-scrollX, y+anchorY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.GREEN); fillRect(x+focusX*fontWidth-scrollX, y+focusY*(fontHeight+1)-scrollY, fontWidth+1, fontHeight+1, COLOR.YELLOW); } code.split("\n").forEach((line, i) => { drawText(x-scrollX, 1+y+i*(fontHeight+1)-scrollY, line); }); } const update = () => { if (tab === "code") { const { focus, focusX, focusY} = codeTabState; const keyboardString = getKeyboardString(); if (keyboardString) { codeTabState.insertText(keyboardString); } // TODO: Handle ctrl-C, ctrl-V, ctrl-X, ctrl-Z // TODO: Make ctrl-/ do commenting out (take inspiration from tab) if (keyPressed(K.ENTER)) { // TODO: Make this play nicely with indentation codeTabState.insertText("\n"); } if (keyPressed(K.TAB)) { if (!shiftKeyDown()) { if (codeTabState.isCollapsed()) { codeTabState.insertText("\t"); } else { codeTabState.indent("\t"); } } else { codeTabState.outdent(/^(\t| )/); } } if (keyPressed(K.BACKSPACE)) { codeTabState.backspace(); } if (keyPressed(K.DELETE)) { codeTabState.delete(); } if (keyPressed(K.ARROW_RIGHT)) { if (shiftKeyDown()) { codeTabState.setFocus(focus+1); } else { codeTabState.setSelection(focus+1); } } if (keyPressed(K.ARROW_LEFT)) { if (shiftKeyDown()) { codeTabState.setFocus(focus-1); } else { codeTabState.setSelection(focus-1); } } if (keyPressed(K.ARROW_DOWN)) { if (shiftKeyDown()) { codeTabState.setFocus({x: focusX, y: focusY+1}); } else { codeTabState.setSelection({x: focusX, y: focusY+1}); } } if (keyPressed(K.ARROW_UP)) { if (shiftKeyDown()) { codeTabState.setFocus({x: focusX, y: focusY-1}); } else { codeTabState.setSelection({x: focusX, y: focusY-1}); } } } } const draw = () => { clearScreen(); if (tab === "code") { drawCodeField(getSheet(0), 0, 8, 128, 112); } } export const editmode = { update, draw, }