From e20fe119f22990cc6b39d719ee3dec19e49738b9 Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Wed, 8 Mar 2023 10:42:27 -0600 Subject: [PATCH] Newer (final?) web rewrite --- app/Bundle.java | 214 ---- app/_boot.js | 308 ------ app/app/App.js | 448 -------- app/app/CPU.js | 106 -- app/app/Debugger.js | 187 ---- app/app/Disassembler.js | 958 ----------------- app/app/Memory.js | 517 --------- app/app/RegisterList.js | 889 ---------------- app/app/Util.js | 44 - app/core/Core.js | 196 ---- app/core/CoreWorker.js | 377 ------- app/core/Sim.js | 151 --- app/locale/en-US.json | 74 -- app/main.js | 34 - app/template.html | 12 - app/theme/dark.css | 26 - app/theme/kiosk.css | 761 -------------- app/theme/light.css | 26 - app/theme/virtual.css | 70 -- app/toolkit/Button.js | 387 ------- app/toolkit/Component.js | 312 ------ app/toolkit/DropDown.js | 125 --- app/toolkit/MenuBar.js | 748 ------------- app/toolkit/ScrollBar.js | 1149 -------------------- app/toolkit/TextBox.js | 145 --- app/toolkit/Toolkit.js | 337 ------ app/toolkit/Window.js | 699 ------------- core/bus.c | Bin 3895 -> 5078 bytes core/cpu.c | 49 +- core/vb.c | 23 +- core/vb.h | Bin 6337 -> 7344 bytes makefile | Bin 1608 -> 1801 bytes wasm/wasm.c | 126 --- web/App.js | 680 ++++++++++++ web/Bundle.java | 203 ++++ web/_boot.js | 310 ++++++ web/core/AudioThread.js | 88 ++ web/core/Core.js | 293 ++++++ web/core/CoreThread.js | 330 ++++++ web/core/Disassembler.js | 542 ++++++++++ web/core/wasm.c | 78 ++ web/debugger/CPU.js | 1442 ++++++++++++++++++++++++++ web/debugger/Debugger.js | 98 ++ web/debugger/ISX.js | 177 ++++ web/debugger/Memory.js | 558 ++++++++++ web/locale/en-US.json | 77 ++ web/template.html | 1 + {app => web}/theme/check.svg | 0 {app => web}/theme/close.svg | 0 {app => web}/theme/collapse.svg | 0 web/theme/dark.css | 28 + {app => web}/theme/expand.svg | 0 {app => web}/theme/inconsolata.woff2 | Bin web/theme/kiosk.css | 548 ++++++++++ web/theme/light.css | 28 + {app => web}/theme/radio.svg | 0 {app => web}/theme/roboto.woff2 | Bin {app => web}/theme/scroll.svg | 0 web/theme/vbemu.css | 223 ++++ web/theme/virtual.css | 63 ++ web/toolkit/App.js | 249 +++++ web/toolkit/Button.js | 119 +++ web/toolkit/Checkbox.js | 144 +++ web/toolkit/Component.js | 473 +++++++++ web/toolkit/Desktop.js | 86 ++ web/toolkit/DropDown.js | 154 +++ web/toolkit/Label.js | 46 + web/toolkit/Menu.js | 92 ++ web/toolkit/MenuBar.js | 106 ++ web/toolkit/MenuItem.js | 455 ++++++++ web/toolkit/Radio.js | 113 ++ web/toolkit/RadioGroup.js | 109 ++ web/toolkit/ScrollBar.js | 380 +++++++ web/toolkit/ScrollPane.js | 317 ++++++ web/toolkit/SplitPane.js | 363 +++++++ web/toolkit/TextBox.js | 66 ++ web/toolkit/Toolkit.js | 51 + web/toolkit/Window.js | 479 +++++++++ 78 files changed, 9571 insertions(+), 9496 deletions(-) delete mode 100644 app/Bundle.java delete mode 100644 app/_boot.js delete mode 100644 app/app/App.js delete mode 100644 app/app/CPU.js delete mode 100644 app/app/Debugger.js delete mode 100644 app/app/Disassembler.js delete mode 100644 app/app/Memory.js delete mode 100644 app/app/RegisterList.js delete mode 100644 app/app/Util.js delete mode 100644 app/core/Core.js delete mode 100644 app/core/CoreWorker.js delete mode 100644 app/core/Sim.js delete mode 100644 app/locale/en-US.json delete mode 100644 app/main.js delete mode 100644 app/template.html delete mode 100644 app/theme/dark.css delete mode 100644 app/theme/kiosk.css delete mode 100644 app/theme/light.css delete mode 100644 app/theme/virtual.css delete mode 100644 app/toolkit/Button.js delete mode 100644 app/toolkit/Component.js delete mode 100644 app/toolkit/DropDown.js delete mode 100644 app/toolkit/MenuBar.js delete mode 100644 app/toolkit/ScrollBar.js delete mode 100644 app/toolkit/TextBox.js delete mode 100644 app/toolkit/Toolkit.js delete mode 100644 app/toolkit/Window.js delete mode 100644 wasm/wasm.c create mode 100644 web/App.js create mode 100644 web/Bundle.java create mode 100644 web/_boot.js create mode 100644 web/core/AudioThread.js create mode 100644 web/core/Core.js create mode 100644 web/core/CoreThread.js create mode 100644 web/core/Disassembler.js create mode 100644 web/core/wasm.c create mode 100644 web/debugger/CPU.js create mode 100644 web/debugger/Debugger.js create mode 100644 web/debugger/ISX.js create mode 100644 web/debugger/Memory.js create mode 100644 web/locale/en-US.json create mode 100644 web/template.html rename {app => web}/theme/check.svg (100%) rename {app => web}/theme/close.svg (100%) rename {app => web}/theme/collapse.svg (100%) create mode 100644 web/theme/dark.css rename {app => web}/theme/expand.svg (100%) rename {app => web}/theme/inconsolata.woff2 (100%) create mode 100644 web/theme/kiosk.css create mode 100644 web/theme/light.css rename {app => web}/theme/radio.svg (100%) rename {app => web}/theme/roboto.woff2 (100%) rename {app => web}/theme/scroll.svg (100%) create mode 100644 web/theme/vbemu.css create mode 100644 web/theme/virtual.css create mode 100644 web/toolkit/App.js create mode 100644 web/toolkit/Button.js create mode 100644 web/toolkit/Checkbox.js create mode 100644 web/toolkit/Component.js create mode 100644 web/toolkit/Desktop.js create mode 100644 web/toolkit/DropDown.js create mode 100644 web/toolkit/Label.js create mode 100644 web/toolkit/Menu.js create mode 100644 web/toolkit/MenuBar.js create mode 100644 web/toolkit/MenuItem.js create mode 100644 web/toolkit/Radio.js create mode 100644 web/toolkit/RadioGroup.js create mode 100644 web/toolkit/ScrollBar.js create mode 100644 web/toolkit/ScrollPane.js create mode 100644 web/toolkit/SplitPane.js create mode 100644 web/toolkit/TextBox.js create mode 100644 web/toolkit/Toolkit.js create mode 100644 web/toolkit/Window.js diff --git a/app/Bundle.java b/app/Bundle.java deleted file mode 100644 index efa3aa9..0000000 --- a/app/Bundle.java +++ /dev/null @@ -1,214 +0,0 @@ -import java.awt.image.*; -import java.io.*; -import java.nio.charset.*; -import java.util.*; -import javax.imageio.*; - -public class Bundle { - - // File loaded from disk - static class File2 implements Comparable { - - // Instance fields - byte[] data; // Contents - String filename; // Full path relative to root - - // Comparator - public int compareTo(File2 o) { - if (filename.equals("app/_boot.js")) - return -1; - if (o.filename.equals("app/_boot.js")) - return 1; - return filename.compareTo(o.filename); - } - - } - - // Load all files in directory tree into memory - static HashMap readFiles(String bundleTitle) { - var dirs = new ArrayDeque(); - var root = new File("."); - var subs = new ArrayDeque(); - var files = new HashMap(); - - // Process all subdirectories - dirs.add(root); - while (!dirs.isEmpty()) { - var dir = dirs.remove(); - - // Add all subdirectories - for (var sub : dir.listFiles(f->f.isDirectory())) { - - // Exclusions - if (dir == root && sub.getName().equals(".git")) - continue; - - // Add the directory for bundling - dirs.add(sub); - } - - // Add all files - for (var file : dir.listFiles(f->f.isFile())) { - var file2 = new File2(); - - // Read the file into memory - try { - var stream = new FileInputStream(file); - file2.data = stream.readAllBytes(); - stream.close(); - } catch (Exception e) { - throw new RuntimeException(e.getMessage()); - } - - // Determine the file's full pathname - subs.clear(); - subs.addFirst(file.getName()); - for (;;) { - file = file.getParentFile(); - if (file.equals(root)) - break; - subs.addFirst(file.getName()); - } - file2.filename = String.join("/", subs); - - // Exclusions - if ( - file2.filename.startsWith(".git" ) || - file2.filename.startsWith(bundleTitle + "_") && - file2.filename.endsWith (".html" ) - ) continue; - - // Add the file to the output - files.put(file2.filename, file2); - } - - } - - return files; - } - - // Prepend manifest object to _boot.js - static void manifest(HashMap files, String bundleName) { - - // Produce a sorted list of files - var values = files.values().toArray(new File2[files.size()]); - Arrays.sort(values); - - // Build a file manifest - var manifest = new StringBuilder(); - manifest.append("\"use strict\";\nlet manifest=["); - for (var file : values) { - manifest.append( - "[\"" + file.filename + "\"," + file.data.length + "]"); - if (file != values[values.length - 1]) - manifest.append(","); - } - manifest.append("],bundleName=\"" + bundleName + "\";"); - - // Prepend the manifest to _boot.js - var boot = files.get("app/_boot.js"); - boot.data = ( - manifest.toString() + - new String(boot.data, StandardCharsets.UTF_8) + - "\u0000" - ).getBytes(StandardCharsets.UTF_8); - } - - // Construct bundled blob - static byte[] blob(HashMap files) { - - // Produce a sorted list of files - var values = files.values().toArray(new File2[files.size()]); - Arrays.sort(values); - - // Build the blob - var blob = new ByteArrayOutputStream(); - for (var file : values) try { - blob.write(file.data); - } catch (Exception e) { } - - return blob.toByteArray(); - } - - // Encode bundled blob as a .png - static byte[] png(byte[] blob) { - - // Calculate the dimensions of the image - int width = (int) Math.ceil(Math.sqrt(blob.length)); - int height = (int) Math.ceil((double) blob.length / width); - - // Prepare the pixel data - var pixels = new int[width * height]; - for (int x = 0; x < blob.length; x++) { - int l = blob[x] & 0xFF; - pixels[x] = 0xFF000000 | l << 16 | l << 8 | l; - } - - // Produce a BufferedImage containing the pixels - var img = new BufferedImage(width, height, - BufferedImage.TYPE_BYTE_GRAY); - img.getRaster().setPixels(0, 0, width, height, pixels); - - // Encode the image as a PNG byte array - var png = new ByteArrayOutputStream(); - try { ImageIO.write(img, "png", png); } - catch (Exception e) { } - return png.toByteArray(); - } - - // Embed bundle .png into template.html as a data URL - static void template(byte[] png, String bundleName) { - - // Encode the PNG as a data URL - String url = "data:image/png;base64," + - Base64.getMimeEncoder().encodeToString(png) - .replaceAll("\\r\\n", ""); - - try { - - // Read template.html into memory - var inStream = new FileInputStream("app/template.html"); - String template = - new String(inStream.readAllBytes(), StandardCharsets.UTF_8) - .replace("src=\"\"", "src=\"" + url + "\"") - ; - inStream.close(); - - // Write the output HTML file - var outStream = new FileOutputStream(bundleName + ".html"); - outStream.write(template.getBytes(StandardCharsets.UTF_8)); - outStream.close(); - } catch (Exception e) { throw new RuntimeException(e.getMessage()); } - - } - - // Determine the filename of the bundle - static String bundleName(String name) { - var calendar = Calendar.getInstance(); - return String.format("%s_%04d%02d%02d", - name, - calendar.get(Calendar.YEAR), - calendar.get(Calendar.MONTH) + 1, - calendar.get(Calendar.DAY_OF_MONTH) - ); - } - - // Program entry point - public static void main(String[] args) { - - // Error checking - if (args.length != 1) { - System.err.println("Usage: Bundle "); - return; - } - - // Program tasks - String bundleName = bundleName(args[0]); - var files = readFiles(args[0]); - manifest(files, bundleName); - var blob = blob(files); - var png = png(blob); - template(png, bundleName); - } - -} diff --git a/app/_boot.js b/app/_boot.js deleted file mode 100644 index 5e93351..0000000 --- a/app/_boot.js +++ /dev/null @@ -1,308 +0,0 @@ -/* - The Bundle.java utility prepends a file manifest to this script before - execution is started. This script runs within the context of an async - function. -*/ - - - -/////////////////////////////////////////////////////////////////////////////// -// Bundle // -/////////////////////////////////////////////////////////////////////////////// - -// Bundled file manager -let Bundle = globalThis.Bundle = new class Bundle extends Array { - - constructor() { - super(); - - // Configure instance fields - this.debug = - location.protocol != "file:" && location.hash == "#debug"; - this.decoder = new TextDecoder(); - this.encoder = new TextEncoder(); - this.moduleCall = (... a)=>this.module (... a); - this.resourceCall = (... a)=>this.resource(... a); - - // Generate the CRC32 lookup table - this.crcLookup = new Uint32Array(256); - for (let x = 0; x <= 255; x++) { - let l = x; - for (let j = 7; j >= 0; j--) - l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); - this.crcLookup[x] = l; - } - - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a file to the bundle - add(name, data) { - this.push(this[name] = new BundledFile(name, data)); - } - - // Export all bundled resources to a .ZIP file - save() { - let centrals = new Array(this.length); - let locals = new Array(this.length); - let offset = 0; - let size = 0; - - // Encode file and directory entries - for (let x = 0; x < this.length; x++) { - let file = this[x]; - let sum = this.crc32(file.data); - locals [x] = file.toZipHeader(sum); - centrals[x] = file.toZipHeader(sum, offset); - offset += locals [x].length; - size += centrals[x].length; - } - - // Encode end of central directory - let end = []; - this.writeInt(end, 4, 0x06054B50); // Signature - this.writeInt(end, 2, 0); // Disk number - this.writeInt(end, 2, 0); // Central dir start disk - this.writeInt(end, 2, this.length); // # central dir this disk - this.writeInt(end, 2, this.length); // # central dir total - this.writeInt(end, 4, size); // Size of central dir - this.writeInt(end, 4, offset); // Offset of central dir - this.writeInt(end, 2, 0); // .ZIP comment length - - // Prompt the user to save the resulting file - let a = document.createElement("a"); - a.download = bundleName + ".zip"; - a.href = URL.createObjectURL(new Blob( - locals.concat(centrals).concat([Uint8Array.from(end)]), - { type: "application/zip" } - )); - a.style.visibility = "hidden"; - document.body.appendChild(a); - a.click(); - a.remove(); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Write a byte array into an output buffer - writeBytes(data, bytes) { - //data.push(... bytes); - for (let b of bytes) - data.push(b); - } - - // Write an integer into an output buffer - writeInt(data, size, value) { - for (; size > 0; size--, value >>= 8) - data.push(value & 0xFF); - } - - // Write a string of text as bytes into an output buffer - writeString(data, text) { - data.push(... this.encoder.encode(text)); - } - - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Calculate the CRC32 checksum for a byte array - crc32(data) { - let c = 0xFFFFFFFF; - for (let x = 0; x < data.length; x++) - c = ((c >>> 8) ^ this.crcLookup[(c ^ data[x]) & 0xFF]); - return ~c & 0xFFFFFFFF; - } - -}(); - - - -/////////////////////////////////////////////////////////////////////////////// -// BundledFile // -/////////////////////////////////////////////////////////////////////////////// - -// Individual bundled file -class BundledFile { - - constructor(name, data) { - - // Configure instance fields - this.data = data; - this.name = name; - - // Resolve the MIME type - this.mime = - name.endsWith(".css" ) ? "text/css;charset=UTF-8" : - name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" : - name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" : - name.endsWith(".json" ) ? "application/json;charset=UTF-8" : - name.endsWith(".png" ) ? "image/png" : - name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" : - name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" : - name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" : - name.endsWith(".wasm" ) ? "application/wasm" : - name.endsWith(".woff2") ? "font/woff2" : - "application/octet-stream" - ; - - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Install a font from a bundled resource - async installFont(name) { - if (name === undefined) { - name = "/" + this.name; - name = name.substring(name.lastIndexOf("/") + 1); - } - let ret = new FontFace(name, "url('"+ - (Bundle.debug ? this.name : this.toDataURL()) + "'"); - await ret.load(); - document.fonts.add(ret); - return ret; - } - - // Install an image as a CSS icon from a bundled resource - installImage(name, filename) { - document.documentElement.style.setProperty("--" + - name || this.name.replaceAll(/\/\./, "_"), - "url('" + (Bundle.debug ? - filename || this.name : this.toDataURL()) + "')"); - return name; - } - - // Install a stylesheet from a bundled resource - installStylesheet(enabled) { - let ret = document.createElement("link"); - ret.href = Bundle.debug ? this.name : this.toDataURL(); - ret.rel = "stylesheet"; - ret.type = "text/css"; - ret.setEnabled = enabled=>{ - if (enabled) - ret.removeAttribute("disabled"); - else ret.setAttribute("disabled", ""); - }; - ret.setEnabled(!!enabled); - document.head.appendChild(ret); - return ret; - } - - // Encode the file data as a data URL - toDataURL() { - return "data:" + this.mime + ";base64," + - btoa(String.fromCharCode(...this.data)); - } - - // Interpret the file's contents as bundled script source data URL - toScript() { - - // Process all URL strings prefixed with /**/ - let parts = this.toString().split("/**/"); - let src = parts.shift(); - for (let part of parts) { - let quote = part.indexOf("\"", 1); - - // Begin with the path of the current file - let path = this.name.split("/"); - path.pop(); - - // Navigate to the path of the target file - let file = part.substring(1, quote).split("/"); - while (file.length > 0) { - let sub = file.shift(); - switch (sub) { - case "..": path.pop(); // Fallthrough - case "." : break; - default : path.push(sub); - } - } - - // Append the file as a data URL - file = Bundle[path.join("/")]; - src += "\"" + file[ - file.mime.startsWith("text/javascript") ? - "toScript" : "toDataURL" - ]() + "\"" + part.substring(quote + 1); - } - - // Encode the transformed source as a data URL - return "data:" + this.mime + ";base64," + btoa(src); - } - - // Decode the file data as a UTF-8 string - toString() { - return Bundle.decoder.decode(this.data); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Produce a .ZIP header for export - toZipHeader(crc32, offset) { - let central = offset !== undefined; - let ret = []; - if (central) { - Bundle.writeInt (ret, 4, 0x02014B50); // Signature - Bundle.writeInt (ret, 2, 20); // Version created by - } else - Bundle.writeInt (ret, 4, 0x04034B50); // Signature - Bundle.writeInt (ret, 2, 20); // Version required - Bundle.writeInt (ret, 2, 0); // Bit flags - Bundle.writeInt (ret, 2, 0); // Compression method - Bundle.writeInt (ret, 2, 0); // Modified time - Bundle.writeInt (ret, 2, 0); // Modified date - Bundle.writeInt (ret, 4, crc32); // Checksum - Bundle.writeInt (ret, 4, this.data.length); // Compressed size - Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size - Bundle.writeInt (ret, 2, this.name.length); // Filename length - Bundle.writeInt (ret, 2, 0); // Extra field length - if (central) { - Bundle.writeInt (ret, 2, 0); // File comment length - Bundle.writeInt (ret, 2, 0); // Disk number start - Bundle.writeInt (ret, 2, 0); // Internal attributes - Bundle.writeInt (ret, 4, 0); // External attributes - Bundle.writeInt (ret, 4, offset); // Relative offset - } - Bundle.writeString (ret, this.name); // Filename - if (!central) - Bundle.writeBytes(ret, this.data); // File data - return Uint8Array.from(ret); - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// Boot Program // -/////////////////////////////////////////////////////////////////////////////// - -// De-register the boot function -delete globalThis.a; - -// Remove the bundle image element from the document -Bundle.src = arguments[0].src; -arguments[0].remove(); - -// Convert the file manifest into BundledFile objects -let buffer = arguments[1]; -let offset = arguments[2] - manifest[0][1]; -for (let entry of manifest) { - Bundle.add(entry[0], buffer.subarray(offset, offset + entry[1])); - offset += entry[1]; - if (Bundle.length == 1) - offset++; // Skip null delimiter -} - -// Begin program operations -import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript()); diff --git a/app/app/App.js b/app/app/App.js deleted file mode 100644 index 0997582..0000000 --- a/app/app/App.js +++ /dev/null @@ -1,448 +0,0 @@ -import { Debugger } from /**/"./Debugger.js"; - - - -/////////////////////////////////////////////////////////////////////////////// -// App // -/////////////////////////////////////////////////////////////////////////////// - -// Web-based emulator application -class App extends Toolkit { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(options) { - super({ - className: "tk tk-app", - label : "app.title", - role : "application", - tagName : "div", - style : { - display : "flex", - flexDirection: "column" - } - }); - - // Configure instance fields - options = options || {}; - this.debugMode = true; - this.dualSims = false; - this.core = options.core; - this.linkSims = true; - this.locales = {}; - this.themes = {}; - this.Toolkit = Toolkit; - - // Configure themes - if ("themes" in options) - for (let theme of Object.entries(options.themes)) - this.addTheme(theme[0], theme[1]); - if ("theme" in options) - this.setTheme(options.theme); - - // Configure locales - if ("locales" in options) - for (let locale of options.locales) - this.addLocale(locale); - if ("locale" in options) - this.setLocale(options.locale); - - // Configure widget - this.localize(this); - - // Not presenting a standalone application - if (!options.standalone) - return; - - // Set up standalone widgets - this.initMenuBar(); - this.desktop = new Toolkit.Desktop(this, - { style: { flexGrow: 1 } }); - this.add(this.desktop); - - // Configure document for presentation - document.body.className = "tk tk-body"; - window.addEventListener("resize", e=> - this.element.style.height = window.innerHeight + "px"); - window.dispatchEvent(new Event("resize")); - document.body.appendChild(this.element); - - // Configure debugger components - this[0] = new Debugger(this, 0, this.core[0]); - this[1] = new Debugger(this, 1, this.core[1]); - - // Configure subscription handling - this.subscriptions = { - [this.core[0].sim]: this[0], - [this.core[1].sim]: this[1] - }; - this.core.onsubscriptions = e=>this.onSubscriptions(e); - - // Temporary config debugging - console.log("Memory keyboard commands:"); - console.log(" Ctrl+G: Goto"); - console.log("Disassembler keyboard commands:"); - console.log(" Ctrl+B: Toggle bytes column"); - console.log(" Ctrl+F: Fit columns"); - console.log(" Ctrl+G: Goto"); - console.log(" F10: Run to next"); - console.log(" F11: Single step"); - console.log("Call dasm(\"key\", value) in the console " + - "to configure the disassembler:"); - console.log(this[0].getDasmConfig()); - window.dasm = (key, value)=>{ - let config = this[0].getDasmConfig(); - if (!key in config || typeof value != typeof config[key]) - return; - if (typeof value == "number" && value != 1 && value != 0) - return; - config[key] = value; - this[0].setDasmConfig(config); - this[1].setDasmConfig(config); - return this[0].getDasmConfig(); - }; - } - - // Configure file menu - initFileMenu(menuBar) { - let menu, item; - - // Menu - menuBar.add(menu = menuBar.file = new Toolkit.MenuItem(this, - { text: "app.menu.file._" })); - - // Load ROM - menu.add(item = menu.loadROM0 = new Toolkit.MenuItem(this, { - text: "app.menu.file.loadROM" - })); - item.setSubstitution("sim", ""); - item.addEventListener("action", - ()=>this.promptFile(f=>this.loadROM(0, f))); - menu.add(item = menu.loadROM1 = new Toolkit.MenuItem(this, { - text : "app.menu.file.loadROM", - visible: false - })); - item.setSubstitution("sim", " 2"); - item.addEventListener("action", - ()=>this.promptFile(f=>this.loadROM(1, f))); - - // Debug Mode - menu.add(item = menu.debugMode = new Toolkit.MenuItem(this, { - checked: this.debugMode, - enabled: false, - text : "app.menu.file.debugMode", - type : "check" - })); - item.addEventListener("action", e=>e.component.setChecked(true)); - } - - // Configure Emulation menu - initEmulationMenu(menuBar) { - let menu, item; - - menuBar.add(menu = menuBar.emulation = new Toolkit.MenuItem(this, - { text: "app.menu.emulation._" })); - - menu.add(item = menu.runPause = new Toolkit.MenuItem(this, { - enabled: false, - text : "app.menu.emulation.run" - })); - - menu.add(item = menu.reset = new Toolkit.MenuItem(this, { - enabled: false, - text : "app.menu.emulation.reset" - })); - - menu.add(item = menu.dualSims = new Toolkit.MenuItem(this, { - checked: this.dualSims, - text : "app.menu.emulation.dualSims", - type : "check" - })); - item.addEventListener("action", - e=>this.setDualSims(e.component.isChecked)); - - menu.add(item = menu.linkSims = new Toolkit.MenuItem(this, { - checked: this.linkSims, - text : "app.menu.emulation.linkSims", - type : "check", - visible: this.dualSims - })); - item.addEventListener("action", - e=>this.setLinkSims(e.component.isChecked)); - } - - // Configure Debug menus - initDebugMenu(menuBar, sim) { - let menu, item; - - menuBar.add(menu = menuBar["debug" + sim] = - new Toolkit.MenuItem(this, { - text : "app.menu.debug._", - visible: sim == 0 || this.dualSims - })); - menu.setSubstitution("sim", - sim == 1 || this.dualSims ? " " + (sim + 1) : ""); - - menu.add(item = menu.console = new Toolkit.MenuItem(this, - { text: "app.menu.debug.console", enabled: false })); - - menu.add(item = menu.memory = new Toolkit.MenuItem(this, - { text: "app.menu.debug.memory" })); - item.addEventListener("action", - ()=>this.showWindow(this[sim].memoryWindow)); - - menu.add(item = menu.cpu = new Toolkit.MenuItem(this, - { text: "app.menu.debug.cpu" })); - item.addEventListener("action", - ()=>this.showWindow(this[sim].cpuWindow)); - - menu.add(item = menu.breakpoints = new Toolkit.MenuItem(this, - { text: "app.menu.debug.breakpoints", enabled: false })); - menu.addSeparator(); - menu.add(item = menu.palettes = new Toolkit.MenuItem(this, - { text: "app.menu.debug.palettes", enabled: false })); - menu.add(item = menu.characters = new Toolkit.MenuItem(this, - { text: "app.menu.debug.characters", enabled: false })); - menu.add(item = menu.bgMaps = new Toolkit.MenuItem(this, - { text: "app.menu.debug.bgMaps", enabled: false })); - menu.add(item = menu.objects = new Toolkit.MenuItem(this, - { text: "app.menu.debug.objects", enabled: false })); - menu.add(item = menu.worlds = new Toolkit.MenuItem(this, - { text: "app.menu.debug.worlds", enabled: false })); - menu.add(item = menu.frameBuffers = new Toolkit.MenuItem(this, - { text: "app.menu.debug.frameBuffers", enabled: false })); - - } - - // Configure Theme menu - initThemeMenu(menuBar) { - let menu, item; - - menuBar.add(menu = menuBar.theme = new Toolkit.MenuItem(this, - { text: "app.menu.theme._" })); - - menu.add(item = menu.light = new Toolkit.MenuItem(this, - { text: "app.menu.theme.light" })); - item.addEventListener("action", e=>this.setTheme("light")); - - menu.add(item = menu.dark = new Toolkit.MenuItem(this, - { text: "app.menu.theme.dark" })); - item.addEventListener("action", e=>this.setTheme("dark")); - - menu.add(item = menu.virtual = new Toolkit.MenuItem(this, - { text: "app.menu.theme.virtual" })); - item.addEventListener("action", e=>this.setTheme("virtual")); - } - - // Set up the menu bar - initMenuBar() { - let menuBar = this.menuBar = new Toolkit.MenuBar(this, - { label: "app.menu._" }); - this.initFileMenu (menuBar); - this.initEmulationMenu(menuBar); - this.initDebugMenu (menuBar, 0); - this.initDebugMenu (menuBar, 1); - this.initThemeMenu (menuBar); - this.add(menuBar); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Subscriptions arrived from the core thread - onSubscriptions(subscriptions) { - for (let sim of Object.entries(subscriptions)) { - let dbg = this.subscriptions[sim[0]]; - for (let sub of Object.entries(sim[1])) switch (sub[0]) { - case "proregs": dbg.programRegisters.refresh(sub[1]); break; - case "sysregs": dbg.systemRegisters .refresh(sub[1]); break; - case "dasm" : dbg.disassembler .refresh(sub[1]); break; - case "memory" : dbg.memory .refresh(sub[1]); break; - } - } - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Register a locale JSON - addLocale(locale) { - if (!("id" in locale)) - throw "No id field in locale"; - this.locales[locale.id] = Toolkit.flatten(locale); - } - - // Register a theme stylesheet - addTheme(id, stylesheet) { - this.themes[id] = stylesheet; - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Specify the language for localization management - setLocale(id) { - if (!(id in this.locales)) { - let lang = id.substring(0, 2); - id = "en-US"; - for (let key of Object.keys(this.locales)) { - if (key.substring(0, 2) == lang) { - id = key; - break; - } - } - } - super.setLocale(this.locales[id]); - } - - // Specify the active color theme - setTheme(key) { - if (!(key in this.themes)) - return; - for (let tkey of Object.keys(this.themes)) - this.themes[tkey].setEnabled(tkey == key); - } - - // Regenerate localized display text - translate() { - if (arguments.length != 0) - return super.translate.apply(this, arguments); - document.title = super.translate("app.title", this); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Load a ROM for a simulation - async loadROM(index, file) { - - // No file was given - if (!file) - return; - - // Load the file into memory - try { file = new Uint8Array(await file.arrayBuffer()); } - catch { - alert(this.translate("error.fileRead")); - return; - } - - // Validate file size - if ( - file.length < 1024 || - file.length > 0x1000000 || - (file.length - 1 & file.length) != 0 - ) { - alert(this.translate("error.romNotVB")); - return; - } - - // Load the ROM into the simulation - if (!(await this[index].sim.setROM(file, { refresh: true }))) { - alert(this.translate("error.romNotVB")); - return; - } - - // Seek the disassembler to PC - this[index].disassembler.seek(0xFFFFFFF0, true); - } - - // Prompt the user to select a file - promptFile(then) { - let file = document.createElement("input"); - file.type = "file"; - file.addEventListener("input", - e=>file.files[0] && then(file.files[0])); - file.click(); - } - - // Attempt to run until the next instruction - async runNext(index) { - let two = this.dualSims && this.linkSims; - - // Perform the operation - let data = await this.core.runNext( - this[index].sim.sim, - two ? this[index ^ 1].sim.sim : 0, { - refresh: true - }); - - // Update the disassemblers - this[index].disassembler.pc = data.pc[0]; - this[index].disassembler.seek(data.pc[0]); - if (two) { - this[index ^ 1].disassembler.pc = data.pc[1]; - this[index ^ 1].disassembler.seek(data.pc[1]); - } - } - - // Specify whether dual sims mode is active - setDualSims(dualSims) { - let sub = dualSims ? " 1" : ""; - - // Configure instance fields - this.dualSims = dualSims = !!dualSims; - - // Configure menus - this.menuBar.emulation.dualSims.setChecked(dualSims); - this.menuBar.emulation.linkSims.setVisible(dualSims); - this.menuBar.file.loadROM0.setSubstitution("sim", sub); - this.menuBar.file.loadROM1.setVisible(dualSims); - this.menuBar.debug0.setSubstitution("sim", sub); - this.menuBar.debug1.setVisible(dualSims); - - // Configure debuggers - this[0].setDualSims(dualSims); - this[1].setDualSims(dualSims); - this.core.connect(this[0].sim.sim, - dualSims && this.linkSims ? this[1].sim.sim : 0); - } - - // Specify whether the sims are connected for communicatinos - setLinkSims(linked) { - linked = !!linked; - - // State is not changing - if (linked == this.linkSims) - return; - - // Link or un-link the sims - if (this.dualSims) - this.core.connect(this[0].sim.sim, linked ? this[1].sim.sim : 0); - } - - // Display a window - showWindow(wnd) { - wnd.setVisible(true); - wnd.focus() - } - - // Execute one instruction - async singleStep(index) { - let two = this.dualSims && this.linkSims; - - // Perform the operation - let data = await this.core.singleStep( - this[index].sim.sim, - two ? this[index ^ 1].sim.sim : 0, { - refresh: true - }); - - // Update the disassemblers - this[index].disassembler.pc = data.pc[0]; - this[index].disassembler.seek(data.pc[0]); - if (two) { - this[index ^ 1].disassembler.pc = data.pc[1]; - this[index ^ 1].disassembler.seek(data.pc[1]); - } - } - -} - - - -export { App }; diff --git a/app/app/CPU.js b/app/app/CPU.js deleted file mode 100644 index f3ec1dd..0000000 --- a/app/app/CPU.js +++ /dev/null @@ -1,106 +0,0 @@ -import { Disassembler } from /**/"./Disassembler.js"; - - - - - - - - -/////////////////////////////////////////////////////////////////////////////// -// CPU // -/////////////////////////////////////////////////////////////////////////////// - -// CPU register editor and disassembler -class CPU extends Toolkit.SplitPane { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(app, sim) { - super(app, { - className: "tk tk-splitpane tk-cpu", - edge : Toolkit.SplitPane.RIGHT, - style : { - position: "relative" - } - }); - - this.app = app; - this.sim = sim; - this.initDasm(); - - this.metrics = new Toolkit.Component(this.app, { - className: "tk tk-mono", - tagName : "div", - style : { - position : "absolute", - visibility: "hidden" - } - }); - let text = ""; - for (let x = 0; x < 16; x++) { - if (x) text += "\n"; - let digit = x.toString(16); - text += digit + "\n" + digit.toUpperCase(); - } - this.metrics.element.innerText = text; - this.splitter.append(this.metrics.element); - - this.setView(1, this.regs = new Toolkit.SplitPane(app, { - className: "tk tk-splitpane", - edge : Toolkit.SplitPane.TOP - })); - - this.regs.setView(0, this.sysregs = new RegisterList(this, true )); - this.regs.setView(1, this.proregs = new RegisterList(this, false)); - - // Adjust split panes to the initial size of the System Registers pane - let resize; - let preshow = e=>this.onPreshow(resize); - resize = new ResizeObserver(preshow); - resize.observe(this.sysregs.viewport); - resize.observe(this.metrics.element); - - this.metrics.addEventListener("resize", e=>this.metricsResize()); - } - - initDasm() { - this.dasm = new Disassembler(this.app, this.sim); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - - - // Resize handler prior to first visibility - onPreshow(resize) { - this.metricsResize(); - - // Once the list of registers is visible, stop listening - if (this.isVisible()) { - resize.disconnect(); - this.sysregs.view.element.style.display = "grid"; - return; - } - - // Update the split panes - let sys = this.sysregs.view.element; - let pro = this.proregs.view.element; - this.setValue( - Math.max(sys.scrollWidth, pro.scrollWidth) + - this.sysregs.vertical.getBounds().width - ); - this.regs.setValue( - this.sysregs[PSW].expansion.getBounds().bottom - - sys.getBoundingClientRect().top - ); - } - - -} - - - -export { CPU }; diff --git a/app/app/Debugger.js b/app/app/Debugger.js deleted file mode 100644 index 48490f0..0000000 --- a/app/app/Debugger.js +++ /dev/null @@ -1,187 +0,0 @@ -import { Disassembler } from /**/"./Disassembler.js"; -import { Memory } from /**/"./Memory.js"; -import { RegisterList } from /**/"./RegisterList.js"; - - - -class Debugger { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(app, index, sim) { - - // Configure instance fields - this.app = app; - this.index = index; - this.sim = sim; - - // Configure components - this.disassembler = new Disassembler(this); - this.memory = new Memory (this); - this.programRegisters = new RegisterList(this, false); - this.systemRegisters = new RegisterList(this, true ); - - // Configure windows - this.initCPUWindow (); - this.initMemoryWindow(); - } - - // Set up the CPU window - initCPUWindow() { - let app = this.app; - - // Produce the window - let wnd = this.cpuWindow = new app.Toolkit.Window(app, { - width : 400, - height : 300, - className : "tk tk-window tk-cpu" + (this.index==0?"":" two"), - closeToolTip: "common.close", - title : "cpu._", - visible : false - }); - wnd.setSubstitution("sim", this.index == 1 ? " 2" : ""); - wnd.addEventListener("close", ()=>wnd.setVisible(false)); - app.desktop.add(wnd); - - // Visibility override - let that = this; - let setVisible = wnd.setVisible; - wnd.setVisible = function(visible) { - that.disassembler .setSubscribed(visible); - that.systemRegisters .setSubscribed(visible); - that.programRegisters.setSubscribed(visible); - setVisible.apply(wnd, arguments); - }; - - // Auto-seek the initial view - let onSeek = ()=>this.disassembler.seek(this.disassembler.pc, true); - Toolkit.addResizeListener(this.disassembler.element, onSeek); - wnd.addEventListener("firstshow", ()=>{ - app.desktop.center(wnd); - Toolkit.removeResizeListener(this.disassembler.element, onSeek); - }); - - // Window splitters - let sptMain = new Toolkit.SplitPane(this.app, { - className: "tk tk-splitpane tk-main", - edge : Toolkit.SplitPane.RIGHT - }); - let sptRegs = new Toolkit.SplitPane(this.app, { - className: "tk tk-splitpane tk-registers", - edge : Toolkit.SplitPane.TOP - }); - - // Configure window splitter initial size - let resize = new ResizeObserver(()=>{ - - // Measure register lists - let sys = this.systemRegisters .getPreferredSize(); - let pro = this.programRegisters.getPreferredSize(); - let height = Math.ceil(Math.max(sys.height, pro.height)); - let width = Math.ceil(Math.max(sys.width , pro.width )) + - this.systemRegisters.vertical.getBounds().width; - - // Configure splitters - if (sptMain.getValue() != width) - sptMain.setValue(width); - if (sptRegs.getValue() != height) - sptRegs.setValue(height); - }); - resize.observe(this.programRegisters.view.element); - resize.observe(this.systemRegisters .view.element); - - // Stop monitoring splitter size when something receives focus - let onFocus = e=>{ - resize.disconnect(); - wnd.removeEventListener("focus", onFocus, true); - }; - sptMain.addEventListener("focus", onFocus, true); - - // Configure window layout - sptMain.setView(0, this.disassembler); - sptMain.setView(1, sptRegs); - sptRegs.setView(0, this.systemRegisters); - sptRegs.setView(1, this.programRegisters); - wnd.append(sptMain); - } - - // Set up the Memory window - initMemoryWindow() { - let app = this.app; - - // Produce the window - let wnd = this.memoryWindow = new app.Toolkit.Window(app, { - width : 400, - height : 300, - className : "tk tk-window" + (this.index == 0 ? "" : " two"), - closeToolTip: "common.close", - title : "memory._", - visible : false - }); - wnd.setSubstitution("sim", this.index == 1 ? " 2" : ""); - wnd.addEventListener("close" , ()=>wnd.setVisible(false)); - wnd.addEventListener("firstshow", ()=>app.desktop.center(wnd)); - app.desktop.add(wnd); - - // Visibility override - let that = this; - let setVisible = wnd.setVisible; - wnd.setVisible = function(visible) { - that.memory.setSubscribed(visible); - setVisible.apply(wnd, arguments); - }; - - // Configure window layout - wnd.append(this.memory); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Retrieve the disassembler configuraiton - getDasmConfig() { - return this.disassembler.getConfig(); - } - - // Attempt to run until the next instruction - runNext() { - this.app.runNext(this.index); - } - - // Update the disassembler configuration - setDasmConfig(config) { - this.disassembler .setConfig(config); - this.memory .dasmChanged(); - this.programRegisters.dasmChanged(); - this.systemRegisters .dasmChanged(); - } - - // Specify whether dual sims mode is active - setDualSims(dualSims) { - - // Update substitutions for sim 1 - if (this.index == 0) { - let sub = dualSims ? " 1" : ""; - this.cpuWindow .setSubstitution("sim", sub); - this.memoryWindow.setSubstitution("sim", sub); - } - - // Hide windows for sim 2 - else if (!dualSims) { - this.cpuWindow .close(false); - this.memoryWindow.close(false); - } - - } - - // Execute one instruction - singleStep() { - this.app.singleStep(this.index); - } - -} - - - -export { Debugger }; diff --git a/app/app/Disassembler.js b/app/app/Disassembler.js deleted file mode 100644 index 249989d..0000000 --- a/app/app/Disassembler.js +++ /dev/null @@ -1,958 +0,0 @@ -import { Util } from /**/"../app/Util.js"; - - - -// Opcode definition -class Opdef { - constructor(format, mnemonic, signExtend) { - this.format = format; - this.mnemonic = mnemonic; - this.signExtend = !!signExtend; - } -} - -// Top-level opcode definition lookup table by opcode -let OPDEFS = [ - new Opdef(1, "MOV" ), new Opdef(1, "ADD" ), new Opdef(1, "SUB" ), - new Opdef(1, "CMP" ), new Opdef(1, "SHL" ), new Opdef(1, "SHR" ), - new Opdef(1, "JMP" ), new Opdef(1, "SAR" ), new Opdef(1, "MUL" ), - new Opdef(1, "DIV" ), new Opdef(1, "MULU" ), new Opdef(1, "DIVU" ), - new Opdef(1, "OR" ), new Opdef(1, "AND" ), new Opdef(1, "XOR" ), - new Opdef(1, "NOT" ), new Opdef(2, "MOV" ,1), new Opdef(2, "ADD",1), - new Opdef(2, "SETF" ), new Opdef(2, "CMP" ,1), new Opdef(2, "SHL" ), - new Opdef(2, "SHR" ), new Opdef(2, "CLI" ), new Opdef(2, "SAR" ), - new Opdef(2, "TRAP" ), new Opdef(2, "RETI" ), new Opdef(2, "HALT" ), - new Opdef(0, null ), new Opdef(2, "LDSR" ), new Opdef(2, "STSR" ), - new Opdef(2, "SEI" ), new Opdef(2, null ), new Opdef(3, "Bcond"), - new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"), - new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"), - new Opdef(3, "Bcond"), new Opdef(5,"MOVEA",1), new Opdef(5,"ADDI",1), - new Opdef(4, "JR" ), new Opdef(4, "JAL" ), new Opdef(5, "ORI" ), - new Opdef(5, "ANDI" ), new Opdef(5, "XORI" ), new Opdef(5, "MOVHI"), - new Opdef(6, "LD.B" ), new Opdef(6, "LD.H" ), new Opdef(0, null ), - new Opdef(6, "LD.W" ), new Opdef(6, "ST.B" ), new Opdef(6, "ST.H" ), - new Opdef(0, null ), new Opdef(6, "ST.W" ), new Opdef(6, "IN.B" ), - new Opdef(6, "IN.H" ), new Opdef(6, "CAXI" ), new Opdef(6, "IN.W" ), - new Opdef(6, "OUT.B"), new Opdef(6, "OUT.H" ), new Opdef(7, null ), - new Opdef(6, "OUT.W") -]; - -// Bit string mnemonic lookup table by sub-opcode -let BITSTRINGS = [ - "SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD", - null , null , null , null , - "ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" , - "ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" -]; - -// Floating-point/Nintendo mnemonic lookup table by sub-opcode -let FLOATENDOS = [ - "CMPF.S", null , "CVT.WS", "CVT.SW" , - "ADDF.S", "SUBF.S", "MULF.S", "DIVF.S" , - "XB" , "XH" , "REV" , "TRNC.SW", - "MPYHW" -]; - -// Program register names -let PROREGS = { 2: "hp", 3: "sp", 4: "gp", 5: "tp", 31: "lp" }; - -// System register names -let SYSREGS = [ - "EIPC", "EIPSW", "FEPC", "FEPSW", - "ECR" , "PSW" , "PIR" , "TKCW" , - null , null , null , null , - null , null , null , null , - null , null , null , null , - null , null , null , null , - "CHCW", "ADTRE", null , null , - null , null , null , null -]; - -// Condition mnemonics -let CONDS = [ - "V" , ["C" , "L" ], ["E" , "Z" ], "NH", - "N" , "T" , "LT" , "LE", - "NV", ["NC", "NL"], ["NE", "NZ"], "H" , - "P" , "F" , "GE" , "GT" -]; - -// Output setting keys -const SETTINGS = [ - "bcondMerged", "branchAddress", "condCase", "condCL", "condEZ", - "condNames", "hexCaps", "hexDollar", "hexSuffix", "imm5OtherHex", - "imm5ShiftHex", "imm5TrapHex", "imm16AddiLargeHex", "imm16AddiSmallHex", - "imm16MoveHex", "imm16OtherHex", "jmpBrackets", "memoryLargeHex", - "memorySmallHex", "memoryInside", "mnemonicCaps", "operandReverse", - "proregCaps", "proregNames", "setfMerged", "sysregCaps", "sysregNames" -]; - - - -/////////////////////////////////////////////////////////////////////////////// -// Line // -/////////////////////////////////////////////////////////////////////////////// - -// One line of output -class Line { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(parent, first) { - - // Configure instance fields - this.first = first; - this.parent = parent; - - // Configure labels - this.lblAddress = this.label("tk-address" , first); - this.lblBytes = this.label("tk-bytes" , first); - this.lblMnemonic = this.label("tk-mnemonic", first); - this.lblOperands = this.label("tk-operands", false); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the elements' display - refresh(row, isPC) { - - // The row is not available - if (!row) { - this.lblAddress .innerText = "--------"; - this.lblBytes .innerText = ""; - this.lblMnemonic.innerText = "---"; - this.lblOperands.innerText = ""; - } - - // Update labels with the disassembled row's contents - else { - this.lblAddress .innerText = row.address; - this.lblBytes .innerText = row.bytes; - this.lblMnemonic.innerText = row.mnemonic; - this.lblOperands.innerText = row.operands; - } - - // Update style according to selection - let method = row && isPC ? "add" : "remove"; - this.lblAddress .classList[method]("tk-selected"); - this.lblBytes .classList[method]("tk-selected"); - this.lblMnemonic.classList[method]("tk-selected"); - this.lblOperands.classList[method]("tk-selected"); - } - - // Specify whether the elements on this line are visible - setVisible(visible) { - - // Column elements - let columns = [ - this.lblAddress, - this.lblBytes, - this.lblMnemonic, - this.lblOperands - ]; - - // Column elements on the first row - if (this.first) { - columns[0] = columns[0].parentNode; // Address - columns[1] = columns[1].parentNode; // Bytes - columns[2] = columns[2].parentNode; // Mnemonic - } - - // Column visibility - visible = [ - visible, // Address - visible && this.parent.hasBytes, // Bytes - visible, // Mnemonic - visible // Operands - ]; - - // Configure elements - for (let x = 0; x < 4; x++) - columns[x].style.display = visible[x] ? "block" : "none"; - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Create a display label - label(className, first) { - - // Create the label element - let label = document.createElement("div"); - label.className = "tk " + className; - - // The label is part of the first row of output - let element = label; - if (first) { - - // Create a container element - element = document.createElement("div"); - element.append(label); - element.max = 0; - - // Ensure the container can always fit the column contents - Toolkit.addResizeListener(element, ()=>{ - let width = Math.ceil(label.getBoundingClientRect().width); - if (width <= element.max) - return; - element.max = width; - element.style.minWidth = width + "px"; - }); - - } - - // Configure elements - this.parent.view.append(element); - return label; - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// Disassembler // -/////////////////////////////////////////////////////////////////////////////// - -// Text disassembler for NVC -class Disassembler extends Toolkit.ScrollPane { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(debug) { - super(debug.app, { - className : "tk tk-scrollpane tk-disassembler", - horizontal: Toolkit.ScrollPane.AS_NEEDED, - focusable : true, - tabStop : true, - tagName : "div", - vertical : Toolkit.ScrollPane.NEVER - }); - - // Configure instance fields - this.address = Util.u32(0xFFFFFFF0); - this.app = debug.app; - this.columns = [ 0, 0, 0, 0 ]; - this.data = []; - this.debug = debug; - this.hasBytes = true; - this.isSubscribed = false; - this.lines = null; - this.pc = this.address; - this.pending = []; - this.rows = []; - this.scroll = 0; - this.sim = debug.sim; - - // Default output settings - this.setConfig({ - bcondMerged : true, - branchAddress : true, - condCase : false, - condCL : 1, - condEZ : 1, - condNames : true, - hexCaps : true, - hexDollar : false, - hexSuffix : false, - imm5OtherHex : false, - imm5ShiftHex : false, - imm5TrapHex : false, - imm16AddiLargeHex: true, - imm16AddiSmallHex: false, - imm16MoveHex : true, - imm16OtherHex : true, - jmpBrackets : true, - memoryLargeHex : true, - memorySmallHex : false, - memoryInside : false, - mnemonicCaps : true, - operandReverse : false, - proregCaps : false, - proregNames : true, - setfMerged : false, - sysregCaps : true, - sysregNames : true - }); - - // Configure viewport - this.viewport.classList.add("tk-mono"); - - // Configure view - let view = document.createElement("div"); - view.className = "tk tk-view"; - Object.assign(view.style, { - display : "grid", - gridTemplateColumns: "repeat(3, max-content) auto" - }); - this.setView(view); - - // Font-measuring element - this.metrics = new Toolkit.Component(this.app, { - className: "tk tk-metrics tk-mono", - tagName : "div", - style : { - position : "absolute", - visibility: "hidden" - } - }); - this.metrics.element.innerText = "X"; - this.append(this.metrics.element); - - // First row always exists - this.lines = [ new Line(this, true) ]; - - // Configure event handlers - Toolkit.addResizeListener(this.viewport, e=>this.onResize(e)); - this.addEventListener("keydown", e=>this.onKeyDown (e)); - this.addEventListener("wheel" , e=>this.onMouseWheel(e)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Key press - onKeyDown(e) { - let tall = this.tall(false); - - - // Ctrl key is pressed - if (e.ctrlKey) switch (e.key) { - - // Toggle bytes column - case "b": case "B": - this.showBytes(!this.hasBytes); - break; - - // Fit columns - case "f": case "F": - this.fitColumns(); - break; - - // Goto - case "g": case "G": - this.promptGoto(); - break; - - default: return; - } - - // Ctrl key is not pressed - else switch (e.key) { - - // Navigation - case "ArrowDown" : this.fetch(+1 , true); break; - case "ArrowUp" : this.fetch(-1 , true); break; - case "PageDown" : this.fetch(+tall, true); break; - case "PageUp" : this.fetch(-tall, true); break; - - // View control - case "ArrowLeft" : this.horizontal.setValue( - this.horizontal.value - this.horizontal.increment); break; - case "ArrowRight": this.horizontal.setValue( - this.horizontal.value + this.horizontal.increment); break; - - // Single step - case "F10": - this.debug.runNext(); - break; - - // Single step - case "F11": - this.debug.singleStep(); - break; - - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - // Mouse wheel - onMouseWheel(e) { - - // User agent scaling action - if (e.ctrlKey) - return; - - // No rotation has occurred - let offset = Math.sign(e.deltaY) * 3; - if (offset == 0) - return; - - // Update the display address - this.fetch(offset, true); - } - - // Viewport resized - onResize(e) { - let fetch = false; - let tall = this.tall(true); - - // Add additional lines to the output - for (let x = 0; x < tall; x++) { - if (x >= this.lines.length) { - fetch = true; - this.lines.push(new Line(this)); - } - this.lines[x].setVisible(true); - } - - // Remove extra lines from the output - for (let x = tall; x < this.lines.length; x++) - this.lines[x].setVisible(false); - - // Configure horizontal scroll bar - if (this.metrics) - this.horizontal.setIncrement(this.metrics.getBounds().width); - - // Update the display - if (fetch) - this.fetch(0, true); - else this.refresh(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Produce disassembly text - disassemble(rows) { - - // Produce a deep copy of the input list - let copy = new Array(rows.length); - for (let x = 0; x < rows.length; x++) { - copy[x] = {}; - Object.assign(copy[x], rows[x]); - } - rows = copy; - - // Process all rows - for (let row of rows) { - row.operands = []; - - // Read instruction bits from the bus - let bits0 = row.bytes[1] << 8 | row.bytes[0]; - let bits1; - if (row.bytes.length == 4) - bits1 = row.bytes[3] << 8 | row.bytes[2]; - - // Working variables - let opcode = bits0 >> 10; - let opdef = OPDEFS[opcode]; - - // Sub-opcode mnemonics - if (row.opcode == 0b011111) - row.mnemonic = BITSTRINGS[bits0 & 31] || "---"; - else if (row.opcode == 0b111110) - row.mnemonic = FLOATENDOS[bits1 >> 10 & 63] || "---"; - else row.mnemonic = opdef.mnemonic; - - // Processing by format - switch (opdef.format) { - case 1: this.formatI (row, bits0 ); break; - case 3: this.formatIII(row, bits0 ); break; - case 4: this.formatIV (row, bits0, bits1); break; - case 6: this.formatVI (row, bits0, bits1); break; - case 7: this.formatVII(row, bits0 ); break; - case 2: - this.formatII(row, bits0, opdef.signExtend); break; - case 5: - this.formatV (row, bits0, bits1, opdef.signExtend); - } - - // Format bytes - let text = []; - for (let x = 0; x < row.bytes.length; x++) - text.push(row.bytes[x].toString(16).padStart(2, "0")); - row.bytes = text.join(" "); - - // Post-processing - row.address = row.address.toString(16).padStart(8, "0"); - if (this.hexCaps) { - row.address = row.address.toUpperCase(); - row.bytes = row.bytes .toUpperCase(); - } - if (!this.mnemonicCaps) - row.mnemonic = row.mnemonic.toLowerCase(); - if (this.operandReverse) - row.operands.reverse(); - row.operands = row.operands.join(", "); - } - - return rows; - } - - // Retrieve all output settings in an object - getConfig() { - let ret = {}; - for (let key of SETTINGS) - ret[key] = this[key]; - return ret; - } - - // Update with disassembly state from the core - refresh(data = 0) { - let bias; - - // Scrolling prefresh - if (typeof data == "number") - bias = 16 + data; - - // Received data from the core thread - else { - this.data = data.rows; - this.pc = data.pc; - if (this.data.length == 0) - return; - this.address = this.data[0].address; - this.rows = this.disassemble(this.data); - bias = 16 + - (data.scroll === null ? 0 : this.scroll - data.scroll); - } - - // Update elements - let count = Math.min(this.tall(true), this.data.length); - for (let y = 0; y < count; y++) { - let index = bias + y; - let line = this.data[index]; - let row = this.rows[index]; - this.lines[y].refresh(row, line && line.address == this.pc); - } - - // Refesh scroll pane - this.update(); - } - - // Bring an address into view - seek(address, force) { - - // Check if the address is already in the view - if (!force) { - let bias = 16; - let tall = this.tall(false); - let count = Math.min(tall, this.data.length); - - // The address is currently visible in the output - for (let y = 0; y < count; y++) { - let row = this.data[bias + y]; - if (!row || Util.u32(address - row.address) >= row.size) - continue; - - // The address is on this row - this.refresh(); - return; - } - - } - - // Place the address at a particular position in the view - this.address = address; - this.fetch(null); - } - - // Update output settings - setConfig(config) { - - // Update settings - for (let key of SETTINGS) - if (key in config) - this[key] = config[key]; - - // Regenerate output - this.refresh({ - pc : this.pc, - rows : this.data, - scroll: null - }); - } - - // Subscribe to or unsubscribe from core updates - setSubscribed(subscribed) { - subscribed = !!subscribed; - - // Nothing to change - if (subscribed == this.isSubscribed) - return; - - // Configure instance fields - this.isSubscribed = subscribed; - - // Subscribe to core updates - if (subscribed) - this.fetch(0); - - // Unsubscribe from core updates - else this.sim.unsubscribe("dasm"); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Select a condition's name - cond(cond) { - let ret = CONDS[cond]; - switch (cond) { - case 1: case 9: return CONDS[cond][this.condCL]; - case 2: case 10: return CONDS[cond][this.condEZ]; - } - return CONDS[cond]; - } - - // Retrieve disassembly data from the core - async fetch(scroll, prefresh) { - let row; - - // Scrolling relative to the current view - if (scroll) { - if (prefresh) - this.refresh(scroll); - this.scroll = Util.s32(this.scroll + scroll); - row = -scroll; - } - - // Jumping to an address directly - else row = scroll === null ? Math.floor(this.tall(false) / 3) + 16 : 0; - - // Retrieve data from the core - this.refresh( - await this.sim.disassemble( - this.address, - row, - this.tall(true) + 32, - scroll === null ? null : this.scroll, { - subscribe: this.isSubscribed && "dasm" - }) - ); - } - - // Shrink all columns to their minimum size - fitColumns() { - let line = this.lines[0]; - for (let column of [ "lblAddress", "lblBytes", "lblMnemonic" ] ) { - let element = line[column].parentNode; - element.max = 0; - element.style.removeProperty("min-width"); - } - } - - // Represent a hexadecimal value - hex(value, digits) { - let sign = Util.s32(value) < 0 ? "-" : ""; - let ret = Math.abs(Util.u32(value)).toString(16).padStart(digits,"0"); - if (this.hexCaps) - ret = ret.toUpperCase(); - if (this.hexSuffix) - ret = ("abcdefABCDEF".indexOf(ret[0]) == -1 ? "" : "0") + - ret + "h"; - else ret = (this.hexDollar ? "$" : "0x") + ret; - return sign + ret; - } - - // Prompt the user to specify a new address - promptGoto() { - - // Receive input from the user - let address = prompt(this.app.translate("common.gotoPrompt")); - if (address == null) - return; - - // Process the input as an address in hexadecimal - address = parseInt(address, 16); - if (isNaN(address)) - return; - - // Move the selection and refresh the display - this.seek(Util.u32(address)); - } - - // Select a program register name - proreg(index) { - let ret = this.proregNames && PROREGS[index] || "r" + index; - return this.proregCaps ? ret.toUpperCase() : ret; - } - - // Specify whether or not to show the bytes column - showBytes(show) { - let tall = this.tall(true); - - // Configure instance fields - this.hasBytes = show; - - // Configure elements - this.view.style.gridTemplateColumns = - "repeat(" + (show ? 3 : 2) + ", max-content) auto"; - for (let x = 0; x < tall; x++) - this.lines[x].setVisible(true); - - // Measure scroll pane - this.update(); - } - - // Measure how many rows of output are visible - tall(partial) { - let lineHeight = !this.metrics ? 0 : - Math.ceil(this.metrics.getBounds().height); - return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"]( - this.getBounds().height / lineHeight)); - } - - - - //////////////////////////// Decoding Methods ///////////////////////////// - - // Disassemble a Format I instruction - formatI(row, bits0) { - let reg1 = this.proreg(bits0 & 31); - - // JMP - if (row.mnemonic == "JMP") { - if (this.jmpBrackets) - reg1 = "[" + reg1 + "]"; - row.operands.push(reg1); - } - - // Other instructions - else { - let reg2 = this.proreg(bits0 >> 5 & 31); - row.operands.push(reg1, reg2); - } - - } - - // Disassemble a Format II instruction - formatII(row, bits0, signExtend) { - - // Bit-string instructions are zero-operand - if (bits0 >> 10 == 0b011111) - return; - - // Processing by mnemonic - switch (row.mnemonic) { - - // Zero-operand - case "---" : // Fallthrough - case "CLI" : // Fallthrough - case "HALT": // Fallthrough - case "RETI": // Fallthrough - case "SEI" : return; - - // Distinct notation - case "LDSR": return this.ldstsr(row, bits0, true ); - case "SETF": return this.setf (row, bits0 ); - case "STSR": return this.ldstsr(row, bits0, false); - } - - // Retrieve immediate operand - let imm = bits0 & 31; - if (signExtend) - imm = Util.signExtend(bits0, 5); - - // TRAP instruction is one-operand - if (row.mnemonic == "TRAP") { - row.operands.push(this.trapHex ? - this.hex(imm, 1) : imm.toString()); - return; - } - - // Processing by mnemonic - let hex = this.imm5OtherHex; - switch (row.mnemonic) { - case "SAR": // Fallthrough - case "SHL": // Fallthrough - case "SHR": hex = this.imm5ShiftHex; - } - imm = hex ? this.hex(imm, 1) : imm.toString(); - - // Two-operand instruction - let reg2 = this.proreg(bits0 >> 5 & 31); - row.operands.push(imm, reg2); - } - - // Disassemble a Format III instruction - formatIII(row, bits0) { - let cond = this.cond(bits0 >> 9 & 15); - let disp = Util.signExtend(bits0 & 0x1FF, 9); - - // Condition merged with mnemonic - if (this.bcondMerged) { - switch (cond) { - case "F": row.mnemonic = "NOP"; return; - case "T": row.mnemonic = "BR" ; break; - default : row.mnemonic = "B" + cond; - } - } - - // Condition as operand - else { - if (!this.condCaps) - cond = cond.toLowerCase(); - row.operands.push(cond); - } - - // Operand as destination address - if (this.branchAddress) { - disp = Util.u32(row.address + disp & 0xFFFFFFFE) - .toString(16).padStart(8, "0"); - if (this.hexCaps) - disp = disp.toUpperCase(); - row.operands.push(disp); - } - - // Operand as displacement - else { - let sign = disp < 0 ? "-" : disp > 0 ? "+" : ""; - let rel = this.hex(Math.abs(disp), 1); - row.operands.push(sign + rel); - } - - } - - // Disassemble a Format IV instruction - formatIV(row, bits0, bits1) { - let disp = Util.signExtend(bits0 << 16 | bits1, 26); - - // Operand as destination address - if (this.branchAddress) { - disp = Util.u32(row.address + disp & 0xFFFFFFFE) - .toString(16).padStart(8, "0"); - if (this.hexCaps) - disp = disp.toUpperCase(); - row.operands.push(disp); - } - - // Operand as displacement - else { - let sign = disp < 0 ? "-" : disp > 0 ? "+" : ""; - let rel = this.hex(Math.abs(disp), 1); - row.operands.push(sign + rel); - } - - } - - // Disassemble a Format V instruction - formatV(row, bits0, bits1, signExtend) { - let imm = signExtend ? Util.signExtend(bits1) : bits1; - let reg1 = this.proreg(bits0 & 31); - let reg2 = this.proreg(bits0 >> 5 & 31); - - if ( - row.mnemonic == "ADDI" ? - Math.abs(imm) <= 256 ? - this.imm16AddiSmallHex : - this.imm16AddiLargeHex - : row.mnemonic == "MOVEA" || row.mnemonic == "MOVHI" ? - this.imm16MoveHex - : - this.imm16OtherHex - ) imm = this.hex(imm, 4); - - row.operands.push(imm, reg1, reg2); - } - - // Disassemble a Format VI instruction - formatVI(row, bits0, bits1) { - let disp = Util.signExtend(bits1); - let reg1 = this.proreg(bits0 & 31); - let reg2 = this.proreg(bits0 >> 5 & 31); - let sign = - disp < 0 ? "-" : - disp == 0 || !this.memoryInside ? "" : - "+"; - - // Displacement is hexadecimal - disp = Math.abs(disp); - if (disp == 0) - disp = "" - else if (disp <= 256 ? this.memorySmallHex : this.memoryLargeHex) - disp = this.hex(disp, 1); - - // Format the displacement figure according to its presentation - disp = this.memoryInside ? - sign == "" ? "" : " " + sign + " " + disp : - sign + disp - ; - - // Apply operands - row.operands.push(this.memoryInside ? - "[" + reg1 + disp + "]" : - disp + "[" + reg1 + "]", - reg2); - - // Swap operands for output and store instructions - switch (row.mnemonic) { - case "OUT.B": case "OUT.H": case "OUT.W": - case "ST.B" : case "ST.H" : case "ST.W" : - row.operands.reverse(); - } - - } - - // Disassemble a Format VII instruction - formatVII(row, bits0) { - let reg1 = this.proreg(bits0 & 31); - let reg2 = this.proreg(bits0 >> 5 & 31); - - // Invalid sub-opcode is zero-operand - if (row.mnemonic == "---") - return; - - // Processing by mnemonic - switch (row.mnemonic) { - case "XB": // Fallthrough - case "XH": break; - default : row.operands.push(reg1); - } - - row.operands.push(reg2); - } - - // Format an LDSR or STSR instruction - ldstsr(row, bits0, reverse) { - - // System register - let sysreg = bits0 & 31; - sysreg = this.sysregNames && SYSREGS[sysreg] || sysreg.toString(); - if (!this.sysregCaps) - sysreg = sysreg.toLowerCase(); - - // Program register - let reg2 = this.proreg(bits0 >> 5 & 31); - - // Operands - row.operands.push(sysreg, reg2); - if (reverse) - row.operands.reverse(); - } - - // Format a SETF instruction - setf(row, bits0) { - let cond = this.cond (bits0 & 15); - let reg2 = this.proreg(bits0 >> 5 & 31); - - // Condition merged with mnemonic - if (!this.bcondMerged) { - row.mnemonic += cond; - } - - // Condition as operand - else { - if (!this.condCaps) - cond = cond.toLowerCase(); - row.operands.push(cond); - } - - row.operands.push(reg2); - } - -} - - - -export { Disassembler }; diff --git a/app/app/Memory.js b/app/app/Memory.js deleted file mode 100644 index c40b724..0000000 --- a/app/app/Memory.js +++ /dev/null @@ -1,517 +0,0 @@ -import { Util } from /**/"./Util.js"; - - - -// Bus indexes -const MEMORY = 0; - -// Text to hex digit conversion -const DIGITS = { - "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, - "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15 -}; - - - -/////////////////////////////////////////////////////////////////////////////// -// Line // -/////////////////////////////////////////////////////////////////////////////// - -// One line of output -class Line { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(parent, index) { - - // Configure instance fields - this.index = index; - this.parent = parent; - - // Address label - this.lblAddress = document.createElement("div"); - this.lblAddress.className = "tk tk-address"; - parent.view.appendChild(this.lblAddress); - - // Byte labels - this.lblBytes = new Array(16); - for (let x = 0; x < 16; x++) { - let lbl = this.lblBytes[x] = document.createElement("div"); - lbl.className = "tk tk-byte tk-" + x.toString(16); - parent.view.appendChild(lbl); - } - - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the elements' display - refresh() { - let bus = this.parent[this.parent.bus]; - let address = this.parent.mask(bus.address + this.index * 16); - let data = bus.data; - let dataAddress = bus.dataAddress; - let hexCaps = this.parent.dasm.hexCaps; - let offset = - (this.parent.row(address) - this.parent.row(dataAddress)) * 16; - - // Format the line's address - let text = address.toString(16).padStart(8, "0"); - if (hexCaps) - text = text.toUpperCase(); - this.lblAddress.innerText = text; - - // The line's data is not available - if (offset < 0 || offset >= data.length) { - for (let lbl of this.lblBytes) { - lbl.innerText = "--"; - lbl.classList.remove("tk-selected"); - } - } - - // The line's data is available - else for (let x = 0; x < 16; x++, offset++) { - let lbl = this.lblBytes[x]; - text = data[offset].toString(16).padStart(2, "0"); - - // The byte is the current selection - if (Util.u32(address + x) == bus.selection) { - lbl.classList.add("tk-selected"); - if (this.parent.digit !== null) - text = this.parent.digit.toString(16); - } - - // The byte is not the current selection - else lbl.classList.remove("tk-selected"); - - // Update the label's text - if (hexCaps) - text = text.toUpperCase(); - lbl.innerText = text; - } - - } - - // Specify whether the elements on this line are visible - setVisible(visible) { - visible = visible ? "block" : "none"; - this.lblAddress.style.display = visible; - for (let lbl of this.lblBytes) - lbl.style.display = visible; - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// Memory // -/////////////////////////////////////////////////////////////////////////////// - -// Memory hex editor -class Memory extends Toolkit.Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(debug) { - super(debug.app, { - className : "tk tk-memory", - tagName : "div", - style : { - alignItems : "stretch", - display : "grid", - gridTemplateRows: "auto", - position : "relative" - } - }); - - // Configure instance fields - this.app = debug.app; - this.bus = MEMORY; - this.dasm = debug.disassembler; - this.debug = debug; - this.digit = null; - this.isSubscribed = false; - this.lines = []; - this.sim = debug.sim; - - // Initialize bus - this[MEMORY] = { - address : 0x05000000, - data : [], - dataAddress: 0x05000000, - selection : 0x05000000 - }; - - // Configure editor pane - this.editor = new Toolkit.ScrollPane(this.app, { - className : "tk tk-scrollpane tk-editor", - horizontal: Toolkit.ScrollPane.AS_NEEDED, - focusable : true, - tabStop : true, - tagName : "div", - vertical : Toolkit.ScrollPane.NEVER - }); - this.append(this.editor); - - // Configure view - this.view = document.createElement("div"); - this.view.className = "tk tk-view"; - Object.assign(this.view.style, { - display : "grid", - gridTemplateColumns: "repeat(17, max-content)" - }); - this.editor.setView(this.view); - - // Font-measuring element - this.metrics = new Toolkit.Component(this.app, { - className: "tk tk-metrics tk-mono", - tagName : "div", - style : { - position : "absolute", - visibility: "hidden" - } - }); - this.metrics.element.innerText = "X"; - this.append(this.metrics.element); - - // Configure event handlers - Toolkit.addResizeListener(this.editor.viewport, e=>this.onResize(e)); - this.addEventListener("keydown" , e=>this.onKeyDown (e)); - this.addEventListener("pointerdown", e=>this.onPointerDown(e)); - this.addEventListener("wheel" , e=>this.onMouseWheel (e)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Typed a digit - onDigit(digit) { - let bus = this[this.bus]; - - // Begin an edit - if (this.digit === null) { - this.digit = digit; - this.setSelection(bus.selection, true); - } - - // Complete an edit - else { - this.digit = this.digit << 4 | digit; - this.setSelection(bus.selection + 1); - } - - } - - // Key press - onKeyDown(e) { - let bus = this[this.bus]; - let key = e.key; - - // A hex digit was entered - if (key.toUpperCase() in DIGITS) { - this.onDigit(DIGITS[key.toUpperCase()]); - key = "digit"; - } - - // Ctrl key is pressed - if (e.ctrlKey) switch (key) { - - // Goto - case "g": case "G": - this.promptGoto(); - break; - - default: return; - } - - // Ctrl key is not pressed - else switch (key) { - - // Arrow key navigation - case "ArrowDown" : this.setSelection(bus.selection + 16); break; - case "ArrowLeft" : this.setSelection(bus.selection - 1); break; - case "ArrowRight": this.setSelection(bus.selection + 1); break; - case "ArrowUp" : this.setSelection(bus.selection - 16); break; - - // Commit current edit - case "Enter": - case " ": - if (this.digit !== null) - this.setSelection(bus.selection); - break; - - // Page key navigation - case "PageDown": - this.setSelection(bus.selection + this.tall(false) * 16); - break; - case "PageUp": - this.setSelection(bus.selection - this.tall(false) * 16); - break; - - // Hex digit: already processed - case "digit": break; - - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - // Mouse wheel - onMouseWheel(e) { - - // User agent scaling action - if (e.ctrlKey) - return; - - // No rotation has occurred - let offset = Math.sign(e.deltaY) * 48; - if (offset == 0) - return; - - // Update the display address - this.fetch(this[this.bus].address + offset, true); - } - - // Pointer down - onPointerDown(e) { - - // Common handling - this.editor.focus(); - e.stopPropagation(); - e.preventDefault(); - - // Not a click action - if (e.button != 0) - return; - - // Determine the row that was clicked on - let lineHeight = !this.metrics ? 0 : - Math.max(0, Math.ceil(this.metrics.getBounds().height)); - if (lineHeight == 0) - return; - let y = Math.floor( - (e.y - this.view.getBoundingClientRect().top) / lineHeight); - - // Determine the column that was clicked on - let columns = this.lines[0].lblBytes; - let bndCur = columns[0].getBoundingClientRect(); - if (e.x >= bndCur.left) for (let x = 0; x < 16; x++) { - let bndNext = x == 15 ? null : - columns[x + 1].getBoundingClientRect(); - - // The current column was clicked: update the selection - if (e.x < (x == 15 ? bndCur.right : - bndCur.right + (bndNext.left - bndCur.right) / 2)) { - this.setSelection(this[this.bus].address + y * 16 + x); - return; - } - - // Advance to the next column - bndCur = bndNext; - } - - } - - // Viewport resized - onResize(e) { - let fetch = false; - let tall = this.tall(true); - - // Add additional lines to the output - for (let x = 0; x < tall; x++) { - if (x >= this.lines.length) { - fetch = true; - this.lines.push(new Line(this, x)); - } - this.lines[x].setVisible(true); - } - - // Remove extra lines from the output - for (let x = tall; x < this.lines.length; x++) - this.lines[x].setVisible(false); - - // Configure horizontal scroll bar - if (this.metrics) this.editor.horizontal - .setIncrement(this.metrics.getBounds().width); - - // Update the display - if (fetch) - this.fetch(this[this.bus].address, true); - else this.refresh(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Update with memory state from the core - refresh(data) { - let bus = this[this.bus]; - - // Update with data from the core thread - if (data) { - bus.data = data.bytes; - bus.dataAddress = data.address; - } - - // Update elements - for (let y = 0, tall = this.tall(true); y < tall; y++) - this.lines[y].refresh(); - } - - // Subscribe to or unsubscribe from core updates - setSubscribed(subscribed) { - subscribed = !!subscribed; - - // Nothing to change - if (subscribed == this.isSubscribed) - return; - - // Configure instance fields - this.isSubscribed = subscribed; - - // Subscribe to core updates - if (subscribed) - this.fetch(this[this.bus].address); - - // Unsubscribe from core updates - else this.sim.unsubscribe("memory"); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // The disassembler configuration has changed - dasmChanged() { - this.refresh(); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Retrieve memory data from the core - async fetch(address, prefresh) { - - // Configure instance fields - this[this.bus].address = address = this.mask(address); - - // Update the view immediately - if (prefresh) - this.refresh(); - - // Retrieve data from the core - this.refresh( - await this.sim.read( - address - 16 * 16, - (this.tall(true) + 32) * 16, { - subscribe: this.isSubscribed && "memory" - }) - ); - } - - // Mask an address according to the current bus - mask(address) { - return Util.u32(address); - } - - // Prompt the user to specify a new address - promptGoto() { - - // Receive input from the user - let address = prompt(this.app.translate("common.gotoPrompt")); - if (address == null) - return; - - // Process the input as an address in hexadecimal - address = parseInt(address, 16); - if (isNaN(address)) - return; - - // The address is not currently visible in the output - let tall = this.tall(false); - if (Util.u32(address - this.address) >= tall * 16) - this.fetch((address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16); - - // Move the selection and refresh the display - this.setSelection(address); - } - - // Determine which row relative to top the selection is on - row(address) { - let row = address - this[this.bus].address & 0xFFFFFFF0; - row = Util.s32(row); - return row / 16; - } - - // Specify which byte is selected - setSelection(address, noCommit) { - let bus = this[this.bus]; - let fetch = false; - - // Commit a pending data entry - if (!noCommit && this.digit !== null) { - this.write(this.digit); - this.digit = null; - fetch = true; - } - - // Configure instance fields - bus.selection = address = this.mask(address); - - // Working variables - let row = this.row(address); - - // The new address is above the top line of output - if (row < 0) { - this.fetch(bus.address + row * 16 & 0xFFFFFFF0, true); - return; - } - - // The new address is below the bottom line of output - let tall = this.tall(false); - if (row >= tall) { - this.fetch(address - tall * 16 + 16 & 0xFFFFFFF0, true); - return; - } - - // Update the display - if (fetch) - this.fetch(bus.address, true); - else this.refresh(); - } - - // Measure how many rows of output are visible - tall(partial) { - let lineHeight = !this.metrics ? 0 : - Math.ceil(this.metrics.getBounds().height); - return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"]( - this.editor.getBounds().height / lineHeight)); - } - - // Write a value to the core thread - write(value) { - let bus = this[this.bus]; - let offset = (this.row(bus.selection) + 16) * 16; - if (offset < bus.data.length) - bus.data[offset | bus.selection & 15] = value; - this.sim.write( - bus.selection, - Uint8Array.from([ value ]), { - refresh: true - }); - } - -} - - - -export { Memory }; diff --git a/app/app/RegisterList.js b/app/app/RegisterList.js deleted file mode 100644 index 15b0e70..0000000 --- a/app/app/RegisterList.js +++ /dev/null @@ -1,889 +0,0 @@ -import { Util } from /**/"./Util.js"; - - - -// Value types -const HEX = 0; -const SIGNED = 1; -const UNSIGNED = 2; -const FLOAT = 3; - -// System register indexes -const ADTRE = 25; -const CHCW = 24; -const ECR = 4; -const EIPC = 0; -const EIPSW = 1; -const FEPC = 2; -const FEPSW = 3; -const PC = -1; -const PIR = 6; -const PSW = 5; -const TKCW = 7; - -// Program register names -const PROREGS = { - [ 2]: "hp", - [ 3]: "sp", - [ 4]: "gp", - [ 5]: "tp", - [31]: "lp" -}; - -// System register names -const SYSREGS = { - [ADTRE]: "ADTRE", - [CHCW ]: "CHCW", - [ECR ]: "ECR", - [EIPC ]: "EIPC", - [EIPSW]: "EIPSW", - [FEPC ]: "FEPC", - [FEPSW]: "FEPSW", - [PC ]: "PC", - [PIR ]: "PIR", - [PSW ]: "PSW", - [TKCW ]: "TKCW", - [29 ]: "29", - [30 ]: "30", - [31 ]: "31" -}; - -// Expansion control types -const BIT = 0; -const INT = 1; - -// Produce a template object for register expansion controls -function ctrl(name, shift, size, disabled) { - return { - disabled: !!disabled, - name : name, - shift : shift, - size : size - }; -} - -// Program register epansion controls -const EXP_PROGRAM = [ - ctrl("cpu.hex" , true , HEX ), - ctrl("cpu.signed" , false, SIGNED ), - ctrl("cpu.unsigned", false, UNSIGNED), - ctrl("cpu.float" , false, FLOAT ) -]; - -// CHCW expansion controls -const EXP_CHCW = [ - ctrl("ICE", 1, 1) -]; - -// ECR expansion controls -const EXP_ECR = [ - ctrl("FECC", 16, 16), - ctrl("EICC", 0, 16) -]; - -// PIR expansion controls -const EXP_PIR = [ - ctrl("PT", 0, 16, true) -]; - -// PSW expansion controls -const EXP_PSW = [ - ctrl("CY", 3, 1), ctrl("FRO", 9, 1), - ctrl("OV", 2, 1), ctrl("FIV", 8, 1), - ctrl("S" , 1, 1), ctrl("FZD", 7, 1), - ctrl("Z" , 0, 1), ctrl("FOV", 6, 1), - ctrl("NP", 15, 1), ctrl("FUD", 5, 1), - ctrl("EP", 14, 1), ctrl("FPR", 4, 1), - ctrl("ID", 12, 1), ctrl("I" , 16, 4), - ctrl("AE", 13, 1) -]; - -// TKCW expansion controls -const EXP_TKCW = [ - ctrl("FIT", 7, 1, true), ctrl("FUT", 4, 1, true), - ctrl("FZT", 6, 1, true), ctrl("FPT", 3, 1, true), - ctrl("FVT", 5, 1, true), ctrl("OTM", 8, 1, true), - ctrl("RDI", 2, 1, true), ctrl("RD" , 0, 2, true) -]; - - - -/////////////////////////////////////////////////////////////////////////////// -// Register // -/////////////////////////////////////////////////////////////////////////////// - -// One register within a register list -class Register { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(list, index, andMask, orMask) { - - // Configure instance fields - this.andMask = andMask; - this.app = list.app; - this.controls = []; - this.dasm = list.dasm; - this.format = HEX; - this.index = index; - this.isExpanded = null; - this.list = list; - this.metrics = { width: 0, height: 0 }; - this.orMask = orMask; - this.sim = list.sim; - this.system = list.system; - this.value = 0x00000000; - - // Establish elements - let row = document.createElement("tr"); - let cell; - list.view.append(row); - - // Processing by type - this[this.system ? "initSystem" : "initProgram"](); - - // Expansion button - this.btnExpand = new Toolkit.Component(this.app, { - className: "tk tk-expand tk-mono", - tagName : "div" - }); - row .append(cell = document.createElement("td")); - cell.className = "tk"; - cell.style.width = "1px"; - cell.append(this.btnExpand.element); - - // Name label - this.lblName = document.createElement("div"); - Object.assign(this.lblName, { - className: "tk tk-name", - id : Toolkit.id(), - innerText: this.dasm.sysregCaps?this.name:this.name.toLowerCase() - }); - this.lblName.style.userSelect = "none"; - row .append(cell = document.createElement("td")); - cell.className = "tk"; - cell.append(this.lblName); - - // Value text box - this.txtValue = new Toolkit.TextBox(this.app, { - className: "tk tk-textbox tk-mono", - maxLength: 8 - }); - this.txtValue.setAttribute("aria-labelledby", this.lblName.id); - this.txtValue.setAttribute("digits", "8"); - this.txtValue.addEventListener("action", e=>this.onValue()); - row .append(cell = document.createElement("td")); - Object.assign(cell.style, { - textAlign: "right", - width : "1px" - }); - cell.className = "tk"; - cell.append(this.txtValue.element); - - // Expansion area - if (this.expansion != null) - this.list.view.append(this.expansion); - - // Enable expansion function - if (this.expansion != null) { - let key = e=>this.expandKeyDown (e); - let pointer = e=>this.expandPointerDown(e); - this.btnExpand.setAttribute("aria-controls", this.expansion.id); - this.btnExpand.setAttribute("aria-labelledby", this.lblName.id); - this.btnExpand.setAttribute("role", "button"); - this.btnExpand.setAttribute("tabindex", "0"); - this.btnExpand.addEventListener("keydown" , key ); - this.btnExpand.addEventListener("pointerdown", pointer); - this.lblName .addEventListener("pointerdown", pointer); - this.setExpanded(this.system && this.index == PSW); - } - - // Expansion function is unavailable - else this.btnExpand.setAttribute("aria-hidden", "true"); - - } - - // Set up a program register - initProgram() { - this.name = PROREGS[this.index] || "r" + this.index.toString(); - this.initExpansion(EXP_PROGRAM); - } - - // Set up a system register - initSystem() { - this.name = SYSREGS[this.index] || this.index.toString(); - - switch (this.index) { - case CHCW : - this.initExpansion(EXP_CHCW); break; - case ECR : - this.initExpansion(EXP_ECR ); break; - case EIPSW: case FEPSW: case PSW: - this.initExpansion(EXP_PSW ); break; - case PIR : - this.initExpansion(EXP_PIR ); break; - case TKCW : - this.initExpansion(EXP_TKCW); break; - } - - } - - // Initialize expansion controls - initExpansion(controls) { - let two = this.index == ECR || this.index == PIR; - - // Establish expansion element - let exp = this.expansion = document.createElement("tr"); - exp.contents = new Toolkit.Component(this.app, { - className: "tk tk-expansion", - id : Toolkit.id(), - tagName : "div", - style : { - display : "grid", - gridTemplateColumns: - this.system ? "repeat(2, max-content)" : "max-content" - } - }); - let cell = document.createElement("td"); - cell.className = "tk"; - cell.colSpan = "3"; - cell.append(exp.contents.element); - exp.append(cell); - exp = exp.contents; - - // Produce program register controls - if (!this.system) { - let group = new Toolkit.Group(); - exp.append(group); - - // Process all controls - for (let template of controls) { - - // Create control - let ctrl = new Toolkit.Radio(this.app, { - group : group, - selected: template.shift, - text : template.name - }); - ctrl.format = template.size; - - // Configure event handler - ctrl.addEventListener("action", - e=>this.setFormat(e.component.format)); - - // Add the control to the element - let box = document.createElement("div"); - box.append(ctrl.element); - exp.append(box); - } - - return; - } - - // Process all control templates - for (let template of controls) { - let box, ctrl; - - // Not using an inner two-column layout - if (!two) - exp.append(box = document.createElement("div")); - - // Bit check box - if (template.size == 1) { - box.classList.add("tk-bit"); - - // Create control - ctrl = new Toolkit.CheckBox(this.app, { - text : "name", - substitutions: { name: template.name } - }); - ctrl.mask = 1 << template.shift; - box.append(ctrl.element); - - // Disable control - if (template.disabled) - ctrl.setEnabled(false); - - // Configure event handler - ctrl.addEventListener("action", e=>this.onBit(e.component)); - } - - // Number text box - else { - if (!two) - box.classList.add("tk-number"); - - // Create label - let label = document.createElement("label"); - Object.assign(label, { - className: "tk tk-label", - innerText: template.name, - }); - if (!two) Object.assign(box.style, { - columnGap : "2px", - display : "grid", - gridTemplateColumns: "max-content auto" - }); - (two ? exp : box).append(label); - - // Create control - ctrl = new Toolkit.TextBox(this.app, { - id : Toolkit.id(), - style: { height: "1em" } - }); - label.htmlFor = ctrl.id; - (two ? exp : box).append(ctrl.element); - - // Control is a hex field - if (template.size == 16) { - ctrl.element.classList.add("tk-mono"); - ctrl.setAttribute("digits", 4); - ctrl.setMaxLength(4); - } - - // Disable control - if (template.disabled) { - ctrl.setEnabled(false); - (two ? label : box).setAttribute("disabled", "true"); - } - - // Configure event handler - ctrl.addEventListener("action", e=>this.onNumber(e.component)); - } - - Object.assign(ctrl, template); - this.controls.push(ctrl); - } - - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Expand button key press - expandKeyDown(e) { - - // Processing by key - switch (e.key) { - case "Enter": - case " ": - this.setExpanded(!this.isExpanded); - break; - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - // Expand button pointer down - expandPointerDown(e) { - - // Focus management - this.btnExpand.focus(); - - // Error checking - if (e.button != 0) - return; - - // Configure event - e.stopPropagation(); - e.preventDefault(); - - // Configure expansion area - this.setExpanded(!this.isExpanded); - } - - // Expansion bit check box - onBit(ctrl) { - this.setValue(ctrl.isSelected ? - this.value | ctrl.mask : - this.value & Util.u32(~ctrl.mask) - ); - } - - // Expansion number text box - onNumber(ctrl) { - let mask = (1 << ctrl.size) - 1 << ctrl.shift; - let value = parseInt(ctrl.getText(), ctrl.size == 16 ? 16 : 10); - this.setValue(isNaN(value) ? this.value : - this.value & Util.u32(~mask) | value << ctrl.shift & mask); - } - - // Register value - onValue() { - let text = this.txtValue.getText(); - let value; - - // Processing by type - switch (this.format) { - - // Unsigned hexadecimal - case HEX: - value = parseInt(text, 16); - break; - - // Decimal - case SIGNED: - case UNSIGNED: - value = parseInt(text); - break; - - // Float - case FLOAT: - value = parseFloat(text); - if (isNaN(value)) - break; - value = Util.fromF32(value); - break; - } - - // Assign the new value - this.setValue(isNaN(value) ? this.value : value); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Disassembler settings have been updated - dasmChanged() { - let dasm = this.list.dasm; - let name = this.name; - - // Program register name - if (!this.system) { - if (!dasm.proregNames) - name = "r" + this.index.toString(); - if (dasm.proregCaps) - name = name.toUpperCase(); - } - - // System register name - else { - if (!dasm.sysregCaps) - name = name.toLowerCase(); - } - - // Common processing - this.lblName.innerText = name; - this.refresh(this.value); - } - - // Update the value returned from the core - refresh(value) { - let text; - - // Configure instance fields - this.value = value = Util.u32(value); - - // Value text box - switch (this.format) { - - // Unsigned hexadecimal - case HEX: - text = value.toString(16).padStart(8, "0"); - if (this.dasm.hexCaps) - text = text.toUpperCase(); - break; - - // Signed decimal - case SIGNED: - text = Util.s32(value).toString(); - break; - - // Unsigned decial - case UNSIGNED: - text = Util.u32(value).toString(); - break; - - // Float - case FLOAT: - if ((value & 0x7F800000) != 0x7F800000) { - text = Util.toF32(value).toFixed(5).replace(/0+$/, ""); - if (text.endsWith(".")) - text += "0"; - } else text = "NaN"; - break; - } - this.txtValue.setText(text); - - // No further processing for program registers - if (!this.system) - return; - - // Process all expansion controls - for (let ctrl of this.controls) { - - // Bit check box - if (ctrl.size == 1) { - ctrl.setSelected(value & ctrl.mask); - continue; - } - - // Integer text box - text = value >> ctrl.shift & (1 << ctrl.size) - 1; - text = ctrl.size != 16 ? text.toString() : - text.toString(16).padStart(4, "0"); - if (this.dasm.hexCaps) - text = text.toUpperCase(); - ctrl.setText(text); - } - - } - - // Specify whether the expansion area is visible - setExpanded(expanded) { - expanded = !!expanded; - - // Error checking - if (this.expansion == null || expanded === this.isExpanded) - return; - - // Configure instance fields - this.isExpanded = expanded; - - // Configure elements - let key = expanded ? "common.collapse" : "common.expand"; - this.btnExpand.setAttribute("aria-expanded", expanded); - this.btnExpand.setToolTip(key); - this.expansion.style.display = - expanded ? "table-row" : "none"; - } - - // Specify the font metrics - setMetrics(width, height) { - - // Configure instance fields - this.metrics = { width: width, height: height }; - - // Height - height += "px"; - this.txtValue.element.style.height = height; - for (let ctrl of this.controls.filter(c=>c.size > 1)) - ctrl.element.style.height = height; - - // Hexadecimal formatting - if (this.format == HEX) { - this.txtValue.element.style.width = (width * 8) + "px"; - this.txtValue.setMaxLength(8); - } - - // Decimal formatting - else { - this.txtValue.element.style.removeProperty("width"); - this.txtValue.setMaxLength(null); - } - - // Expansion text boxes - for (let box of this.controls.filter(c=>c.size > 1)) { - box.element.style.height = height; - if (box.size == 16) - box.element.style.width = (width * 4) + "px"; - } - - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Specify the formatting type of the register value - setFormat(format) { - if (format == this.format) - return; - this.format = format; - this.txtValue.element - .classList[format == HEX ? "add" : "remove"]("tk-mono"); - this.setMetrics(this.metrics.width, this.metrics.height); - this.refresh(this.value); - } - - // Specify a new value for the register - async setValue(value) { - - // Update the display with the new value immediately - value = Util.u32(value & this.andMask | this.orMask); - let matched = value == this.value; - this.refresh(value); - if (matched) - return; - - // Update the new value in the core - let options = { refresh: true }; - this.refresh(await ( - !this.system ? - this.sim.setProgramRegister(this.index, value, options) : - this.index == PC ? - this.sim.setProgramCounter ( value, options) : - this.sim.setSystemRegister (this.index, value, options) - )); - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// RegisterList // -/////////////////////////////////////////////////////////////////////////////// - -// Scrolling list of registers -class RegisterList extends Toolkit.ScrollPane { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(debug, system) { - super(debug.app, { - className: "tk tk-scrollpane tk-reglist " + - (system ? "tk-system" : "tk-program"), - vertical : Toolkit.ScrollPane.ALWAYS - }); - - // Configure instance fields - this.app = debug.app; - this.dasm = debug.disassembler; - this.method = system?"getSystemRegisters":"getProgramRegisters"; - this.registers = []; - this.sim = debug.sim; - this.subscription = system ? "sysregs" : "proregs"; - this.system = system; - - // Configure view element - this.setView(new Toolkit.Component(debug.app, { - className: "tk tk-list", - tagName : "table", - style : { - width: "100%" - } - })); - - // Font-measuring element - let text = ""; - for (let x = 0; x < 16; x++) { - if (x != 0) text += "\n"; - let digit = x.toString(16); - text += digit + "\n" + digit.toUpperCase(); - } - this.metrics = new Toolkit.Component(this.app, { - className: "tk tk-mono", - tagName : "div", - style : { - position : "absolute", - visibility: "hidden" - } - }); - this.metrics.element.innerText = text; - this.metrics.addEventListener("resize", e=>this.onMetrics()); - this.viewport.append(this.metrics.element); - - // Processing by type - this[system ? "initSystem" : "initProgram"](); - - // Configure component - this.addEventListener("keydown", e=>this.onKeyDown (e)); - this.addEventListener("wheel" , e=>this.onMouseWheel(e)); - } - - // Initialize a list of program registers - initProgram() { - this.add(new Register(this, 0, 0x00000000, 0x00000000)); - for (let x = 1; x < 32; x++) - this.add(new Register(this, x, 0xFFFFFFFF, 0x00000000)); - } - - // Initialie a list of system registers - initSystem() { - this.add(new Register(this, PC , 0xFFFFFFFE, 0x00000000)); - this.add(new Register(this, PSW , 0x000FF3FF, 0x00000000)); - this.add(new Register(this, ADTRE, 0xFFFFFFFE, 0x00000000)); - this.add(new Register(this, CHCW , 0x00000002, 0x00000000)); - this.add(new Register(this, ECR , 0xFFFFFFFF, 0x00000000)); - this.add(new Register(this, EIPC , 0xFFFFFFFE, 0x00000000)); - this.add(new Register(this, EIPSW, 0x000FF3FF, 0x00000000)); - this.add(new Register(this, FEPC , 0xFFFFFFFE, 0x00000000)); - this.add(new Register(this, FEPSW, 0x000FF3FF, 0x00000000)); - this.add(new Register(this, PIR , 0x00000000, 0x00005346)); - this.add(new Register(this, TKCW , 0x00000000, 0x000000E0)); - this.add(new Register(this, 29 , 0xFFFFFFFF, 0x00000000)); - this.add(new Register(this, 30 , 0x00000000, 0x00000004)); - this.add(new Register(this, 31 , 0xFFFFFFFF, 0x00000000)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Key press - onKeyDown(e) { - - // Processing by key - switch (e.key) { - case "ArrowDown": - this.vertical.setValue(this.vertical.value + - this.vertical.increment); - break; - case "ArrowLeft": - this.horizontal.setValue(this.horizontal.value - - this.horizontal.increment); - break; - case "ArrowRight": - this.horizontal.setValue(this.horizontal.value + - this.horizontal.increment); - break; - case "ArrowUp": - this.vertical.setValue(this.vertical.value - - this.vertical.increment); - break; - case "PageDown": - this.vertical.setValue(this.vertical.value + - this.vertical.extent); - break; - case "PageUp": - this.vertical.setValue(this.vertical.value - - this.vertical.extent); - break; - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - // Metrics element resized - onMetrics() { - - // Error checking - if (!this.metrics) - return; - - // Measure the dimensions of one hex character - let bounds = this.metrics.getBounds(); - if (bounds.height <= 0) - return; - let width = Math.ceil(bounds.width); - let height = Math.ceil(bounds.height / 32); - - // Resize all text boxes - for (let reg of this.registers) - reg.setMetrics(width, height); - - // Update scroll bars - this.horizontal.setIncrement(height); - this.vertical .setIncrement(height); - } - - // Mouse wheel - onMouseWheel(e) { - - // User agent scaling action - if (e.ctrlKey) - return; - - // No rotation has occurred - let offset = Math.sign(e.deltaY) * 3; - if (offset == 0) - return; - - // Update the display address - this.vertical.setValue(this.vertical.value + - this.vertical.increment * offset); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Update with CPU state from the core - refresh(registers) { - - // System registers - if (this.system) { - for (let reg of Object.entries(SYSREGS)) - this[reg[0]].refresh(registers[reg[1].toLowerCase()]); - } - - // Program registers - else for (let x = 0; x < 32; x++) - this[x].refresh(registers[x]); - } - - // Subscribe to or unsubscribe from core updates - setSubscribed(subscribed) { - subscribed = !!subscribed; - - // Nothing to change - if (subscribed == this.isSubscribed) - return; - - // Configure instance fields - this.isSubscribed = subscribed; - - // Subscribe to core updates - if (subscribed) - this.fetch(); - - // Unsubscribe from core updates - else this.sim.unsubscribe(this.subscription); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Disassembler settings have been updated - dasmChanged() { - for (let reg of this.registers) - reg.dasmChanged(); - } - - // Determine the initial size of the register list - getPreferredSize() { - let ret = { - height: 0, - width : 0 - }; - - // Error checking - if (!this.view) - return ret; - - // Measure the view element - ret.width = this.view.element.scrollWidth; - - // Locate the bottom of PSW - if (this.system && this[PSW].expansion) { - ret.height = this[PSW].expansion.getBoundingClientRect().bottom - - this.view.getBounds().top; - } - - return ret; - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Add a register to the list - add(reg) { - this[reg.index] = reg; - this.registers.push(reg); - } - - // Retrieve CPU state from the core - async fetch() { - this.refresh( - await this.sim[this.method]({ - subscribe: this.isSubscribed && this.subscription - }) - ); - } - -} - - - -export { RegisterList }; diff --git a/app/app/Util.js b/app/app/Util.js deleted file mode 100644 index 243ef03..0000000 --- a/app/app/Util.js +++ /dev/null @@ -1,44 +0,0 @@ -let F32 = new Float32Array( 1); -let S32 = new Int32Array (F32.buffer, 0, 1); -let U32 = new Uint32Array (F32.buffer, 0, 1); - -// Interpret a floating short as a 32-bit integer -function fromF32(x) { - F32[0] = x; - return S32[0]; -} - -// Interpret a 32-bit integer as a floating short -function toF32(x) { - S32[0] = x; - return F32[0]; -} - -// Represent a value as a signed 32-bit integer -function s32(x) { - S32[0] = x; - return S32[0]; -} - -// Sign-extend a value with a given number of bits -function signExtend(value, bits) { - bits = 32 - bits; - S32[0] = value << bits; - return S32[0] >> bits; -} - -// Represent a value as an unsigned 32-bit integer -function u32(x) { - U32[0] = x; - return U32[0]; -} - - - -export let Util = { - fromF32 : fromF32, - toF32 : toF32, - s32 : s32, - signExtend: signExtend, - u32 : u32 -}; diff --git a/app/core/Core.js b/app/core/Core.js deleted file mode 100644 index b56a341..0000000 --- a/app/core/Core.js +++ /dev/null @@ -1,196 +0,0 @@ -import { Sim } from /**/"./Sim.js"; - -let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString(); - -let RESTRICT = {}; -let WASM_URL = url(/**/"./core.wasm" ); -let WORKER_URL = url(/**/"./CoreWorker.js"); - - - -/////////////////////////////////////////////////////////////////////////////// -// Core // -/////////////////////////////////////////////////////////////////////////////// - -// Environment manager for simulated Virtual Boys -class Core { - - //////////////////////////////// Constants //////////////////////////////// - - // States - static IDLE = 0; - static RUNNING = 1; - - - - ///////////////////////////// Static Methods ////////////////////////////// - - // Create a new instance of Core - static create(options) { - return new Core(RESTRICT).init(options); - } - - - - ///////////////////////// Initialization Methods ////////////////////////// - - // Stub constructor - constructor(restrict) { - if (restrict != RESTRICT) { - throw "Cannot instantiate Core directly. " + - "Use Core.create() instead."; - } - } - - // Substitute constructor - async init(options = {}) { - - // Configure instance fields - this.length = 0; - this.onsubscriptions = null; - this.resolutions = []; - this.state = Core.IDLE; - this.worker = new Worker(WORKER_URL); - this.worker.onmessage = e=>this.onMessage(e.data); - - // Issue a create command - if ("sims" in options) - await this.create(options.sims, WASM_URL); - - // Only initialize the WebAssembly module - else this.send("init", false, { wasm: WASM_URL }); - - return this; - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Worker message received - onMessage(data) { - - // Process a promised response - if ("response" in data) - this.resolutions.shift()(data.response); - - // Process subscriptions - if (this.onsubscriptions && data.subscriptions) - this.onsubscriptions(data.subscriptions); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Associate two simulations as peers, or remove an association - connect(a, b, options = {}) { - return this.send({ - command: "connect", - respond: !("respond" in options) || !!options.respond, - sims : [ a, b ] - }); - } - - // Create and initialize new simulations - async create(sims, wasm) { - let numSims = sims===undefined ? 1 : Math.max(0, parseInt(sims) || 0); - - // Execute the command in the core thread - let response = await this.send({ - command: "create", - sims : numSims, - wasm : wasm - }); - - // Process the core thread's response - let ret = []; - for (let x = 0; x < numSims; x++, this.length++) - ret.push(this[this.length] = - new Sim(this, response[x], this.length)); - return sims === undefined ? ret[0] : ret; - } - - // Delete a simulation - destroy(sim, options = {}) { - - // Configure simulation - sim = this[sim] || sim; - if (sim.core != this) - return; - let ptr = sim.destroy(); - - // State management - for (let x = sim.index + 1; x < this.length; x++) - (this[x - 1] = this[x]).index--; - delete this[--this.length]; - - // Execute the command on the core thread - return this.send({ - command: "destroy", - respond: !("respond" in options) || !!options.respond, - sim : ptr - }); - } - - // Attempt to run until the next instruction - runNext(a, b, options = {}) { - return this.send({ - command: "runNext", - refresh: !!options.refresh, - respond: !("respond" in options) || !!options.respond, - sims : [ a, b ] - }); - } - - // Execute one instruction - singleStep(a, b, options = {}) { - return this.send({ - command: "singleStep", - refresh: !!options.refresh, - respond: !("respond" in options) || !!options.respond, - sims : [ a, b ] - }); - } - - // Unsubscribe from frame data - unsubscribe(key, sim = 0) { - this.send({ - command: "unsubscribe", - key : key, - respond: false, - sim : sim - }); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Send a message to the Worker - send(data = {}, transfers = []) { - - // Create the message object - Object.assign(data, { - respond: !("respond" in data) || !!data.respond, - run : !("run" in data) || !!data.run - }); - - // Do not wait on a response - if (!data.respond) - this.worker.postMessage(data, transfers); - - // Wait for the response to come back - else return new Promise((resolve, reject)=>{ - this.resolutions.push(response=>resolve(response)); - this.worker.postMessage(data, transfers); - }); - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// - -export { Core }; diff --git a/app/core/CoreWorker.js b/app/core/CoreWorker.js deleted file mode 100644 index 4bc8f9a..0000000 --- a/app/core/CoreWorker.js +++ /dev/null @@ -1,377 +0,0 @@ -"use strict"; - - - -// Un-sign a 32-bit integer -// Emscripten is sign-extending uint32_t and Firefox can't import in Workers -let u32 = (()=>{ - let U32 = new Uint32Array(1); - return x=>{ U32[0] = x; return U32[0]; }; -})(); - - - -/////////////////////////////////////////////////////////////////////////////// -// CoreWorker // -/////////////////////////////////////////////////////////////////////////////// - -// Thread manager for Core commands -new class CoreWorker { - - ///////////////////////// Initialization Methods ////////////////////////// - - // Stub constructor - constructor() { - onmessage = async e=>{ - await this.init(e.data.wasm); - onmessage = e=>this.onCommand(e.data, false); - onmessage(e); - }; - } - - // Substitute constructor - async init(wasm) { - - // Load the WebAssembly module - let imports = { - env: { emscripten_notify_memory_growth: ()=>this.onMemory() } - }; - this.wasm = await (typeof wasm == "string" ? - WebAssembly.instantiateStreaming(fetch(wasm), imports) : - WebAssembly.instantiate ( wasm , imports) - ); - - // Configure instance fields - this.api = this.wasm.instance.exports; - this.frameData = null; - this.isRunning = false; - this.memory = this.api.memory.buffer; - this.ptrSize = this.api.PointerSize(); - this.ptrType = this.ptrSize == 4 ? Uint32Array : Uint64Array; - this.subscriptions = {}; - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Message from audio thread - onAudio(frames) { - } - - // Message from main thread - onCommand(data) { - - // Subscribe to the command - if (data.subscribe) { - let sub = data.sim || 0; - sub = this.subscriptions[sub] || (this.subscriptions[sub] = {}); - sub = sub[data.subscribe] = {}; - Object.assign(sub, data); - delete sub.promised; - delete sub.run; - delete sub.subscribe; - } - - // Execute the command - if (data.run) - this[data.command](data); - - // Process all subscriptions to refresh any debugging interfaces - if (data.refresh) - this.doSubscriptions(data.sim ? [ data.sim ] : data.sims); - - // Reply to the main thread - if (data.respond) { - postMessage({ - response: data.response - }, data.transfers); - } - } - - // Memory growth - onMemory() { - this.memory = this.api.memory.buffer; - } - - - - //////////////////////////////// Commands ///////////////////////////////// - - // Associate two simulations as peers, or remove an association - connect(data) { - this.api.vbConnect(data.sims[0], data.sims[1]); - } - - // Allocate and initialize a new simulation - create(data) { - let ptr = this.api.Create(data.sims); - data.response = new this.ptrType(this.memory, ptr, data.sims).slice(); - data.transfers = [ data.response.buffer ]; - this.api.Free(ptr); - } - - // Delete a simulation - destroy(data) { - this.api.Destroy(data.sim); - } - - // Locate instructions for disassembly - disassemble(data) { - let decode; // Address of next row - let index; // Index in list of next row - let rows = new Array(data.rows); - let pc = u32(this.api.vbGetProgramCounter(data.sim)); - let row; // Located output row - - // The target address is before or on the first row of output - if (data.row <= 0) { - decode = u32(data.target - 4 * Math.max(0, data.row + 10)); - - // Locate the target row - for (;;) { - row = this.dasmRow(data.sim, decode, pc); - if (u32(data.target - decode) < row.size) - break; - decode = u32(decode + row.size); - } - - // Locate the first row of output - for (index = data.row; index < 0; index++) { - decode = u32(decode + row.size); - row = this.dasmRow(data.sim, decode, pc); - } - - // Prepare to process remaining rows - decode = u32(decode + row.size); - rows[0] = row; - index = 1; - } - - // The target address is after the first row of output - else { - let circle = new Array(data.row + 1); - let count = Math.min(data.row + 1, data.rows); - let src = 0; - decode = u32(data.target - 4 * (data.row + 10)); - - // Locate the target row - for (;;) { - row = circle[src] = this.dasmRow(data.sim, decode, pc); - decode = u32(decode + row.size); - if (u32(data.target - row.address) < row.size) - break; - src = (src + 1) % circle.length; - } - - // Copy entries from the circular buffer to the output list - for (index = 0; index < count; index++) { - src = (src + 1) % circle.length; - rows[index] = circle[src]; - } - - } - - // Locate any remaining rows - for (; index < data.rows; index++) { - let row = rows[index] = this.dasmRow(data.sim, decode, pc); - decode = u32(decode + row.size); - } - - // Respond to main thread - data.response = { - pc : pc, - rows : rows, - scroll: data.scroll - }; - } - - // Retrieve all CPU program registers - getProgramRegisters(data) { - let ret = data.response = new Uint32Array(32); - for (let x = 0; x < 32; x++) - ret[x] = this.api.vbGetProgramRegister(data.sim, x); - data.transfers = [ ret.buffer ]; - } - - // Retrieve the value of a system register - getSystemRegister(data) { - data.response = u32(this.api.vbGetSystemRegister(data.sim, data.id)); - } - - // Retrieve all CPU system registers (including PC) - getSystemRegisters(data) { - data.response = { - adtre: u32(this.api.vbGetSystemRegister(data.sim, 25)), - chcw : u32(this.api.vbGetSystemRegister(data.sim, 24)), - ecr : u32(this.api.vbGetSystemRegister(data.sim, 4)), - eipc : u32(this.api.vbGetSystemRegister(data.sim, 0)), - eipsw: u32(this.api.vbGetSystemRegister(data.sim, 1)), - fepc : u32(this.api.vbGetSystemRegister(data.sim, 2)), - fepsw: u32(this.api.vbGetSystemRegister(data.sim, 3)), - pc : u32(this.api.vbGetProgramCounter(data.sim )), - pir : u32(this.api.vbGetSystemRegister(data.sim, 6)), - psw : u32(this.api.vbGetSystemRegister(data.sim, 5)), - tkcw : u32(this.api.vbGetSystemRegister(data.sim, 7)), - [29] : u32(this.api.vbGetSystemRegister(data.sim, 29)), - [30] : u32(this.api.vbGetSystemRegister(data.sim, 30)), - [31] : u32(this.api.vbGetSystemRegister(data.sim, 31)) - }; - } - - // Read bytes from the simulation - read(data) { - let ptr = this.api.Malloc(data.length); - this.api.ReadBuffer(data.sim, ptr, data.address, data.length); - let buffer = new Uint8Array(this.memory, ptr, data.length).slice(); - this.api.Free(ptr); - data.response = { - address: data.address, - bytes : buffer - }; - data.transfers = [ buffer.buffer ]; - } - - // Attempt to execute until the following instruction - runNext(data) { - this.api.RunNext(data.sims[0], data.sims[1]); - let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ]; - if (data.sims[1]) - pc.push(u32(this.api.vbGetProgramCounter(data.sims[1]))); - data.response = { - pc: pc - } - } - - // Specify a new value for PC - setProgramCounter(data) { - data.response = - u32(this.api.vbSetProgramCounter(data.sim, data.value)); - } - - // Specify a new value for a program register - setProgramRegister(data) { - data.response = this.api.vbSetProgramRegister - (data.sim, data.id, data.value); - } - - // Specify a ROM buffer - setROM(data) { - let ptr = this.api.Malloc(data.rom.length); - let buffer = new Uint8Array(this.memory, ptr, data.rom.length); - for (let x = 0; x < data.rom.length; x++) - buffer[x] = data.rom[x]; - data.response = !!this.api.SetROM(data.sim, ptr, data.rom.length); - } - - // Specify a new value for a system register - setSystemRegister(data) { - data.response = u32(this.api.vbSetSystemRegister - (data.sim, data.id, data.value)); - } - - // Execute one instruction - singleStep(data) { - this.api.SingleStep(data.sims[0], data.sims[1]); - let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ]; - if (data.sims[1]) - pc.push(u32(this.api.vbGetProgramCounter(data.sims[1]))); - data.response = { - pc: pc - } - } - - // Unsubscribe from frame data - unsubscribe(data) { - let sim = data.sim || 0; - if (sim in this.subscriptions) { - let subs = this.subscriptions[sim]; - delete subs[data.key]; - if (Object.keys(subs).length == 0) - delete this.subscriptions[sim]; - } - } - - // Write bytes to the simulation - write(data) { - let ptr = this.api.Malloc(data.bytes.length); - let buffer = new Uint8Array(this.memory, ptr, data.bytes.length); - for (let x = 0; x < data.bytes.length; x++) - buffer[x] = data.bytes[x]; - this.api.WriteBuffer(data.sim, ptr, data.address, data.bytes.length); - this.api.Free(ptr); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Retrieve basic information for a row of disassembly - dasmRow(sim, address, pc) { - let bits = this.api.vbRead(sim, address, 3 /* VB_U16 */); - let opcode = bits >> 10 & 63; - let size = ( - opcode < 0b101000 || // Formats I through III - opcode == 0b110010 || // Illegal - opcode == 0b110110 // Illegal - ) ? 2 : 4; - - // Establish row information - let row = { - address: address, - bytes : [ bits & 0xFF, bits >> 8 ], - size : u32(address + 2) == pc ? 2 : size - }; - - // Read additional bytes - if (size == 4) { - bits = this.api.vbRead(sim, address + 2, 3 /* VB_U16 */); - row.bytes.push(bits & 0xFF, bits >> 8); - } - - return row; - } - - // Process subscriptions and send a message to the main thread - doSubscriptions(sims) { - let message = { subscriptions: {} }; - let transfers = []; - - // Process all simulations - for (let sim of sims) { - - // There are no subscriptions for this sim - if (!(sim in this.subscriptions)) - continue; - - // Working variables - let subs = message.subscriptions[sim] = {}; - - // Process all subscriptions - for (let sub of Object.entries(this.subscriptions[sim])) { - - // Run the command - this[sub[1].command](sub[1]); - - // Add the response to the message - if (sub[1].response) { - subs[sub[0]] = sub[1].response; - delete sub[1].response; - } - - // Add the transferable objects to the message - if (sub[1].transfers) { - transfers.push(... sub[1].transfers); - delete sub[1].transfers; - } - - } - - } - - // Send the message to the main thread - if (Object.keys(message).length != 0) - postMessage(message, transfers); - } - -}(); diff --git a/app/core/Sim.js b/app/core/Sim.js deleted file mode 100644 index f7d6928..0000000 --- a/app/core/Sim.js +++ /dev/null @@ -1,151 +0,0 @@ -/////////////////////////////////////////////////////////////////////////////// -// Sim // -/////////////////////////////////////////////////////////////////////////////// - -// One simulated Virtual Boy -class Sim { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(core, sim, index) { - this.core = core; - this.index = index; - this.sim = sim; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Locate CPU instructions - disassemble(target, row, rows, scroll, options = {}) { - return this.core.send({ - command : "disassemble", - row : row, - rows : rows, - scroll : scroll, - sim : this.sim, - subscribe: options.subscribe, - target : target - }); - } - - // Retrieve all CPU program registers - getProgramRegisters(options = {}) { - return this.core.send({ - command : "getProgramRegisters", - sim : this.sim, - subscribe: options.subscribe - }); - } - - // Retrieve the value of a system register - getSystemRegister(id, options = {}) { - return this.core.send({ - command : "getSystemRegister", - id : id, - sim : this.sim, - subscribe: options.subscribe - }); - } - - // Retrieve all CPU system registers (including PC) - getSystemRegisters(options = {}) { - return this.core.send({ - command : "getSystemRegisters", - sim : this.sim, - subscribe: options.subscribe - }); - } - - // Read multiple bytes from the bus - read(address, length, options = {}) { - return this.core.send({ - address : address, - command : "read", - debug : !("debug" in options) || !!options.debug, - length : length, - sim : this.sim, - subscribe: options.subscribe - }); - } - - // Specify a new value for PC - setProgramCounter(value, options = {}) { - return this.core.send({ - command: "setProgramCounter", - refresh: !!options.refresh, - sim : this.sim, - value : value - }); - } - - // Specify a new value for a program register - setProgramRegister(id, value, options = {}) { - return this.core.send({ - command: "setProgramRegister", - id : id, - refresh: !!options.refresh, - sim : this.sim, - value : value - }); - } - - // Specify the current ROM buffer - setROM(rom, options = {}) { - return this.core.send({ - command: "setROM", - rom : rom, - refresh: !!options.refresh, - sim : this.sim - }, [ rom.buffer ]); - } - - // Specify a new value for a system register - setSystemRegister(id, value, options = {}) { - return this.core.send({ - command: "setSystemRegister", - id : id, - refresh: !!options.refresh, - sim : this.sim, - value : value - }); - } - - // Ubsubscribe from frame data - unsubscribe(key) { - return this.core.unsubscribe(key, this.sim); - } - - // Write multiple bytes to the bus - write(address, bytes, options = {}) { - return this.core.send({ - address : address, - command : "write", - bytes : bytes, - debug : !("debug" in options) || !!options.debug, - refresh : !!options.refresh, - sim : this.sim, - subscribe: options.subscribe - }, - bytes instanceof ArrayBuffer ? [ bytes ] : - bytes.buffer instanceof ArrayBuffer ? [ bytes.buffer ] : - undefined - ); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // The simulation has been destroyed - destroy() { - let sim = this.sim; - this.core = null; - this.sim = 0; - return sim; - } - -} - -export { Sim }; diff --git a/app/locale/en-US.json b/app/locale/en-US.json deleted file mode 100644 index ab06e98..0000000 --- a/app/locale/en-US.json +++ /dev/null @@ -1,74 +0,0 @@ -{ - "id": "en-US", - - "common": { - "close" : "Close", - "collapse" : "Collapse", - "expand" : "Expand", - "gotoPrompt": "Enter the address to go to:" - }, - - "error": { - "fileRead": "An error occurred when reading the file.", - "romNotVB": "The selected file is not a Virtual Boy ROM." - }, - - "app": { - "title": "Virtual Boy Emulator", - - "menu": { - "_": "Application menu bar", - - "file": { - "_" : "File", - "loadROM" : "Load ROM{sim}...", - "debugMode": "Debug mode" - }, - - "emulation": { - "_" : "Emulation", - "run" : "Run", - "reset" : "Reset", - "dualSims": "Dual sims", - "linkSims": "Link sims" - }, - - "debug": { - "_" : "Debug{sim}", - "console" : "Console", - "memory" : "Memory", - "cpu" : "CPU", - "breakpoints" : "Breakpoints", - "palettes" : "Palettes", - "characters" : "Characters", - "bgMaps" : "BG maps", - "objects" : "Objects", - "worlds" : "Worlds", - "frameBuffers": "Frame buffers" - }, - - "theme": { - "_" : "Theme", - "light" : "Light", - "dark" : "Dark", - "virtual": "Virtual" - } - - } - - }, - - "cpu": { - "_" : "CPU{sim}", - "float" : "Float", - "hex" : "Hex", - "signed" : "Signed", - "splitHorizontal": "Program and system registers splitter", - "splitVertical" : "Disassembler and registers splitter", - "unsigned" : "Unsigned" - }, - - "memory": { - "_": "Memory{sim}" - } -} diff --git a/app/main.js b/app/main.js deleted file mode 100644 index 55ea892..0000000 --- a/app/main.js +++ /dev/null @@ -1,34 +0,0 @@ -// Global theme assets -Bundle["app/theme/kiosk.css"].installStylesheet(true); -await Bundle["app/theme/inconsolata.woff2"].installFont( - "Inconsolata SemiExpanded Medium"); -await Bundle["app/theme/roboto.woff2"].installFont("Roboto"); -Bundle["app/theme/check.svg" ].installImage("tk-check" , "check.svg" ); -Bundle["app/theme/close.svg" ].installImage("tk-close" , "close.svg" ); -Bundle["app/theme/collapse.svg"].installImage("tk-collapse", "collapse.svg"); -Bundle["app/theme/expand.svg" ].installImage("tk-expand" , "expand.svg" ); -Bundle["app/theme/radio.svg" ].installImage("tk-radio" , "radio.svg" ); -Bundle["app/theme/scroll.svg" ].installImage("tk-scroll" , "scroll.svg" ); - -// Module imports -import { Core } from /**/"./core/Core.js"; -import { Toolkit } from /**/"./toolkit/Toolkit.js"; -import { App } from /**/"./app/App.js"; - -// Begin application -let dark = matchMedia("(prefers-color-scheme: dark)").matches; -let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString(); -new App({ - core : await Core.create({ sims: 2 }), - locale : navigator.language, - standalone: true, - theme : dark ? "dark" : "light", - locales : [ - await (await fetch(url(/**/"./locale/en-US.json"))).json() - ], - themes : { - dark : Bundle["app/theme/dark.css" ].installStylesheet( dark), - light : Bundle["app/theme/light.css" ].installStylesheet(!dark), - virtual: Bundle["app/theme/virtual.css"].installStylesheet(false) - } -}); diff --git a/app/template.html b/app/template.html deleted file mode 100644 index 93fd934..0000000 --- a/app/template.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - Virtual Boy Emulator - - - - - - - diff --git a/app/theme/dark.css b/app/theme/dark.css deleted file mode 100644 index fd61751..0000000 --- a/app/theme/dark.css +++ /dev/null @@ -1,26 +0,0 @@ -:root { - --tk-control : #333333; - --tk-control-active : #555555; - --tk-control-border : #cccccc; - --tk-control-highlight : #444444; - --tk-control-shadow : #9b9b9b; - --tk-control-text : #cccccc; - --tk-desktop : #111111; - --tk-selected : #008542; - --tk-selected-blur : #325342; - --tk-selected-blur-text : #ffffff; - --tk-selected-text : #ffffff; - --tk-splitter-focus : #ffffff99; - --tk-window : #222222; - --tk-window-blur-close : #d9aeae; - --tk-window-blur-close-text: #eeeeee; - --tk-window-blur-title : #9fafb9; - --tk-window-blur-title2 : #c0b2ab; - --tk-window-blur-title-text: #444444; - --tk-window-close : #ee9999; - --tk-window-close-text : #ffffff; - --tk-window-text : #cccccc; - --tk-window-title : #80ccff; - --tk-window-title2 : #ffb894; - --tk-window-title-text : #000000; -} diff --git a/app/theme/kiosk.css b/app/theme/kiosk.css deleted file mode 100644 index e4de36e..0000000 --- a/app/theme/kiosk.css +++ /dev/null @@ -1,761 +0,0 @@ -:root { - --tk-font-dialog: "Roboto", sans-serif; - --tk-font-mono : "Inconsolata SemiExpanded Medium", monospace; - --tk-font-size : 12px; -} - -.tk { - font-family: var(--tk-font-dialog); - font-size : var(--tk-font-size); - line-height: 1em; - margin : 0; - outline : none; - padding : 0; -} - -table.tk { - border : none; - border-spacing: 0; -} - -.tk-body { - overflow: hidden; -} - -.tk-app { - /* Height managed through resize listener */ - width: 100%; -} - -.tk-mono { - font-family: var(--tk-font-mono); -} - - - -/********************************** Button ***********************************/ - -.tk-button > * { - background: var(--tk-control); - border : 1px solid var(--tk-control-shadow); - box-shadow: 1px 1px 0 0 var(--tk-control-shadow); - color : var(--tk-control-text); - margin : 0 1px 1px 0; - padding : 3px; - text-align: center; -} - -.tk-button:focus > * { - background: var(--tk-control-active); -} - -.tk-button.active > * { - box-shadow: none; - margin : 1px 0 0 1px; -} - -.tk-button[aria-disabled="true"] > * { - color: var(--tk-control-shadow); -} - - - -/********************************* Check Box *********************************/ - -.tk-checkbox { - align-items: center; - column-gap : 2px; -} - -.tk-checkbox .tk-icon { - align-items: center; - background : var(--tk-window); - border : 1px solid var(--tk-control-shadow); - box-sizing : border-box; - display : flex; - height : 12px; - width : 12px; -} - -.tk-checkbox .tk-icon:before { - background : transparent; - content : ""; - height : 100%; - display : block; - mask-image : var(--tk-check); - mask-position: center; - mask-repeat : no-repeat; - mask-size : contain; - width : 100%; - -webkit-mask-image : var(--tk-check); - -webkit-mask-position: center; - -webkit-mask-repeat : no-repeat; - -webkit-mask-size : contain; -} - -.tk-checkbox[aria-checked="true"] .tk-icon:before { - background: var(--tk-window-text); -} - -.tk-checkbox:focus .tk-icon { - background: var(--tk-control-active); -} - -.tk-checkbox[aria-checked="true"]:focus .tk-icon:before { - background: var(--tk-control-text); -} - -.tk-checkbox.active:focus .tk-icon:before { - background: var(--tk-control-shadow); -} - - - -/****************************** Drop-Down List *******************************/ - -.tk-dropdown { - background : var(--tk-window); - border : 1px solid var(--tk-control-shadow); - border-radius: 0; - color : var(--tk-window-text); - margin : 0; - padding : 1px; -} - - - -/********************************* Menu Bar **********************************/ - -.tk-menu-bar { - background : var(--tk-control); - border-bottom: 1px solid var(--tk-control-border); - color : var(--tk-control-text); - column-gap : 1px; - display : flex; - flex-wrap : wrap; - padding : 2px; - user-select : none; - white-space : nowrap; -} - -.tk-menu { - background : var(--tk-control); - border : 1px solid var(--tk-control-border); - box-shadow : 1px 1px 0 0 var(--tk-control-border); - display : flex; - flex-direction: column; - padding : 3px; - row-gap : 1px; -} - -.tk-menu-item > * { - background: var(--tk-control); - border : 1px solid transparent; - column-gap: 4px; - display : flex; - margin : 0 1px 1px 0; - padding : 3px; -} - -.tk-menu-item > * > .tk-icon { - box-sizing: border-box; - display : none; - height : 1em; - width : 1em; -} - -.tk-menu-item > * > .tk-text { - flex-grow: 1; -} - -.tk-menu-item[aria-disabled="true"] > * > .tk-text { - color: var(--tk-control-shadow); -} - -.tk-menu-item:not(.active, [aria-disabled="true"]):hover > *, -.tk-menu-item:not(.active):focus > * { - border-color: var(--tk-control-shadow); - box-shadow : 1px 1px 0 0 var(--tk-control-shadow); -} - -.tk-menu.icons > .tk-menu-item > * > .tk-icon { - display: block; -} - -.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon { - border: 1px solid var(--tk-control-border); -} - -.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon:before { - background : transparent; - content : ""; - height : 100%; - display : block; - mask-image : var(--tk-check); - mask-position: center; - mask-repeat : no-repeat; - mask-size : contain; - width : 100%; - -webkit-mask-image : var(--tk-check); - -webkit-mask-position: center; - -webkit-mask-repeat : no-repeat; - -webkit-mask-size : contain; -} - -.tk-menu-item[role="menuitemcheckbox"][aria-checked="true"] - > * > .tk-icon:before { - background: var(--tk-control-text); -} - -.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"] - > * > .tk-icon { - border: 1px solid var(--tk-control-shadow); -} - -.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"] - > * > .tk-icon:before { - background: var(--tk-control-shadow); -} - -.tk-menu-item:not(.active):focus > * { - background: var(--tk-control-active); -} - -.tk-menu-item.active > * { - background : var(--tk-control-active); - border-color: var(--tk-control-shadow); - margin : 1px 0 0 1px; -} - -.tk-menu-separator { - border : 0 solid var(--tk-control-shadow); - border-width : 1px 0 0 0; - height : 0; - margin-bottom: 1px; -} - - - -/*********************************** Radio ***********************************/ - -.tk-radio { - align-items: center; - column-gap : 2px; -} - -.tk-radio .tk-icon { - align-items : center; - background : var(--tk-window); - border : 1px solid var(--tk-control-shadow); - border-radius: 50%; - box-sizing : border-box; - display : flex; - height : 10px; - width : 10px; -} - -.tk-radio .tk-icon:before { - background : transparent; - content : ""; - height : 100%; - display : block; - mask-image : var(--tk-radio); - mask-position: center; - mask-repeat : no-repeat; - mask-size : contain; - width : 100%; - -webkit-mask-image : var(--tk-radio); - -webkit-mask-position: center; - -webkit-mask-repeat : no-repeat; - -webkit-mask-size : contain; -} - -.tk-radio[aria-checked="true"] .tk-icon:before { - background: var(--tk-window-text); -} - -.tk-radio:focus .tk-icon { - background: var(--tk-control-active); -} - -.tk-radio[aria-checked="true"]:focus .tk-icon:before { - background: var(--tk-control-text); -} - -.tk-radio.active[aria-checked="false"]:focus .tk-icon:before { - background: var(--tk-control-shadow); -} - - - -/******************************** Scroll Bar *********************************/ - -.tk-scrollbar { - background: var(--tk-control-highlight); - box-shadow: 0 0 0 1px var(--tk-control-shadow) inset; - box-sizing: border-box; -} - -.tk-scrollbar .tk-thumb, -.tk-scrollbar .tk-unit-down, -.tk-scrollbar .tk-unit-up { - background: var(--tk-control); - border : 1px solid var(--tk-control-border); - box-sizing: border-box; - color : var(--tk-control-text); -} - -.tk-scrollbar:focus .tk-thumb, -.tk-scrollbar:focus .tk-unit-down, -.tk-scrollbar:focus .tk-unit-up { - background: var(--tk-control-active); -} - -.tk-scrollbar .tk-unit-down, -.tk-scrollbar .tk-unit-up { - height: 13px; - width : 13px; -} - -.tk-scrollbar .tk-unit-down:before, -.tk-scrollbar .tk-unit-up:before { - background : currentColor; - content : ""; - display : block; - height : 100%; - mask-image : var(--tk-scroll); - mask-position: center; - mask-repeat : no-repeat; - mask-size : 100%; - width : 100%; - -webkit-mask-image : var(--tk-scroll); - -webkit-mask-position: center; - -webkit-mask-repeat : no-repeat; - -webkit-mask-size : 100%; -} - -.tk-scrollbar[aria-orientation="horizontal"] .tk-unit-down:before { - transform: rotate(-90deg); -} - -.tk-scrollbar[aria-orientation="horizontal"] .tk-unit-up:before { - transform: rotate(90deg); -} - -.tk-scrollbar[aria-orientation="vertical"] .tk-unit-down:before { -} - -.tk-scrollbar[aria-orientation="vertical"] .tk-unit-up:before { - transform: rotate(180deg); -} - -.tk-scrollbar .tk-unit-down.tk-active:before, -.tk-scrollbar .tk-unit-up.tk-active:before { - mask-size: calc(100% - 2px); - -webkit-mask-size: calc(100% - 2px); -} - -.tk-scrollbar[aria-disabled="true"] .tk-unit-down, -.tk-scrollbar[aria-disabled="true"] .tk-unit-up, -.tk-scrollbar.tk-full .tk-unit-down, -.tk-scrollbar.tk-full .tk-unit-up { - background: var(--tk-control); - border-color: var(--tk-control-shadow); - color : var(--tk-control-shadow); -} - -.tk-scrollbar .tk-block-down, -.tk-scrollbar .tk-block-up { - background : var(--tk-control-highlight); - border-color: var(--tk-control-shadow); - border-style: solid; - border-width: 0 1px; -} - -.tk-scrollbar[aria-orientation="horizontal"] .tk-block-down, -.tk-scrollbar[aria-orientation="horizontal"] .tk-block-up { - border-width: 1px 0; -} - -.tk-scrollbar .tk-block-down.tk-active, -.tk-scrollbar .tk-block-up.tk-active { - background: var(--tk-control-shadow); -} - -.tk-scrollbar[aria-disabled="true"] .tk-thumb, -.tk-scrollbar[aria-disabled="true"] .tk-block-down, -.tk-scrollbar[aria-disabled="true"] .tk-block-up, -.tk-scrollbar.tk-full .tk-thumb, -.tk-scrollbar.tk-full .tk-block-down, -.tk-scrollbar.tk-full .tk-block-up { - visibility: hidden; -} - - - -/******************************** Scroll Pane ********************************/ - -.tk-scrollpane { - background: var(--tk-control); -} - -.tk-scrollpane > .tk-scrollbar { - border: 0 solid var(--tk-control); -} - -.tk-scrollpane > .tk-scrollbar[aria-orientation="horizontal"] { - border-width: 1px 0 0 0; -} - -.tk-scrollpane > .tk-scrollbar[aria-orientation="vertical"] { - border-width: 0 0 0 1px; -} - - - -/******************************** Split Pane *********************************/ - -.tk-splitpane > [role="separator"][aria-orientation="horizontal"] { - cursor: ns-resize; - height: 3px; -} - -.tk-splitpane > [role="separator"][aria-orientation="vertical"] { - cursor: ew-resize; - width : 3px; -} - -.tk-splitpane > [role="separator"]:focus { - background: var(--tk-splitter-focus); - z-index : 1; -} - - - -/********************************* Text Box **********************************/ - -.tk-textbox { - background: var(--tk-window); - border : 1px solid var(--tk-control-shadow); - color : var(--tk-window-text); - margin : 0; - padding : 2px; -} - - - -/********************************** Windows **********************************/ - -.tk-desktop { - background: var(--tk-desktop); -} - -.tk-window > * { - border : 1px solid var(--tk-control-border); - box-shadow: 1px 1px 0 0 var(--tk-control-border); - margin : 0 1px 1px 0; -} - -.tk-window > * > .tk-nw {left : -1px; top : -1px; height: 8px; width : 8px; } -.tk-window > * > .tk-n {left : 7px; top : -1px; right : 8px; height: 3px; } -.tk-window > * > .tk-ne {right: 0px; top : -1px; height: 8px; width : 8px; } -.tk-window > * > .tk-w {left : -1px; top : 7px; width : 3px; bottom: 8px; } -.tk-window > * > .tk-e {right: 0px; top : 7px; width : 3px; bottom: 8px; } -.tk-window > * > .tk-sw {left : -1px; bottom: 0px; height: 8px; width : 8px; } -.tk-window > * > .tk-s {left : 7px; bottom: 0px; right : 8px; height: 3px; } -.tk-window > * > .tk-se {right: 0px; bottom: 0px; height: 8px; width : 8px; } - -.tk-window > * > .tk-title { - align-items : center; - background : var(--tk-window-blur-title); - border-bottom: 1px solid var(--tk-control-shadow); - box-sizing : border-box; - color : var(--tk-window-blur-title-text); - overflow : hidden; - padding : 1px; - position : relative; -} - -.tk-window.two > * > .tk-title { - background: var(--tk-window-blur-title2); -} - -.tk-window > * > .tk-title .tk-text { - cursor : default; - flex-basis : 0; - font-weight : bold; - min-width : 0; - overflow : hidden; - padding : 1px 1px 1px calc(1em + 3px); - text-align : center; - text-overflow: ellipsis; - user-select : none; - white-space : nowrap; -} - -.tk-window > * > .tk-title .tk-close { - background: var(--tk-window-blur-close); - border : 1px solid var(--tk-control-shadow); - color : var(--tk-window-blur-close-text); - height : calc(1em - 1px); - margin : 1px 1px 1px 0; - overflow : none; - width : calc(1em - 1px); -} - -.tk-window > * > .tk-title .tk-close:before { - background : currentColor; - content : ""; - display : block; - height : 100%; - width : 100%; - mask-image : var(--tk-close); - mask-position: center; - mask-repeat : no-repeat; - mask-size : 100%; - -webkit-mask-image : var(--tk-close); - -webkit-mask-position: center; - -webkit-mask-repeat : no-repeat; - -webkit-mask-size : 100%; -} - -.tk-window > * > .tk-title .tk-close.active:before { - mask-size: calc(100% - 2px); - -webkit-mask-size: calc(100% - 2px); -} - -.tk-window:focus-within > * > .tk-title { - background: var(--tk-window-title); - color : var(--tk-window-title-text); -} - -.tk-window.two:focus-within > * > .tk-title { - background: var(--tk-window-title2); -} - -.tk-window:focus-within > * > .tk-title .tk-close { - background: var(--tk-window-close); - color : var(--tk-window-close-text); -} - -.tk-window > * > .tk-client { - background: var(--tk-control); -} - - - -/************************************ CPU ************************************/ - -.tk-cpu .tk-main { - height: 100%; - width : 100%; -} - -.tk-cpu .tk-main > .tk-a, -.tk-cpu .tk-registers > .tk-a, -.tk-cpu .tk-registers > .tk-b { - box-shadow: 0 0 0 1px var(--tk-control),0 0 0 2px var(--tk-control-shadow); -} - -.tk-cpu .tk-main > .tk-a { margin : 3px; } -.tk-cpu .tk-main > [role="separator"] { margin : 1px -2px; } -.tk-cpu .tk-main > .tk-b { margin : 3px; } -.tk-cpu .tk-registers > .tk-a { margin-bottom: 3px; } -.tk-cpu .tk-registers > [role="separator"] { margin : -2px; } -.tk-cpu .tk-registers > .tk-b { margin-top : 3px; } - - - -.tk-disassembler .tk-viewport { - background: var(--tk-window); - color : var(--tk-window-text); -} - -.tk-disassembler .tk-view { - height: 100%; -} - -.tk-disassembler .tk-metrics { - padding-bottom: 1px; -} - -.tk-disassembler .tk { - cursor : default; - font-family: var(--tk-font-mono); - user-select: none; - white-space: nowrap; -} - -.tk-disassembler .tk-bytes, -.tk-disassembler .tk-mnemonic, -.tk-disassembler .tk-operands { - padding: 0 0 1px calc(1.2em - 1px); -} - -.tk-disassembler .tk-address { - padding-left: 1px; -} - -.tk-disassembler .tk-operands { - padding-right: 1px; -} - -.tk-disassembler .tk-selected { - background: var(--tk-selected-blur); - color : var(--tk-selected-blur-text); -} - -.tk-disassembler:focus-within .tk-selected { - background: var(--tk-selected); - color : var(--tk-selected-text); -} - - - -.tk-reglist .tk-viewport { - background: var(--tk-window); - color : var(--tk-window-text); -} - -.tk-reglist .tk-list { - align-items: center; -} - -.tk-reglist .tk-expand { - align-items : center; - border-radius : 2px; - display : flex; - height : 11px; - justify-content: center; - width : 11px; -} - -.tk-reglist .tk-expand:before { - content : ""; - height : 100%; - display : block; - mask-position: center; - mask-repeat : no-repeat; - mask-size : contain; - width : 100%; - -webkit-mask-position: center; - -webkit-mask-repeat : no-repeat; - -webkit-mask-size : contain; -} - -.tk-reglist .tk-expand:focus { - background: var(--tk-control-active); -} - -.tk-reglist .tk-expand[aria-expanded]:before { - background: var(--tk-window-text); -} -.tk-reglist .tk-expand[aria-expanded]:focus:before { - background: var(--tk-control-text); -} - -.tk-reglist .tk-expand[aria-expanded="false"]:before { - mask-image: var(--tk-expand); - -webkit-mask-image: var(--tk-expand); -} - -.tk-reglist .tk-expand[aria-expanded="true"]:before { - mask-image: var(--tk-collapse); - -webkit-mask-image: var(--tk-collapse); -} - -.tk-reglist .tk-name { - padding: 0 0.5em 0 1px; -} - -.tk-reglist .tk-textbox { - background: transparent; - border : none; - padding : 0; - width : 1.5em; -} - -.tk-reglist.tk-program .tk-textbox:not(.tk-mono) { - text-align: right; - width : 6em; -} - -.tk-reglist .tk-expansion { - align-items : center; - column-gap : 0.8em; - margin-bottom: 2px; - padding : 2px 0 0 1.5em; -} - -.tk-reglist .tk-expansion .tk-number .tk-label { - align-items : center; - display : flex; - justify-content: center; - min-width : 12px; -} - -.tk-reglist .tk-expansion .tk-checkbox[aria-disabled="true"][aria-checked="true"] - .tk-icon:before { - background: var(--tk-control-shadow); -} - -.tk-reglist .tk-expansion .tk-checkbox[aria-disabled="true"] .tk-contents, -.tk-reglist .tk-expansion .tk-number[disabled] *, -.tk-reglist .tk-expansion .tk-label[disabled], -.tk-reglist .tk-expansion .tk-textbox[disabled] { - color: var(--tk-control-shadow); -} - - - -/********************************** Memory ***********************************/ - -.tk-window .tk-memory { - height: 100%; - width : 100%; -} - -.tk-memory .tk-editor { - box-shadow: 0 0 0 1px var(--tk-control),0 0 0 2px var(--tk-control-shadow); - height : calc(100% - 6px); - margin : 3px; - width : calc(100% - 6px); -} - -.tk-memory .tk-viewport { - background: var(--tk-window); - color : var(--tk-window-text); -} - -.tk-memory .tk-metrics, -.tk-memory .tk-view * { - padding-bottom: 1px; - cursor : default; - font-family : var(--tk-font-mono); - user-select : none; -} - -.tk-memory .tk-byte { - border : 0 solid transparent; - padding : 0 1px; - text-align: center; -} - -.tk-memory .tk-byte:not(.tk-15) { - margin-right: calc(0.6em - 1px); -} - -.tk-memory .tk-address, -.tk-memory .tk-byte.tk-7 { - margin-right: calc(1.2em - 1px); -} - -.tk-memory .tk-byte.tk-selected { - background: var(--tk-selected-blur); - color : var(--tk-selected-blur-text); -} - -.tk-memory .tk-editor:focus-within .tk-byte.tk-selected { - background: var(--tk-selected); - color : var(--tk-selected-text); -} diff --git a/app/theme/light.css b/app/theme/light.css deleted file mode 100644 index 3515cb2..0000000 --- a/app/theme/light.css +++ /dev/null @@ -1,26 +0,0 @@ -:root { - --tk-control : #eeeeee; - --tk-control-active : #cccccc; - --tk-control-border : #000000; - --tk-control-highlight : #f8f8f8; - --tk-control-shadow : #6c6c6c; - --tk-control-text : #000000; - --tk-desktop : #cccccc; - --tk-selected : #008542; - --tk-selected-blur : #325342; - --tk-selected-blur-text : #ffffff; - --tk-selected-text : #ffffff; - --tk-splitter-focus : #00000080; - --tk-window : #ffffff; - --tk-window-blur-close : #d9aeae; - --tk-window-blur-close-text: #eeeeee; - --tk-window-blur-title : #aac4d5; - --tk-window-blur-title2 : #dbc4b8; - --tk-window-blur-title-text: #444444; - --tk-window-close : #ee9999; - --tk-window-close-text : #ffffff; - --tk-window-text : #000000; - --tk-window-title : #80ccff; - --tk-window-title2 : #ffb894; - --tk-window-title-text : #000000; -} diff --git a/app/theme/virtual.css b/app/theme/virtual.css deleted file mode 100644 index 98f675f..0000000 --- a/app/theme/virtual.css +++ /dev/null @@ -1,70 +0,0 @@ -:root { - --tk-control : #000000; - --tk-control-active : #550000; - --tk-control-border : #ff0000; - --tk-control-highlight : #550000; - --tk-control-shadow : #aa0000; - --tk-control-text : #ff0000; - --tk-desktop : #000000; - --tk-selected : #550000; - --tk-selected-blur : #550000; - --tk-selected-blur-text : #ff0000; - --tk-selected-text : #ff0000; - --tk-splitter-focus : #ff000099; - --tk-window : #000000; - --tk-window-blur-close : #000000; - --tk-window-blur-close-text: #aa0000; - --tk-window-blur-title : #000000; - --tk-window-blur-title2 : #000000; - --tk-window-blur-title-text: #aa0000; - --tk-window-close : #550000; - --tk-window-close-text : #ff0000; - --tk-window-text : #ff0000; - --tk-window-title : #550000; - --tk-window-title2 : #550000; - --tk-window-title-text : #ff0000; -} - -input { - filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v"); -} - - - -/******************************** Scroll Bar *********************************/ - -.tk-scrollbar .tk-thumb, -.tk-scrollbar .tk-unit-down, -.tk-scrollbar .tk-unit-up { - background : #aa0000; - border-color: #550000; - color : #000000; -} - -.tk-scrollbar:focus .tk-thumb, -.tk-scrollbar:focus .tk-unit-down, -.tk-scrollbar:focus .tk-unit-up { - background : #ff0000; - border-color: #aa0000; -} - -.tk-scrollbar[aria-disabled="true"] .tk-thumb, -.tk-scrollbar[aria-disabled="true"] .tk-unit-down, -.tk-scrollbar[aria-disabled="true"] .tk-unit-up, -.tk-scrollbar.tk-full .tk-thumb, -.tk-scrollbar.tk-full .tk-unit-down, -.tk-scrollbar.tk-full .tk-unit-up { - background : #550000; - border-color: #aa0000; - color : #aa0000; -} - -.tk-scrollbar .tk-block-down, -.tk-scrollbar .tk-block-up { - background : #550000; - border-color: #aa0000; -} - -.tk-window > * > .tk-client > .tk-memory { - box-shadow: 0 0 0 1px #000000, 0 0 0 2px #ff0000; -} diff --git a/app/toolkit/Button.js b/app/toolkit/Button.js deleted file mode 100644 index 6f0f6f6..0000000 --- a/app/toolkit/Button.js +++ /dev/null @@ -1,387 +0,0 @@ -import { Component } from /**/"./Component.js"; -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// Button // -/////////////////////////////////////////////////////////////////////////////// - -// Push, toggle or radio button -class Button extends Component { - static Component = Component; - - //////////////////////////////// Constants //////////////////////////////// - - // Types - static BUTTON = 0; - static RADIO = 1; - static TOGGLE = 2; - - - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-button", - focusable: true, - role : "button", - tagName : "div", - style : { - display : "inline-block", - userSelect: "none" - } - }); - - // Configure instance fields - options = options || {}; - this.attribute = options.attribute || "aria-pressed"; - this.group = null; - this.isEnabled = null; - this.isSelected = false; - this.text = null; - this.type = Button.BUTTON; - - // Configure contents - this.contents = document.createElement("div"); - this.append(this.contents); - - // Configure component - this.setEnabled(!("enabled" in options) || options.enabled); - if ("group" in options) - options.group.add(this); - this.setText (options.text); - this.setType (options.type); - if ("selected" in options) - this.setSelected(options.selected); - - // Configure event handlers - this.addEventListener("keydown" , e=>this.onKeyDown (e)); - this.addEventListener("pointerdown", e=>this.onPointerDown(e)); - this.addEventListener("pointermove", e=>this.onPointerMove(e)); - this.addEventListener("pointerup" , e=>this.onPointerUp (e)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Key press - onKeyDown(e) { - - // Processing by key - switch (e.key) { - case "Enter": // Fallthrough - case " " : - this.click(); - break; - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - // Pointer down - onPointerDown(e) { - this.focus(); - - // Error checking - if ( - !this.isEnabled || - this.element.hasPointerCapture(e.pointerId) || - e.button != 0 - ) return; - - // Configure event - this.element.setPointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure component - this.element.classList.add("active"); - } - - // Pointer move - onPointerMove(e) { - - // Error checking - if (!this.element.hasPointerCapture(e.pointerId)) - return; - - // Configure event - e.stopPropagation(); - e.preventDefault(); - - // Configure component - this.element.classList[ - Toolkit.isInside(this.element, e) ? "add" : "remove"]("active"); - } - - // Pointer up - onPointerUp(e) { - - // Error checking - if ( - !this.isEnabled || - e.button != 0 || - !this.element.hasPointerCapture(e.pointerId) - ) return; - - // Configure event - this.element.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure component - this.element.classList.remove("active"); - - // Item is an action - let bounds = this.getBounds(); - if (this.menu == null && Toolkit.isInside(this.element, e)) - this.click(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Programmatically activate the button - click() { - if (this instanceof Toolkit.CheckBox) - this.setSelected(this instanceof Toolkit.Radio||!this.isSelected); - this.event("action"); - } - - // Specify whether the button can be activated - setEnabled(enabled) { - this.isEnabled = enabled = !!enabled; - this.setAttribute("aria-disabled", enabled ? null : "true"); - } - - // Specify whether the toggle or radio button is selected - setSelected(selected) { - selected = !!selected; - - // Take no action - if (selected == this.isSelected) - return; - - // Processing by button type - switch (this.type) { - case Button.RADIO : - if (selected && this.group != null) - this.group.deselect(); - // Fallthrough - case Button.TOGGLE: - this.isSelected = selected; - this.setAttribute(this.attribute, selected); - } - - } - - // Specify the widget's display text - setText(text) { - this.text = (text || "").toString().trim(); - this.translate(); - } - - // Specify what kind of button this is - setType(type) { - switch (type) { - case Button.BUTTON: - this.type = type; - this.setAttribute(this.attribute, null); - this.setSelected(false); - break; - case Button.RADIO : // Fallthrough - case Button.TOGGLE: - this.type = type; - this.setAttribute(this.attribute, this.isSelected); - this.setSelected(this.isSelected); - break; - default: return; - } - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - - // Regenerate localized display text - translate() { - super.translate(); - if (this.contents != null) - this.contents.innerText = this.gui.translate(this.text, this); - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// CheckBox // -/////////////////////////////////////////////////////////////////////////////// - -// On/off toggle box -class CheckBox extends Button { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(app, options) { - - // Default options override - let uptions = {}; - Object.assign(uptions, options || {}); - for (let entry of Object.entries({ - attribute: "aria-checked", - className: "tk tk-checkbox", - role : "checkbox", - style : {}, - type : Button.TOGGLE - })) if (!(entry[0] in uptions)) - uptions[entry[0]] = entry[1]; - - // Default styles override - for (let entry of Object.entries({ - display : "inline-grid", - gridTemplateColumns: "max-content auto" - })) if (!(entry[0] in uptions.style)) - uptions.style[entry[0]] = entry[1]; - - // Component overrides - super(app, uptions); - this.contents.classList.add("tk-contents"); - - // Configure icon - this.icon = document.createElement("div"); - this.icon.className = "tk tk-icon"; - this.icon.setAttribute("aria-hidden", "true"); - this.prepend(this.icon); - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// Radio // -/////////////////////////////////////////////////////////////////////////////// - -// Single selection box -class Radio extends CheckBox { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(app, options) { - - // Default options override - let uptions = {}; - Object.assign(uptions, options || {}); - for (let entry of Object.entries({ - className: "tk tk-radio", - role : "radio", - type : Button.RADIO - })) if (!(entry[0] in uptions)) - uptions[entry[0]] = entry[1]; - - // Component overrides - super(app, uptions); - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// Group // -/////////////////////////////////////////////////////////////////////////////// - -// Radio button or menu item group -class Group extends Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(app) { - super(app, { - tagName: "div", - style : { - height : "0", - position: "absolute", - width : "0" - } - }); - - // Configure instance fields - this.items = []; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add an item - add(item) { - - // Error checking - if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1) - return item; - - // Configure component - this.setAttribute("role", - item instanceof Toolkit.Radio ? "radiogroup" : "group"); - - // Configure item - if (item.group != null) - item.group.remove(item); - item.group = this; - - // Add the item to the collection - item.id = item.id || Toolkit.id(); - this.items.push(item); - this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" ")); - - return item; - } - - // Remove all items - clear() { - this.items.splice(); - this.setAttribute("aria-owns", ""); - } - - // Un-check all items in the group - deselect() { - for (let item of this.items) - if (item.isSelected && "setSelected" in item) - item.setSelected(false); - } - - // Remove an item - remove(item) { - - // Error checking - let index = this.items.indexOf(item); - if (index == -1) - return; - - // Remove the item from the collection - this.items.splice(index, 1); - this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" ")); - item.group = null; - - return item; - } - -} - - - -export { Button, CheckBox, Group, Radio }; diff --git a/app/toolkit/Component.js b/app/toolkit/Component.js deleted file mode 100644 index 9d00b30..0000000 --- a/app/toolkit/Component.js +++ /dev/null @@ -1,312 +0,0 @@ -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// Component // -/////////////////////////////////////////////////////////////////////////////// - -// Abstract class representing a distinct UI element -class Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options, defaults) { - - // Configure instance fields - this.children = []; - this.gui = gui || this; - this.label = null; - this.resizeObserver = null; - this.substitutions = {}; - this.toolTip = null; - - // Configure default options - let uptions = options || {}; - options = {}; - Object.assign(options, uptions); - options.style = options.style || {}; - defaults = defaults || {}; - defaults.style = defaults.style || {}; - for (let key of Object.keys(defaults)) - if (!(key in options)) - options[key] = defaults[key]; - for (let key of Object.keys(defaults.style)) - if (!(key in options.style)) - options.style[key] = defaults.style[key]; - this.visibility = !!options.visibility; - - // Configure element - this.element = document.createElement( - ("tagName" in options ? options.tagName : null) || "div"); - if (Object.keys(options.style).length != 0) - Object.assign(this.element.style, options.style); - if ("className" in options && options.className) - this.element.className = options.className; - if ("focusable" in options) - this.setFocusable(options.focusable, options.tabStop); - if ("id" in options) - this.setId(options.id); - if ("role" in options && options.role ) - this.element.setAttribute("role", options.role); - if ("visible" in options) - this.setVisible(options.visible); - - // Configure component - this.setAttribute("name", options.name || ""); - this.setLabel (options.label || ""); - this.setToolTip (options.toolTip || ""); - - // Configure substitutions - if ("substitutions" in options) { - for (let sub of Object.entries(options.substitutions)) - this.setSubstitution(sub[0], sub[1], true); - this.translate(); - } - - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a child component - add(component) { - - // The component is already a child of this component - let index = this.children.indexOf(component); - if (index != -1) - return index; - - // The component has a different parent already - if (component.parent != null) - component.parent.remove(component); - - // Add the child component to this component - component.parent = this; - this.children.push(component); - if ("addHook" in this) - this.addHook(component); - else this.append(component); - if ("addedHook" in component) - component.addedHook(this); - return this.children.length - 1; - } - - // Listen for events - addEventListener(type, listener, useCapture) { - let callback = e=>{ - e.component = this; - return listener(e); - }; - - // Register the listener for the event type - this.element.addEventListener(type, callback, useCapture); - - // Listen for resize events on the element - if (type == "resize" && this.resizeObserver == null) { - this.resizeObserver = new ResizeObserver( - ()=>this.event("resize")); - this.resizeObserver.observe(this.element); - } - - return callback; - } - - // Add a DOM element as a sibling after this component - after(child) { - let element = child instanceof Element ? child : child.element; - this.element.after(element); - } - - // Add a DOM element to the end of this component's children - append(child) { - let element = child instanceof Element ? child : child.element; - this.element.append(element); - } - - // Add a DOM element as a sibling before this component - before(child) { - let element = child instanceof Element ? child : child.element; - this.element.before(element); - } - - // Request non-focus on this component - blur() { - this.element.blur(); - } - - // Determine whether this component contains another or an element - contains(child) { - - // Child is an element - if (child instanceof Element) - return this.element.contains(child); - - // Child is a component - for (let component = child; component; component = component.parent) - if (component == this) - return true; - return false; - } - - // Request focus on the component - focus() { - this.element.focus(); - } - - // Retrieve the current DOM position of the element - getBounds() { - return this.element.getBoundingClientRect(); - } - - // Determine whether this component currently has focus - hasFocus() { - return document.activeElement == this.element; - } - - // Determine whether the component is visible - isVisible() { - - // Common visibility test - if ( - !document.contains(this.element) || - this.parent && !this.parent.isVisible() - ) return false; - - // Overridden visibility test - if ("visibleHook" in this) { - if (!this.visibleHook()) - return false; - } - - // Default visibility test - else { - let style = getComputedStyle(this.element); - if (style.display == "none" || style.visibility == "hidden") - return false; - } - - return true; - } - - // Add a DOM element to the beginning of this component's children - prepend(child) { - let element = child instanceof Element ? child : child.element; - this.element.prepend(element); - } - - // Remove a child component - remove(component) { - let index = this.children.indexOf(component); - - // The component does not belong to this component - if (index == -1) - return -1; - - // Remove the child component from this component - this.children.splice(index, 1); - if ("removeHook" in this) - this.removeHook(component); - else component.element.remove(); - if ("removedHook" in component) - component.removedHook(this); - return index; - } - - // Remove an event listener - removeEventListener(type, listener, useCapture) { - this.element.removeEventListener(type, listener, useCapture); - } - - // Specify an HTML attribute's value - setAttribute(name, value) { - value = - value === false ? false : - value === null || value === undefined ? "" : - value.toString().trim() - ; - if (value === "") - this.element.removeAttribute(name); - else this.element.setAttribute(name, value); - } - - // Specify whether or not the element is focusable - setFocusable(focusable, tabStop) { - if (!focusable) - this.element.removeAttribute("tabindex"); - else this.element.setAttribute("tabindex", - tabStop || tabStop === undefined ? "0" : "-1"); - } - - // Specify a localization key for the accessible name label - setLabel(key) { - this.label = key; - this.translate(); - } - - // Specify the DOM Id for this element - setId(id) { - this.id = id = id || null; - this.setAttribute("id", id); - } - - // Specify text to substitute within localized contexts - setSubstitution(key, text, noTranslate) { - let ret = this.substitutions[key] || null; - - // Providing new text - if (text !== null) - this.substitutions[key] = text.toString(); - - // Removing an association - else if (key in this.substitutions) - delete this.substitutions[key]; - - // Update display text - if (!noTranslate) - this.translate(); - return ret; - } - - // Specify a localization key for the tool tip text - setToolTip(key) { - this.toolTip = key; - this.translate(); - } - - // Specify whether the component is visible - setVisible(visible) { - let prop = this.visibility ? "visibility" : "display"; - if (!!visible) - this.element.style.removeProperty(prop); - else this.element.style[prop] = this.visibility ? "hidden" : "none"; - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Dispatch an event - event(type, fields) { - this.element.dispatchEvent(Toolkit.event(type, this, fields)); - } - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - - // Regenerate localized display text - translate() { - if (this.label) - this.setAttribute("aria-label", this.gui.translate(this.label, this)); - if (this.toolTip) - this.setAttribute("title", this.gui.translate(this.toolTip, this)); - } - -}; - - - -export { Component }; diff --git a/app/toolkit/DropDown.js b/app/toolkit/DropDown.js deleted file mode 100644 index 73ef37b..0000000 --- a/app/toolkit/DropDown.js +++ /dev/null @@ -1,125 +0,0 @@ -import { Component } from /**/"./Component.js"; -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// DropDown // -/////////////////////////////////////////////////////////////////////////////// - -// Text entry field -class DropDown extends Component { - static Component = Component; - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-dropdown", - tagName : "select" - }); - - // Configure instance fields - this.isEnabled = null; - this.options = []; - - // Configure component - options = options || {}; - this.setEnabled(!("enabled" in options) || options.enabled); - if ("options" in options) - this.setOptions(options.options); - this.setSelectedIndex( - ("selectedIndex" in options ? options : this).selectedIndex); - - // Configure event handlers - this.addEventListener("keydown" , e=>e.stopPropagation()); - this.addEventListener("pointerdown", e=>e.stopPropagation()); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Programmatically change the selection - change() { - this.element.dispatchEvent(this.event("input")); - } - - // Retrieve the current selection index - getSelectedIndex() { - return this.element.selectedIndex; - } - - // Specify whether the button can be activated - setEnabled(enabled) { - this.isEnabled = enabled = !!enabled; - this.setAttribute("disabled", enabled ? null : "true"); - } - - // Specify the list contents - setOptions(options) { - - // Error checking - if (!Array.isArray(options)) - return; - - // Erase the list of options - this.options.splice(0); - this.element.replaceChildren(); - - // Add options from the input - for (let option of options) { - if (typeof option != "string") - continue; - this.options.push(option); - this.element.add(document.createElement("option")); - } - - // Update the display text - this.translate(); - } - - // Specify the current selection - setSelectedIndex(index) { - - // Error checking - if (typeof index != "number" || isNaN(index)) - return this.element.selectedIndex; - index = Math.round(index); - if (index < -1 || index >= this.options.length) - return this.element.selectedIndex; - - // Configure element and instance fields - return this.element.selectedIndex = index; - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - - // Regenerate localized display text - translate() { - super.translate(); - - // Error checking - if (!this.options) - return; - - // Update the list items - for (let x = 0; x < this.options.length; x++) { - this.element.item(x).innerText = - this.gui.translate(this.options[x], this); - } - - } - -} - - - -export { DropDown }; diff --git a/app/toolkit/MenuBar.js b/app/toolkit/MenuBar.js deleted file mode 100644 index 2d9b7fa..0000000 --- a/app/toolkit/MenuBar.js +++ /dev/null @@ -1,748 +0,0 @@ -import { Component } from /**/"./Component.js"; -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// Menu // -/////////////////////////////////////////////////////////////////////////////// - -// Pop-up menu container, child of MenuItem -class Menu extends Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className : "tk tk-menu", - role : "menu", - tagName : "div", - visibility: true, - visible : false, - style : { - position: "absolute", - } - }); - - // Trap pointer events - this.addEventListener("pointerdown", e=>{ - e.stopPropagation(); - e.preventDefault(); - }); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Replacement behavior for parent.add() - addedHook(parent) { - this.setAttribute("aria-labelledby", parent.id); - } - -}; - - - -/////////////////////////////////////////////////////////////////////////////// -// MenuSeparator // -/////////////////////////////////////////////////////////////////////////////// - -// Separator between groups of menu items -class MenuSeparator extends Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-menu-separator", - role : "separator", - tagName : "div" - }); - } - -}; - - - -/////////////////////////////////////////////////////////////////////////////// -// MenuItem // -/////////////////////////////////////////////////////////////////////////////// - -// Individual menu selection -class MenuItem extends Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-menu-item", - focusable: true, - tabStop : false, - tagName : "div" - }); - options = options || {}; - - // Configure instance fields - this.isEnabled = null; - this.isExpanded = false; - this.menu = null; - this.menuBar = null; - this.text = null; - this.type = null; - - // Configure element - this.contents = document.createElement("div"); - this.append(this.contents); - this.eicon = document.createElement("div"); - this.eicon.className = "tk tk-icon"; - this.contents.append(this.eicon); - this.etext = document.createElement("div"); - this.etext.className = "tk tk-text"; - this.contents.append(this.etext); - - // Configure event handlers - this.addEventListener("blur" , e=>this.onBlur (e)); - this.addEventListener("keydown" , e=>this.onKeyDown (e)); - this.addEventListener("pointerdown", e=>this.onPointerDown(e)); - this.addEventListener("pointermove", e=>this.onPointerMove(e)); - this.addEventListener("pointerup" , e=>this.onPointerUp (e)); - - // Configure widget - this.gui.localize(this); - this.setEnabled("enabled" in options ? !!options.enabled : true); - this.setId (Toolkit.id()); - this.setText (options.text); - this.setType (options.type, options.checked); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Focus lost - onBlur(e) { - - // An item in a different menu is receiving focus - if (this.menu != null) { - if ( - !this .contains(e.relatedTarget) && - !this.menu.contains(e.relatedTarget) - ) this.setExpanded(false); - } - - // Item is an action - else if (e.component == this) - this.element.classList.remove("active"); - - // Simulate a bubbling event sequence - if (this.parent) - this.parent.onBlur(e); - } - - // Key press - onKeyDown(e) { - - // Processing by key - switch (e.key) { - - case "ArrowDown": - - // Error checking - if (!this.parent) - break; - - // Top-level: open the menu and focus its first item - if (this.parent == this.menuBar) { - if (this.menu == null) - return; - this.setExpanded(true); - this.listItems()[0].focus(); - } - - // Sub-menu: cycle to the next sibling - else { - let items = this.parent.listItems(); - items[(items.indexOf(this) + 1) % items.length].focus(); - } - - break; - - case "ArrowLeft": - - // Error checking - if (!this.parent) - break; - - // Sub-menu: close and focus parent - if ( - this.parent != this.menuBar && - this.parent.parent != this.menuBar - ) { - this.parent.setExpanded(false); - this.parent.focus(); - } - - // Top-level: cycle to previous sibling - else { - let menu = this.parent == this.menuBar ? - this : this.parent; - let items = this.menuBar.listItems(); - let prev = items[(items.indexOf(menu) + - items.length - 1) % items.length]; - if (menu.isExpanded) - prev.setExpanded(true); - prev.focus(); - } - - break; - - case "ArrowRight": - - // Error checking - if (!this.parent) - break; - - // Sub-menu: open the menu and focus its first item - if (this.menu != null && this.parent != this.menuBar) { - this.setExpanded(true); - (this.listItems()[0] || this).focus(); - } - - // Top level: cycle to next sibling - else { - let menu = this; - while (menu.parent != this.menuBar) - menu = menu.parent; - let expanded = this.menuBar.expandedMenu() != null; - let items = this.menuBar.listItems(); - let next = items[(items.indexOf(menu) + 1) % items.length]; - next.focus(); - if (expanded) - next.setExpanded(true); - } - - break; - - case "ArrowUp": - - // Error checking - if (!this.parent) - break; - - // Top-level: open the menu and focus its last item - if (this.parent == this.menuBar) { - if (this.menu == null) - return; - this.setExpanded(true); - let items = this.listItems(); - (items[items.length - 1] || this).focus(); - } - - // Sub-menu: cycle to previous sibling - else { - let items = this.parent.listItems(); - items[(items.indexOf(this) + - items.length - 1) % items.length].focus(); - } - - break; - - case "End": - { - - // Error checking - if (!this.parent) - break; - - // Focus last sibling - let expanded = this.isExpanded && - this.parent == this.menuBar; - let items = this.parent.listItems(); - let last = items[items.length - 1] || this; - last.focus(); - if (expanded) - last.setExpanded(true); - } - break; - - case "Enter": - case " ": - - // Do nothing - if (!this.isEnabled) - break; - - // Action item: activate the menu item - if (this.menu == null) - this.activate(this.type == "check" && e.key == " "); - - // Sub-menu: open the menu and focus its first item - else { - this.setExpanded(true); - let items = this.listItems(); - if (items[0]) - items[0].focus(); - } - break; - - case "Escape": - - // Error checking - if (!this.parent) - break; - - // Top-level (not specified by WAI-ARIA) - if (this.parent == this.menuBar) { - if (this.isExpanded) - this.setExpanded(false); - else this.menuBar.exit(); - } - - // Sub-menu: close and focus parent - else { - this.parent.setExpanded(false); - this.parent.focus(); - } - break; - - case "Home": - { - - // Error checking - if (!this.parent) - break; - - // Focus first sibling - let expanded = this.isExpanded && - this.parent == this.menuBar; - let first = this.parent.listItems()[0] || this; - first.focus(); - if (expanded) - first.setExpanded(true); - } - break; - - // Do not handle the event - default: return; - } - - // The event was handled - e.stopPropagation(); - e.preventDefault(); - } - - // Pointer press - onPointerDown(e) { - this.focus(); - - // Error checking - if ( - !this.isEnabled || - this.element.hasPointerCapture(e.pointerId) || - e.button != 0 - ) return; - - // Configure event - if (this.menu == null) - this.element.setPointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure component - if (this.menu != null) - this.setExpanded(!this.isExpanded); - else this.element.classList.add("active"); - } - - // Pointer move - onPointerMove(e) { - - // Hovering over a menu when a sibling menu is already open - let expanded = this.parent && this.parent.expandedMenu(); - if (this.menu != null && expanded != null && expanded != this) { - - // Configure component - this.setExpanded(true); - this.focus(); - - // Configure event - e.stopPropagation(); - e.preventDefault(); - return; - } - - // Not dragging - if (!this.element.hasPointerCapture(e.pointerId)) - return; - - // Configure event - e.stopPropagation(); - e.preventDefault(); - - // Not an action item - if (this.menu != null) - return; - - // Check if the cursor is within the bounds of the component - this.element.classList[ - Toolkit.isInside(this.element, e) ? "add" : "remove"]("active"); - } - - // Pointer release - onPointerUp(e) { - - // Error checking - if ( - !this.isEnabled || - e.button != 0 || - (this.parent && this.parent.hasFocus() ? - this.menu != null : - !this.element.hasPointerCapture(e.pointerId) - ) - ) return; - - // Configure event - this.element.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Item is an action - let bounds = this.getBounds(); - if (this.menu == null && Toolkit.isInside(this.element, e)) - this.activate(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Invoke an action command - activate(noExit) { - if (this.menu != null) - return; - - if (this.type == "check") - this.setChecked(!this.isChecked); - - if (!noExit) - this.menuBar.exit(); - - this.element.dispatchEvent(Toolkit.event("action", this)); - } - - // Add a separator between groups of menu items - addSeparator(options) { - let sep = new Toolkit.MenuSeparator(this, options); - this.add(sep); - return sep; - } - - // Produce a list of child items - listItems(invisible) { - return this.children.filter(c=> - c instanceof Toolkit.MenuItem && - (invisible || c.isVisible()) - ); - } - - // Specify whether the menu item is checked - setChecked(checked) { - if (this.type != "check") - return; - this.isChecked = !!checked; - this.setAttribute("aria-checked", this.isChecked); - } - - // Specify whether the menu item can be activated - setEnabled(enabled) { - this.isEnabled = enabled = !!enabled; - this.setAttribute("aria-disabled", enabled ? null : "true"); - if (!enabled) - this.setExpanded(false); - } - - // Specify whether the sub-menu is open - setExpanded(expanded) { - - // State is not changing - expanded = !!expanded; - if (this.menu == null || expanded === this.isExpanded) - return; - - // Position the sub-menu - if (expanded) { - let bndGUI = this.gui .getBounds(); - let bndMenu = this.menu.getBounds(); - let bndThis = this .getBounds(); - let bndParent = !this.parent ? bndThis : ( - this.parent == this.menuBar ? this.parent : this.parent.menu - ).getBounds(); - this.menu.element.style.left = Math.max(0, - Math.min( - (this.parent && this.parent == this.menuBar ? - bndThis.left : bndThis.right) - bndParent.left, - bndGUI.right - bndMenu.width - ) - ) + "px"; - this.menu.element.style.top = Math.max(0, - Math.min( - (this.parent && this.parent == this.menuBar ? - bndThis.bottom : bndThis.top) - bndParent.top, - bndGUI.bottom - bndMenu.height - ) - ) + "px"; - } - - // Close all open sub-menus - else for (let child of this.listItems()) - child.setExpanded(false); - - // Configure component - this.isExpanded = expanded; - this.setAttribute("aria-expanded", expanded); - this.menu.setVisible(expanded); - if (expanded) - this.element.classList.add("active"); - else this.element.classList.remove("active"); - } - - // Specify the widget's display text - setText(text) { - this.text = (text || "").toString().trim(); - this.translate(); - } - - // Specify what kind of menu item this is - setType(type, arg) { - this.type = type = (type || "").toString().trim() || "normal"; - switch (type) { - case "check": - this.setAttribute("role", "menuitemcheckbox"); - this.setChecked(arg); - break; - default: // normal - this.setAttribute("role", "menuitem"); - this.setAttribute("aria-checked", null); - break; - } - if (this.parent && "checkIcons" in this.parent) - this.parent.checkIcons(); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Replacement behavior for add() - addHook(component) { - - // Convert to sub-menu - if (this.menu == null) { - this.menu = new Toolkit.Menu(this); - this.after(this.menu); - this.setAttribute("aria-haspopup", "menu"); - this.setAttribute("aria-expanded", "false"); - if (this.parent && "checkIcons" in this.parent) - this.parent.checkIcons(); - } - - // Add the child component - component.menuBar = this.menuBar; - this.menu.append(component); - if (component instanceof Toolkit.MenuItem && component.menu != null) - this.menu.append(component.menu); - - // Configure icon mode - this.checkIcons(); - } - - // Check whether any child menu items contain icons - checkIcons() { - if (this.menu == null) - return; - if (this.children.filter(c=> - c instanceof Toolkit.MenuItem && - c.menu == null && - c.type != "normal" - ).length != 0) - this.menu.element.classList.add("icons"); - else this.menu.element.classList.remove("icons"); - } - - // Replacement behavior for remove() - removeHook(component) { - - // Remove the child component - component.element.remove(); - if (component instanceof Toolkit.MenuItem && component.menu != null) - component.menu.element.remove(); - - // Convert to action item - if (this.children.length == 0) { - this.menu.element.remove(); - this.menu = null; - this.setAttribute("aria-haspopup", null); - this.setAttribute("aria-expanded", "false"); - if (this.parent && "checkIcons" in this.parent) - this.parent.checkIcons(); - } - - } - - // Regenerate localized display text - translate() { - super.translate(); - if (!("contents" in this)) - return; - this.etext.innerText = this.gui.translate(this.text, this); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Retrieve the currently expanded sub-menu, if any - expandedMenu() { - return this.children.filter(c=>c.isExpanded)[0] || null; - } - -}; - - - -/////////////////////////////////////////////////////////////////////////////// -// MenuBar // -/////////////////////////////////////////////////////////////////////////////// - -// Application menu bar -class MenuBar extends Component { - static Component = Component; - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-menu-bar", - focusable: false, - tagName : "div", - tabStop : true, - role : "menubar", - style : { - position: "relative", - zIndex : "1" - } - }); - - // Configure instance fields - this.focusTarget = null; - this.menuBar = this; - - // Configure event handlers - this.addEventListener("blur" , e=>this.onBlur (e), true); - this.addEventListener("focus" , e=>this.onFocus (e), true); - this.addEventListener("keydown", e=>this.onKeyDown(e), true); - - // Configure widget - this.gui.localize(this); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Focus lost - onBlur(e) { - if (this.contains(e.relatedTarget)) - return; - let items = this.listItems(); - if (items[0]) - items[0].setFocusable(true, true); - let menu = this.expandedMenu(); - if (menu != null) - menu.setExpanded(false); - } - - // Focus gained - onFocus(e) { - if (this.contains(e.relatedTarget)) - return; - let items = this.listItems(); - if (items[0]) - items[0].setFocusable(true, false); - this.focusTarget = e.relatedTarget; - } - - // Key pressed - onKeyDown(e) { - if (e.key != "Tab") - return; - e.stopPropagation(); - e.preventDefault(); - this.exit(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Produce a list of child items - listItems(invisible) { - return this.children.filter(c=> - c instanceof Toolkit.MenuItem && - (invisible || c.isVisible()) - ); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Replacement behavior for add() - addHook(component) { - component.menuBar = this.menuBar; - this.append(component); - if (component instanceof Toolkit.MenuItem && component.menu != null) - this.append(component.menu); - let items = this.listItems(); - if (items[0]) - items[0].setFocusable(true, true); - } - - // Return control to the application - exit() { - this.onBlur({ relatedTarget: null }); - if (this.focusTarget) - this.focusTarget.focus(); - else document.activeElement.blur(); - } - - // Replacement behavior for remove() - removeHook(component) { - component.element.remove(); - if (component instanceof Toolkit.MenuItem && component.menu != null) - component.menu.element.remove(); - let items = this.listItems(); - if (items[0]) - items[0].setFocusable(true, true); - } - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Retrieve the currently expanded menu, if any - expandedMenu() { - return this.children.filter(c=>c.isExpanded)[0] || null; - } - -}; - - - -export { Menu, MenuBar, MenuItem, MenuSeparator }; diff --git a/app/toolkit/ScrollBar.js b/app/toolkit/ScrollBar.js deleted file mode 100644 index 889766b..0000000 --- a/app/toolkit/ScrollBar.js +++ /dev/null @@ -1,1149 +0,0 @@ -import { Component } from /**/"./Component.js"; -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// ScrollBar // -/////////////////////////////////////////////////////////////////////////////// - -// Range picker with track, scroll box and scroll buttons -class ScrollBar extends Component { - static Component = Component; - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-scrollbar", - focusable: true, - role : "scrollbar", - tabStop : true, - tagName : "div", - style : { - alignItems : "stretch", - display : "flex", - flexDirection: "column" - } - }); - - // Configure instance fields - this.extent = 50; - this.increment = 1; - this.isEnabled = true; - this.maximum = 100; - this.minimum = 0; - this.orientation = "vertical"; - this.value = 25; - - // Unit decrement button - this.unitDown = document.createElement("div"); - this.unitDown.className = "tk tk-unit-down"; - this.append(this.unitDown); - this.unitDown.addEventListener("pointerdown", - e=>this.onIncrementPointerDown(e)); - this.unitDown.addEventListener("pointermove", - e=>this.onIncrementPointerMove(e)); - this.unitDown.addEventListener("pointerup" , - e=>this.onIncrementPointerUp (e)); - - // Block decrement track - this.blockDown = document.createElement("div"); - this.blockDown.className = "tk tk-block-down"; - this.append(this.blockDown); - this.blockDown.addEventListener("pointerdown", - e=>this.onIncrementPointerDown(e)); - this.blockDown.addEventListener("pointermove", - e=>this.onIncrementPointerMove(e)); - this.blockDown.addEventListener("pointerup" , - e=>this.onIncrementPointerUp (e)); - - // Scroll box - this.thumb = document.createElement("div"); - this.thumb.className = "tk tk-thumb"; - this.append(this.thumb); - this.thumb.addEventListener("pointerdown", - e=>this.onThumbPointerDown(e)); - this.thumb.addEventListener("pointermove", - e=>this.onThumbPointerMove(e)); - this.thumb.addEventListener("pointerup" , - e=>this.onThumbPointerUp (e)); - - // Block increment track - this.blockUp = document.createElement("div"); - this.blockUp.className = "tk tk-block-up"; - this.append(this.blockUp); - this.blockUp.addEventListener("pointerdown", - e=>this.onIncrementPointerDown(e)); - this.blockUp.addEventListener("pointermove", - e=>this.onIncrementPointerMove(e)); - this.blockUp.addEventListener("pointerup" , - e=>this.onIncrementPointerUp (e)); - - // Unit increment track - this.unitUp = document.createElement("div"); - this.unitUp.className = "tk tk-unit-up"; - this.append(this.unitUp); - this.unitUp.addEventListener("pointerdown", - e=>this.onIncrementPointerDown(e)); - this.unitUp.addEventListener("pointermove", - e=>this.onIncrementPointerMove(e)); - this.unitUp.addEventListener("pointerup" , - e=>this.onIncrementPointerUp (e)); - - // Configure component - options = options || {}; - this.setOrientation("vertical", true); - this.addEventListener("resize", ()=>this.update()); - this.addEventListener("keydown", e=>this.onKeyDown(e)); - if ("enabled" in options) - this.setEnabled (options.enabled ); - if ("extent" in options) - this.setExtent (options.extent , true); - if ("increment" in options) - this.setIncrement (options.increment , true); - if ("minimum" in options) - this.setMinimum (options.minimum , true); - if ("maximum" in options) - this.setMaximum (options.maximum , true); - if ("orientation" in options) - this.setOrientation(options.orientation, true); - this.setValue("value" in options ? options.value : - Math.round((this.minimum + this.maximum - this.extent) / 2)); - } - - - - ///////////////////////////// Event Listeners ///////////////////////////// - - // Increment pointer down - onIncrementPointerDown(e) { - this.focus(); - - // Error checking - if ( - !this.isEnabled || e.button != 0 || - this.extent >= this.maximum - this.minimum || - e.target.hasPointerCapture(e.pointerCapture) - ) return; - - // Configure event - e.target.setPointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure element - e.target.classList.add("tk-active"); - } - - // Increment pointer move - onIncrementPointerMove(e) { - - // Error checking - if (!e.target.hasPointerCapture(e.pointerId)) - return; - - // Configure event - e.stopPropagation(); - e.preventDefault(); - - // Determine whether the event is within the element's bounds - let bounds = e.target.getBoundingClientRect(); - e.target.classList[ - e.offsetX >= 0 && e.offsetX < bounds.width && - e.offsetY >= 0 && e.offsetY < bounds.height ? - "add" : "remove" - ]("tk-active"); - } - - // Increment pointer up - onIncrementPointerUp(e) { - - // Error checking - if (!e.target.hasPointerCapture(e.pointerId) || e.button != 0) - return; - - // Configure event - e.target.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure component - e.target.classList.remove("tk-active"); - - // Take the appropriate action - let bounds = e.target.getBoundingClientRect(); - if ( - e.offsetX >= 0 && e.offsetX < bounds.width && - e.offsetY >= 0 && e.offsetY < bounds.height - ) switch (e.target) { - case this.blockDown: - this.setValue(this.value - this.extent ); break; - case this.blockUp: - this.setValue(this.value + this.extent ); break; - case this.unitDown: - this.setValue(this.value - this.increment); break; - case this.unitUp: - this.setValue(this.value + this.increment); break; - } - - } - - // Scroll bar key press - onKeyDown(e) { - - // Error checking - if (!this.isEnabled) - return; - - // Processing by key - switch (e.key) { - - // Arrow key navigation - case "ArrowDown": - if (this.orientation == "horizontal") - return; - this.setValue(this.value + this.increment); - break; - case "ArrowLeft": - if (this.orientation == "vertical") - return; - this.setValue(this.value - this.increment); - break; - case "ArrowRight": - if (this.orientation == "vertical") - return; - this.setValue(this.value + this.increment); - break; - case "ArrowUp": - if (this.orientation == "horizontal") - return; - this.setValue(this.value - this.increment); - break; - - // Page key navigation - case "PageDown": - this.setValue(this.value + this.extent); - break; - case "PageUp": - this.setValue(this.value - this.extent); - break; - - // Cancel a thumb drag - case "Escape": - - // No thumb drag is in progress - if ( - this.drag == null || - !this.thumb.hasPointerCapture(this.drag.pointerId) - ) return; - - // Cancel the thumb drag - this.thumb.releasePointerCapture(this.drag.pointerId); - this.setValue(this.drag.value); - this.drag = null; - break; - - default: return; - } - - // Configure element - e.stopPropagation(); - e.preventDefault(); - } - - // Thumb pointer down - onThumbPointerDown(e) { - this.focus(); - - // Error checking - if ( - !this.isEnabled || e.button != 0 || - this.extent >= this.maximum - this.minimum || - this.thumb.hasPointerCapture(e.pointerId) - ) return; - - // Configure event - this.thumb.setPointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Begin dragging - this.measure(); - this.drag = { - pointerId: e.pointerId, - thumbPos : this.thumbPos, - value : this.value, - x : e.screenX / devicePixelRatio, - y : e.screenY / devicePixelRatio - }; - - } - - // Thumb pointer move - onThumbPointerMove(e) { - - // Error checking - if (!this.thumb.hasPointerCapture(e.pointerId)) - return; - - // Configure event - e.stopPropagation(); - e.preventDefault(); - - // Update the thumb's position and potentially the value - let delta = this.orientation == "horizontal" ? - e.screenX / devicePixelRatio - this.drag.x : - e.screenY / devicePixelRatio - this.drag.y - ; - this.update(this.drag.thumbPos + delta); - } - - // Thumb pointer up - onThumbPointerUp(e) { - - // Error checking - if (!this.thumb.hasPointerCapture(e.pointerId) || e.button != 0) - return; - - // Configure event - this.thumb.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure instance fields - this.drag = null; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Specify whether or not the scroll bar is enabled - setEnabled(enabled) { - enabled = !!enabled; - if (enabled == this.isEnabled) - return; - this.isEnabled = enabled; - this.setAttribute("aria-disabled", !enabled); - this.update(); - } - - // Specify how many scroll units are currently visible - setExtent(extent, noUpdate) { - - // Error checking - extent = parseInt(extent); - if (isNaN(extent)) - return; - - // Configure instance fields - this.extent = Math.max(0, extent); - - // Update elements - this.setAttribute("aria-valuemax", - Math.max(this.minimum, this.maximum - this.extent)); - if (!noUpdate) - this.update(); - } - - // Specify the value change when clicking a unit scroll button - setIncrement(increment) { - - // Error checking - increment = parseInt(increment); - if (isNaN(increment) || increment < 1) - return; - - // Configure instance fields - this.increment = increment; - } - - // Specify the maximum scroll value - setMaximum(maximum, noUpdate) { - - // Error checking - maximum = parseInt(maximum); - if (isNaN(maximum)) - return; - - // Update maximum - this.maximum = maximum; - this.setAttribute("aria-valuemax", maximum); - - // Update other properties as needed - if (maximum < this.minimum) - this.setMinimum(maximum , true); - if (maximum - this.extent < this.value) - this.setValue (maximum - this.extent , true); - - // Update elements - this.setAttribute("aria-valuemax", - Math.max(this.minimum, this.maximum - this.extent)); - if (!noUpdate) - this.update(); - } - - // Specify the minimum scroll value - setMinimum(minimum, noUpdate) { - - // Error checking - minimum = parseInt(minimum); - if (isNaN(minimum)) - return; - - // Update minimum - this.minimum = minimum; - this.setAttribute("aria-valuemin", minimum); - - // Update other properties as needed - if (minimum > this.maximum) - this.setMaximum(minimum, true); - if (minimum > this.value) - this.setValue (minimum, true); - - // Update elements - if (!noUpdate) - this.update(); - } - - // Specify the widget's orientation - setOrientation(orientation, noUpdate) { - - // Configure element - switch (orientation) { - case "horizontal": - this.element.style.flexDirection = "row"; - this.element.setAttribute("aria-orientation", "horizontal"); - break; - case "vertical": - this.element.style.flexDirection = "column"; - this.element.setAttribute("aria-orientation", "vertical"); - break; - default: return; - } - - // Configure instance fields - this.orientation = orientation; - - // Update elements - if (!noUpdate) - this.update(); - } - - // Specify the current scroll value - setValue(value, noUpdate) { - - // Error checking - value = parseInt(value); - if (isNaN(value) || value == this.value) - return; - - // Update value - value = Math.max(this.minimum, - Math.min(value, this.maximum - this.extent)); - if (value == this.value) - return; - this.value = value; - this.setAttribute("aria-valuenow", value); - - // Update elements - if (!noUpdate) - this.update(); - - // Notify event listeners - this.event("input", { value: value }); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - - // Configure elements given the current widget state - update(thumbPos) { - this.measure(); - - // Update the value according to the given thumb position - if (thumbPos !== undefined) { - let maxPos = this.trackSize - this.thumbSize; - thumbPos = Math.max(0, Math.min(maxPos, thumbPos)); - this.setValue(Math.round( - this.minimum + thumbPos / maxPos * - (this.maximum - this.extent - this.minimum) - ), true); - } - - // Reposition the thumb according to current value - else { - thumbPos = Math.round( - (this.trackSize - this.thumbSize ) * - (this.value - this.minimum) / - (this.maximum - this.extent - this.minimum) - ); - } - - // Configure elements - this.thumb .style.flexBasis = this.thumbSize + "px"; - this.blockDown.style.flexBasis = thumbPos + "px"; - this.blockUp .style.flexBasis = - (this.trackSize - this.thumbSize - thumbPos) + "px"; - this.element.classList[ - this.isEnabled && - this.extent >= this.maximum - this.minimum ? - "add" : "remove" - ]("tk-full"); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Measure the current dimensions of widget components - measure() { - let bndBlockDown = this.blockDown.getBoundingClientRect(); - let bndThis = this.getBounds(); - let bndUnitDown = this.unitDown.getBoundingClientRect(); - let bndUnitUp = this.unitUp .getBoundingClientRect(); - let dim = this.orientation=="horizontal" ? "width" : "height"; - - // Track size is total size less the unit buttons - this.trackSize = bndThis[dim] - bndUnitDown[dim] - bndUnitUp[dim]; - - // Thumb size is proportional to extent - this.thumbSize = Math.max(0, Math.min(this.trackSize, Math.max(4, - this.minimum == this.maximum ? this.trackSize : Math.round( - this.trackSize * this.extent / - (this.maximum - this.minimum) - ) - ))); - - // Thumb position is the size of the block down track - this.thumbPos = bndBlockDown[dim]; - } - -}; - - - -/////////////////////////////////////////////////////////////////////////////// -// ScrollPane // -/////////////////////////////////////////////////////////////////////////////// - -// Scrolling viewport for an external view -class ScrollPane extends Component { - - //////////////////////////////// Constants //////////////////////////////// - - // Scroll bar policies - static ALWAYS = 0; - static AS_NEEDED = 1; - static NEVER = 2; - - - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-scrollpane", - tagName : "div", - style : { - overflow: "hidden", - position: "relative" - } - }); - - // Configure instance fields - this.view = null; - this.viewResize = null; - - // Viewport - this.viewport = document.createElement("div"); - this.viewport.className = "tk tk-viewport"; - Object.assign(this.viewport.style, { - position: "absolute", - bottom : "0", - left : "0", - overflow: "hidden", - right : "0", - top : "0" - }); - this.append(this.viewport); - - // Vertical scroll bar - this.vertical = new ScrollBar(gui, { - orientation: "vertical", - visibility : true, - style : { - bottom : "0", - position: "absolute", - right : "0", - top : "0" - } - }); - this.append(this.vertical); - this.vertical.addEventListener("input", - e=>this.onVerticalScroll(e)); - - // Horizontal scroll bar - this.horizontal = new ScrollBar(gui, { - orientation: "horizontal", - visibility : true, - style : { - bottom : "0", - left : "0", - position: "absolute", - right : "0" - } - }); - this.append(this.horizontal); - this.horizontal.addEventListener("input", - e=>this.onHorizontalScroll(e)); - - // Configure component - options = options || {}; - this.viewport.addEventListener("scroll", e=>this.onScroll(e)); - this.addEventListener("resize" , ()=>this.update()); - this.addEventListener("pointerdown", ()=>this.focus (), true); - if ("horizontal" in options) - this.setPolicy("horizontal", options.horizontal, true); - if ("vertical" in options) - this.setPolicy("vertical" , options.vertical , true); - if ("view" in options) - this.setView (options.view , true); - this.update(); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Horizontal scroll bar scroll - onHorizontalScroll(e) { - if (this.view != null) - this.viewport.scrollLeft = e.value; - } - - // Placeholder for element resize - onResize(e) { } - - // Viewport scrolled - onScroll(e) { - this.horizontal.setValue(this.viewport.scrollLeft); - this.vertical .setValue(this.viewport.scrollTop ); - } - - // Vertical scroll bar scroll - onVerticalScroll(e) { - if (this.view != null) - this.viewport.scrollTop = e.value; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Specify a scroll bar visibility policy - setPolicy(orientation, value, noUpdate) { - - // Error checking - switch (orientation) { - case "horizontal": break; - case "vertical" : break; - default : return; - } - switch (value) { - case ScrollPane.ALWAYS : break; - case ScrollPane.AS_NEEDED: break; - case ScrollPane.NEVER : break; - default : return; - } - - // Configure instance fields - this[orientation].policy = value; - - // Update elements - if (!noUpdate) - this.update(); - } - - // Specify the internal view - setView(view, noUpdate) { - - // Error checking - if (view == this.view) - return; - - // Remove the previous view - if (this.view != null) { - if (Toolkit.isComponent(this.view)) { - this.view.parent = null; - this.view.element.remove; - this.view.removeEventListener("scroll", this.viewScroll); - } else this.view.remove(); - Toolkit.removeResizeListener(this.viewResize); - } - - // Error checking - if (!(view instanceof Element || Toolkit.isComponent(view))) - view = null; - - // Associate the new view - if (view != null) { - this.viewport.append(view instanceof Element?view:view.element); - if (Toolkit.isComponent(view)) - view.parent = this; - } - - // Configure instance fields - this.view = view; - - // Monitor events - if (view) { - if (Toolkit.isComponent(view)) - view = view.element; - this.viewResize = e=>this.onResize(e); - Toolkit.addResizeListener(view, this.viewResize); - } - - // Update elements - if (!noUpdate) - this.update(); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Configure elements given the current widget state - update(noUpdate) { - let bndHorz = this.horizontal.getBounds(); - let bndThis = this .getBounds(); - let bndVert = this.vertical .getBounds(); - - // Configure the initial dimensions of the viewport - let height = bndThis.height; - let width = bndThis.width; - - // Determine the view element - let view = Toolkit.isComponent(this.view)?this.view.element:this.view; - - // Check whether the horizontal scroll bar is visible - let horz = - this.horizontal.policy == ScrollPane.ALWAYS || - this.horizontal.policy != ScrollPane.NEVER && - view != null && width < view.scrollWidth - ; - if (horz) height = Math.max(0, bndThis.height - bndHorz.height); - - // Check whether the vertical scroll bar is visible - let vert = - this.vertical.policy == ScrollPane.ALWAYS || - this.vertical.policy != ScrollPane.NEVER && - view != null && height < view.scrollHeight - ; - if (vert) width = Math.max(0, bndThis.width - bndVert.width); - - // Check the horizontal scroll bar again - if (!horz) { - horz = - this.horizontal.policy != ScrollPane.NEVER && - view != null && width < view.scrollWidth - ; - - // The vertical scroll bar necessitated the horizontal scroll bar - if (horz) height = Math.max(0, bndThis.height - bndHorz.height); - } - - // Resize the viewport - Object.assign(this.viewport.style, { - height: height + "px", - width : width + "px" - }); - - // Configure horizontal scroll bar - this.horizontal.setMaximum( - view == null ? 0 : view.scrollWidth, true); - this.horizontal.setExtent(this.view == null ? 0 : width); - this.horizontal.setVisible(horz); - this.horizontal.element.style.right = - vert ? bndVert.width + "px" : 0; - - // Configure vertical scroll bar - this.vertical.setMaximum( - view == null ? 0 : view.scrollHeight, true); - this.vertical.setExtent(view == null ? 0 : height); - this.vertical.setVisible(vert); - this.vertical.element.style.bottom = - horz ? bndHorz.height + "px" : 0; - } - -} - - - -/////////////////////////////////////////////////////////////////////////////// -// SplitPane // -/////////////////////////////////////////////////////////////////////////////// - -// Window splitter with resizable regions -class SplitPane extends Component { - - //////////////////////////////// Constants //////////////////////////////// - - // Edges - static BOTTOM = 0; - static LEFT = 1; - static RIGHT = 2; - static TOP = 3; - - - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-splitpane", - tagName : "div", - style : { - alignItems : "stretch", - display : "flex", - flexDirection: "row" - } - }); - - // Configure instance fields - this.collapsed = null; - this.increment = 10; - - // Configure top/left region - this[0] = document.createElement("div"); - this[0].className = "tk tk-a"; - this[0].id = Toolkit.id(); - Object.assign(this[0].style, { - alignItems : "stretch", - display : "grid", - gridTemplateRows: "auto", - justifyContent : "stretch" - }); - - // Configure the bottom/right region - this[1] = document.createElement("div"); - this[1].className = "tk tk-b"; - Object.assign(this[1].style, { - alignItems : "stretch", - display : "grid", - gridTemplateRows: "auto", - justifyContent : "stretch" - }); - - // Configure the splitter - this.splitter = new Toolkit.Component(gui, { - className: "tk tk-splitter", - focusable: true, - role : "separator", - tagName : "div", - }); - this.splitter.setAttribute("aria-controls", this[0].id); - this.splitter.addEventListener("keydown" ,e=>this.onKeyDown (e)); - this.splitter.addEventListener("pointerdown",e=>this.onPointerDown(e)); - this.splitter.addEventListener("pointermove",e=>this.onPointerMove(e)); - this.splitter.addEventListener("pointerup" ,e=>this.onPointerUp (e)); - - // Configure layout - this.append(this[0]); - this.append(this.splitter); - this.append(this[1]); - - // Configure component - options = options || {}; - this.setEdge(("edge" in options) ? options.edge : SplitPane.LEFT); - this.addEventListener("resize", e=>this.measure()); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Key press - onKeyDown(e) { - let args = this.getArgs(); - - // Dragging is in progress - if (this.drag != null) switch (e.key) { - case "Escape": - this.splitter.element - .releasePointerCapture(this.drag.pointerId); - this.setValue(this.drag.value); - this.drag = null; - break; - default: return; - } - - // No drag is in progress - else switch (e.key) { - - // Arrow keys - case "ArrowDown": - if (!args.horizontal) - return; - this.setValue(args.value + this.increment * - (this.edge == SplitPane.TOP ? 1 : -1)); - break; - case "ArrowLeft": - if (args.horizontal) - return; - this.setValue(args.value + this.increment * - (this.edge == SplitPane.LEFT ? -1 : 1)); - break; - case "ArrowRight": - if (args.horizontal) - return; - this.setValue(args.value + this.increment * - (this.edge == SplitPane.LEFT ? 1 : -1)); - break; - case "ArrowUp": - if (!args.horizontal) - return; - this.setValue(args.value + this.increment * - (this.edge == SplitPane.TOP ? -1 : 1)); - break; - - // Extent keys - case "End": - this.setValue( - this.edge == SplitPane.TOP || - this.edge == SplitPane.LEFT ? - args.max : 0 - ); - break; - case "Home": - this.setValue( - this.edge == SplitPane.TOP || - this.edge == SplitPane.LEFT ? - 0 : args.max - ); - break; - - // Miscellaneous - case "Enter": - if (this.collapsed === null) { - this.setValue(0); - this.collapsed = args.value; - } else this.setValue(this.collapsed); - break; - - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - // Pointer down - onPointerDown(e) { - this.splitter.focus(); - - // Error checking - if (this.splitter.element.hasPointerCapture(e.pointerId)||e.button!=0) - return; - - // Configure event - this.splitter.element.setPointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Record pointer parameters - this.drag = this.getArgs(); - Object.assign(this.drag, { - pointerId: e.pointerId, - primary : this[this.drag.primary], - property : this.drag.horizontal ? "height" : "width", - x : e.screenX / devicePixelRatio, - y : e.screenY / devicePixelRatio - }); - } - - // Pointer move - onPointerMove(e) { - - // Error checking - if (!this.splitter.element.hasPointerCapture(e.pointerId)) - return; - - // Configure event - e.stopPropagation(); - e.preventDefault(); - - // Update splitter position - let coord=e[this.drag.horizontal?"screenY":"screenX"]/devicePixelRatio; - let value = this.drag.value; - switch (this.edge) { - case SplitPane.BOTTOM: value += this.drag.y - coord; break; - case SplitPane.LEFT : value += coord - this.drag.x; break; - case SplitPane.RIGHT : value += this.drag.x - coord; break; - case SplitPane.TOP : value += coord - this.drag.y; break; - } - this.setValue(value); - } - - // Pointer up - onPointerUp(e) { - - // Error checking - if (!this.splitter.element.hasPointerCapture(e.pointerId)||e.button!=0) - return; - - // Configure event - this.splitter.element.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - - // Configure instance fields - this.drag = null; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Retrieve the current position - getValue() { - return this.getArgs().value; - } - - // Specify which edge is controlled by the splitter - setEdge(edge) { - - // Error checking - let args = this.getArgs(edge); - if (args == null) - return; - - // Configure instance fields - this.edge = edge; - - // Configure elements - let pri = this[args.primary ].style; - let sec = this[args.primary ^ 1].style; - this.element.style.flexDirection = args.horizontal ? "column" : "row"; - this.splitter.setAttribute("aria-orientation", - args.horizontal ? "horizontal" : "vertical"); - pri.removeProperty("flex-grow"); - pri.removeProperty("height" ); - pri.removeProperty("width" ); - sec.flexGrow = "1"; - sec.removeProperty("height" ); - sec.removeProperty("width" ); - this.measure(); - } - - // Specify how many pixels to change for an arrow key press - setIncrement(increment) { - if (typeof increment == "number" && !isNaN(increment)) - this.increment = Math.max(1, Math.round(increment)); - } - - // Specify the position of the splitter - setValue(value, args) { - args = args || this.getArgs(); - this[args.primary].style[args.horizontal ? "height" : "width"] = - Math.min(args.max, Math.max(0, Math.round(value))) + "px"; - this.collapsed = null; - this.measure(); - } - - // Specify a child element - setView(index, view, noMeasure) { - index = this[index]; - - // Error checking - if (view == index.view) - return; - - // Remove the previous view - if (index.view != null) { - if (Toolkit.isComponent(index.view)) { - index.view.parent = null; - index.view.element.remove; - } else index.view.remove(); - } - - // Error checking - if (!(view instanceof Element || Toolkit.isComponent(view))) - view = null; - - // Configure instance fields - index.view = view; - - // Associate the new view - if (view != null) { - index.append(view instanceof Element ? view : view.element); - if (Toolkit.isComponent(view)) - view.parent = this; - } - - // Update elements - if (!noMeasure) - this.measure(); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Determine information regarding the current element configuration - getArgs(edge = this.edge) { - let bndA = this[0].getBoundingClientRect(); - let bndB = this[1].getBoundingClientRect(); - - // Processing by edge - let horizontal; - let primary; - switch (edge) { - case SplitPane.BOTTOM: horizontal = true ; primary = 1; break; - case SplitPane.LEFT : horizontal = false; primary = 0; break; - case SplitPane.RIGHT : horizontal = false; primary = 1; break; - case SplitPane.TOP : horizontal = true ; primary = 0; break; - default: return null; - } - - // Processing by orientation - let bndPrimary = primary ? bndB : bndA; - let max; - let value; - if (horizontal) { - max = bndA.height + bndB.height; - value = bndPrimary.height; - } else { - max = bndA.width + bndB.width; - value = bndPrimary.width; - } - - return { - horizontal: horizontal, - max : Math.round(max), - primary : primary, - value : Math.round(value) - }; - } - - // Measure the current element configuration - measure() { - let args = this.getArgs(); - this.splitter.setAttribute("aria-valuemax", args.max); - this.splitter.setAttribute("aria-valuemin", 0); - this.splitter.setAttribute("aria-valuenow", args.value); - } - -} - - - -export { ScrollBar, ScrollPane, SplitPane }; diff --git a/app/toolkit/TextBox.js b/app/toolkit/TextBox.js deleted file mode 100644 index b1d734b..0000000 --- a/app/toolkit/TextBox.js +++ /dev/null @@ -1,145 +0,0 @@ -import { Component } from /**/"./Component.js"; -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// TextBox // -/////////////////////////////////////////////////////////////////////////////// - -// Text entry field -class TextBox extends Component { - static Component = Component; - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-textbox", - tagName : "input" - }); - this.element.type = "text"; - this.setAttribute("spellcheck", "false"); - - // Configure instance fields - this.isEnabled = null; - this.maxLength = null; - this.pattern = null; - - // Configure component - options = options || {}; - this.setEnabled(!("enabled" in options) || options.enabled); - if ("maxLength" in options) - this.setMaxLength(options.maxLength); - if ("pattern" in options) - this.setPattern(options.pattern); - this.setText (options.text); - - // Configure event handlers - this.addEventListener("blur" , e=>this.commit ( )); - this.addEventListener("pointerdown", e=>e.stopPropagation( )); - this.addEventListener("keydown" , e=>this.onKeyDown (e)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Key press - onKeyDown(e) { - - // Processing by key - switch (e.key) { - case "ArrowLeft": - case "ArrowRight": - e.stopPropagation(); - return; - case "Enter": - this.commit(); - break; - default: return; - } - - // Configure event - e.stopPropagation(); - e.preventDefault(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Programmatically commit the text box - commit() { - this.event("action"); - } - - // Retrieve the control's value - getText() { - return this.element.value; - } - - // Specify whether the button can be activated - setEnabled(enabled) { - this.isEnabled = enabled = !!enabled; - this.setAttribute("disabled", enabled ? null : "true"); - } - - // Specify the maximum length of the text - setMaxLength(length) { - - // Remove limitation - if (length === null) { - this.maxLength = null; - this.setAttribute("maxlength", null); - return; - } - - // Error checking - if (typeof length != "number" || isNaN(length)) - return; - - // Range checking - length = Math.floor(length); - if (length < 0) - return; - - // Configure component - this.maxLength = length; - this.setAttribute("maxlength", length); - } - - // Specify a regex pattern for valid text characters - setPattern(pattern) { - /* - Disabled because user agents may not prevent invalid input - - // Error checking - if (pattern && typeof pattern != "string") - return; - - // Configure component - this.pattern = pattern = pattern || null; - this.setAttribute("pattern", pattern); - */ - } - - // Specify the widget's display text - setText(text = "") { - this.element.value = text.toString(); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - -} - - - -export { TextBox }; diff --git a/app/toolkit/Toolkit.js b/app/toolkit/Toolkit.js deleted file mode 100644 index 2235ff9..0000000 --- a/app/toolkit/Toolkit.js +++ /dev/null @@ -1,337 +0,0 @@ -import { Component } from /**/"./Component.js"; -import { Button, CheckBox, Group, Radio } from /**/"./Button.js" ; -import { DropDown } from /**/"./DropDown.js" ; -import { Menu, MenuBar, MenuItem, MenuSeparator } from /**/"./MenuBar.js" ; -import { ScrollBar, ScrollPane, SplitPane } from /**/"./ScrollBar.js"; -import { TextBox } from /**/"./TextBox.js" ; -import { Desktop, Window } from /**/"./Window.js" ; - - - -/////////////////////////////////////////////////////////////////////////////// -// Toolkit // -/////////////////////////////////////////////////////////////////////////////// - -// Top-level user interface manager -let Toolkit = globalThis.Toolkit = (class GUI extends Component { - - static initializer() { - - // Static state - this.nextId = 0; - - // Locale presets - this.NO_LOCALE = { id: "(Null)" }; - - // Component classes - this.components = []; - Button .setToolkit(this); this.components.push(Button .Component); - Component.setToolkit(this); this.components.push( Component); - DropDown .setToolkit(this); this.components.push(DropDown .Component); - MenuBar .setToolkit(this); this.components.push(MenuBar .Component); - ScrollBar.setToolkit(this); this.components.push(ScrollBar.Component); - TextBox .setToolkit(this); this.components.push(TextBox .Component); - Window .setToolkit(this); this.components.push(Window .Component); - this.Button = Button; - this.CheckBox = CheckBox; - this.Component = Component; - this.Desktop = Desktop; - this.DropDown = DropDown; - this.Group = Group; - this.Menu = Menu; - this.MenuBar = MenuBar; - this.MenuItem = MenuItem; - this.MenuSeparator = MenuSeparator; - this.Radio = Radio; - this.ScrollBar = ScrollBar; - this.ScrollPane = ScrollPane; - this.SplitPane = SplitPane; - this.TextBox = TextBox; - this.Window = Window; - - return this; - } - - - - ///////////////////////////// Static Methods ////////////////////////////// - - // Monitor resize events on an element - static addResizeListener(element, listener) { - - // Establish a ResizeObserver - if (!("resizeListeners" in element)) { - element.resizeListeners = []; - element.resizeObserver = new ResizeObserver( - (e,o)=>element.dispatchEvent(this.event("resize"))); - element.resizeObserver.observe(element); - } - - // Associate the listener - if (element.resizeListeners.indexOf(listener) == -1) { - element.resizeListeners.push(listener); - element.addEventListener("resize", listener); - } - - } - - // Stop monitoring resize events on an element - static clearResizeListeners(element) { - while ("resizeListeners" in element) - this.removeResizeListener(element, element.resizeListeners[0]); - } - - // Produce a custom event object - static event(type, component, fields) { - let event = new Event(type, { - bubbles : true, - cancelable: true - }); - if (component) - event.component = component; - if (fields) - Object.assign(event, fields); - return event; - } - - // Produce a unique element ID - static id() { - return "tk" + (this.nextId++); - } - - // Determine whether an object is a component - // The user agent may not resolve imports to the same classes - static isComponent(o) { - return !!this.components.find(c=>o instanceof c); - } - - // Determine whether a pointer event is inside an element - static isInside(element, e) { - let bounds = element.getBoundingClientRect(); - return ( - e.offsetX >= 0 && e.offsetX < bounds.width && - e.offsetY >= 0 && e.offsetY < bounds.height - ); - } - - // Generate a list of focusable child elements - static listFocusables(element) { - return Array.from(element.querySelectorAll( - "*:not(*:not(a[href], area, button, details, input, " + - "textarea, select, [tabindex='0'])):not([disabled])" - )).filter(e=>{ - for (; e instanceof Element; e = e.parentNode) { - let style = getComputedStyle(e); - if (style.display == "none" || style.visibility == "hidden") - return false; - } - return true; - }); - } - - // Stop monitoring resize events on an element - static removeResizeListener(element, listener) { - - // Error checking - if (!("resizeListeners" in element)) - return; - let index = element.resizeListeners.indexOf(listener); - if (index == -1) - return; - - // Remove the listener - element.removeEventListener("resize", element.resizeListeners[index]); - element.resizeListeners.splice(index, 1); - - // No more listeners: delete the ResizeObserver - if (element.resizeListeners.length == 0) { - element.resizeObserver.unobserve(element); - delete element.resizeListeners; - delete element.resizeObserver; - } - - } - - // Compute pointer event screen coordinates - static screenCoords(e) { - return { - x: e.screenX / window.devicePixelRatio, - y: e.screenY / window.devicePixelRatio - }; - } - - - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(options) { - super(null, options); - - // Configure instance fields - this.locale = Toolkit.NO_LOCALE; - this.localized = []; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Specify the locale to use for translated strings - setLocale(locale) { - this.locale = locale || Toolkit.NO_LOCALE; - for (let component of this.localized) - component.translate(); - } - - // Translate a string in the selected locale - translate(key, component) { - - // Front-end method - if (key === undefined) { - super.translate(); - return; - } - - // Working variables - let subs = component ? component.substitutions : {}; - key = (key || "").toString().trim(); - - // Error checking - if (this.locale == null || key == "") - return key; - - // Resolve the key first in the substitutions then in the locale - let text = key; - key = key.toLowerCase(); - if (key in subs) - text = subs[key]; - else if (key in this.locale) - text = this.locale[key]; - else return "!" + text.toUpperCase(); - - // Process all substitutions - for (;;) { - - // Working variables - let sIndex = 0; - let rIndex = -1; - let lIndex = -1; - let zIndex = -1; - - // Locate the inner-most {} or [] pair - for (;;) { - let match = Toolkit.subCtrl(text, sIndex); - - // No control characters found - if (match == -1) - break; - sIndex = match + 1; - - // Processing by control character - switch (text.charAt(match)) { - - // Opening a substitution group - case "{": rIndex = match; continue; - case "[": lIndex = match; continue; - - // Closing a recursion group - case "}": - if (rIndex != -1) { - lIndex = -1; - zIndex = match; - } - break; - - // Closing a literal group - case "]": - if (lIndex != -1) { - rIndex = -1; - zIndex = match; - } - break; - } - - break; - } - - // Process a recursion substitution - if (rIndex != -1) { - text = - text.substring(0, rIndex) + - this.translate( - text.substring(rIndex + 1, zIndex), - component - ) + - text.substring(zIndex + 1) - ; - } - - // Process a literal substitution - else if (lIndex != -1) { - text = - text.substring(0, lIndex) + - text.substring(lIndex + 1, zIndex) - .replaceAll("{", "{{") - .replaceAll("}", "}}") - .replaceAll("[", "[[") - .replaceAll("]", "]]") - + - text.substring(zIndex + 1) - ; - } - - // No more substitutions - else break; - } - - // Unescape all remaining control characters - return (text - .replaceAll("{{", "{") - .replaceAll("}}", "}") - .replaceAll("[[", "[") - .replaceAll("]]", "]") - ); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Reduce an object to a single level of depth - static flatten(obj, ret = {}, id) { - for (let entry of Object.entries(obj)) { - let key = (id ? id + "." : "") + entry[0].toLowerCase(); - let value = entry[1]; - if (value instanceof Object) - this.flatten(value, ret, key); - else ret[key] = value; - } - return ret; - } - - // Register a component for localization management - localize(component) { - if (this.localized.indexOf(component) != -1) - return; - this.localized.push(component); - component.translate(); - } - - // Locate a substitution control character in a string - static subCtrl(text, index) { - for (; index < text.length; index++) { - let c = text.charAt(index); - if ("{}[]".indexOf(c) == -1) - continue; - if (index < text.length - 1 || text.charAt(index + 1) != c) - return index; - index++; - } - return -1; - } - -}).initializer(); - - - -export { Toolkit }; diff --git a/app/toolkit/Window.js b/app/toolkit/Window.js deleted file mode 100644 index 40239f9..0000000 --- a/app/toolkit/Window.js +++ /dev/null @@ -1,699 +0,0 @@ -import { Component } from /**/"./Component.js"; -let Toolkit; - - - -/////////////////////////////////////////////////////////////////////////////// -// Window // -/////////////////////////////////////////////////////////////////////////////// - -// Standalone movable dialog -class Window extends Component { - static Component = Component; - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className : "tk tk-window", - focusable : true, - role : "dialog", - tabStop : false, - tagName : "div", - visibility: true, - style : { - position: "absolute" - } - }); - - // Configure instance fields - this.firstShown = false; - this.lastFocus = null; - - // DOM container - this.contents = document.createElement("div"); - this.contents.style.display = "flex"; - this.contents.style.flexDirection = "column"; - this.element.append(this.contents); - - // Sizing borders - this.borders = {} - this.border("n" ); this.border("w" ); - this.border("e" ); this.border("s" ); - this.border("nw"); this.border("ne"); - this.border("sw"); this.border("se"); - - // Title bar - this.titleBar = document.createElement("div"); - this.titleBar.className = "tk tk-title"; - this.titleBar.style.display = "flex"; - this.contents.append(this.titleBar); - this.titleBar.addEventListener( - "pointerdown", e=>this.onTitlePointerDown(e)); - this.titleBar.addEventListener( - "pointermove", e=>this.onTitlePointerMove(e)); - this.titleBar.addEventListener( - "pointerup" , e=>this.onTitlePointerUp (e)); - - // Title bar text - this.titleText = document.createElement("div"); - this.titleText.className = "tk tk-text"; - this.titleText.id = Toolkit.id(); - this.titleText.style.flexGrow = "1"; - this.titleText.style.position = "relative"; - this.titleBar.append(this.titleText); - this.setAttribute("aria-labelledby", this.titleText.id); - - // Close button - this.titleClose = document.createElement("div"); - this.titleClose.className = "tk tk-close"; - this.titleClose.setAttribute("aria-hidden", "true"); - this.titleBar.append(this.titleClose); - this.titleClose.addEventListener( - "pointerdown", e=>this.onClosePointerDown(e)); - this.titleClose.addEventListener( - "pointermove", e=>this.onClosePointerMove(e)); - this.titleClose.addEventListener( - "pointerup" , e=>this.onClosePointerUp (e)); - - // Window client area - this.client = document.createElement("div"); - this.client.className = "tk tk-client"; - this.client.style.flexGrow = "1"; - this.client.style.minHeight = "0"; - this.client.style.minWidth = "0"; - this.client.style.overflow = "hidden"; - this.client.style.position = "relative"; - this.contents.append(this.client); - - // User agent behavior override - let observer = new ResizeObserver( - ()=>this.titleBar.style.width = - this.client.getBoundingClientRect().width + "px" - ); - observer.observe(this.client); - - // Configure element - this.setAttribute("aria-modal", "false"); - this.setBounds( - options.x , options.y, - options.width, options.height - ); - - // Configure component - this.gui.localize(this); - this.setTitle (options.title ); - this.setCloseToolTip(options.closeToolTip); - this.addEventListener("focus" , e=>this.onFocus(e), true); - this.addEventListener("keydown" , e=>this.onWindowKeyDown (e)); - this.addEventListener("pointerdown", e=>this.onWindowPointerDown(e)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Border pointer down - onBorderPointerDown(e, edge) { - if (e.target.hasPointerCapture(e.pointerId) || e.button != 0) - return; - e.target.setPointerCapture(e.pointerId); - e.preventDefault(); - let bndClient = this.client.getBoundingClientRect(); - let bndWindow = this .getBounds (); - let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow; - let coords = Toolkit.screenCoords(e); - this.drag = { - clickX : coords.x, - clickY : coords.y, - mode : "resize", - pointerId : e.pointerId, - startHeight: bndClient.height, - startWidth : bndClient.width, - startX : bndWindow.x - bndDesktop.x, - startY : bndWindow.y - bndDesktop.y, - target : e.target - }; - } - - // Border pointer move - onBorderPointerMove(e, edge) { - if (!e.target.hasPointerCapture(e.pointerId)) - return; - e.stopPropagation(); - e.preventDefault(); - let bndWindow = this.getBounds(); - this["resize" + edge.toUpperCase()]( - Toolkit.screenCoords(e), - this.client .getBoundingClientRect(), - this.parent ? this.parent.getBounds() : bndWindow, - bndWindow, - this.titleBar.getBoundingClientRect() - ); - } - - // Border pointer up - onBorderPointerUp(e, edge) { - if (!e.target.hasPointerCapture(e.pointerId) || e.button != 0) - return; - e.target.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - } - - // Close pointer down - onClosePointerDown(e) { - if (this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0) - return; - this.titleClose.setPointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - this.titleClose.classList.add("active"); - this.drag = { - mode: "close", - x : e.offsetX, - y : e.offsetY - }; - } - - // Close pointer move - onClosePointerMove(e) { - if (!this.titleClose.hasPointerCapture(e.pointerId)) - return; - e.stopPropagation(); - e.preventDefault(); - if (Toolkit.isInside(this.titleClose, e)) - this.titleClose.classList.add("active"); - else this.titleClose.classList.remove("active"); - } - - // Close pointer up - onClosePointerUp(e) { - if (!this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0) - return; - this.titleClose.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - this.titleClose.classList.remove("active"); - if (Toolkit.isInside(this.titleClose, e)) - this.element.dispatchEvent(Toolkit.event("close", this)); - this.drag = null; - } - - // Focus capture - onFocus(e) { - - // Bring this window to the foreground of its siblings - if (!this.contains(e.relatedTarget) && this.parent) - this.parent.bringToFront(this); - - // The target is not the window itself - if (e.target != this.element) { - this.lastFocus = e.target; - return; - } - - // Select the first focusable child - if (this.lastFocus == null) - this.lastFocus = Toolkit.listFocusables(this.element)[0] || null; - - // Send focus to the most recently focused element - if (this.lastFocus) - this.lastFocus.focus(); - } - - // Title pointer down - onTitlePointerDown(e) { - if (this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0) - return; - this.titleBar.setPointerCapture(e.pointerId); - e.preventDefault(); - let bndWindow = this.getBounds(); - let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow; - let coords = Toolkit.screenCoords(e); - this.drag = { - clickX : coords.x, - clickY : coords.y, - mode : "move", - pointerId: e.pointerId, - startX : bndWindow.x - bndDesktop.x, - startY : bndWindow.y - bndDesktop.y - }; - } - - // Title pointer move - onTitlePointerMove(e) { - if (!this.titleBar.hasPointerCapture(e.pointerId)) - return; - e.stopPropagation(); - e.preventDefault(); - let coords = Toolkit.screenCoords(e); - let valid = this.getValidLocations( - this.drag.startX + coords.x - this.drag.clickX, - this.drag.startY + coords.y - this.drag.clickY - ); - this.setLocation(valid.x, valid.y); - } - - // Title pointer up - onTitlePointerUp(e) { - if (!this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0) - return; - this.titleBar.releasePointerCapture(e.pointerId); - e.stopPropagation(); - e.preventDefault(); - this.drag = null; - } - - // Window key press - onWindowKeyDown(e) { - - // Process by key - switch (e.key) { - - // Undo un-committed bounds modifications - case "Escape": - - // Not dragging - if (this.drag == null) - return; - - // Moving - if (this.drag.mode == "move") { - this.titleBar.releasePointerCapture(this.drag.pointerId); - this.setLocation(this.drag.startX, this.drag.startY); - this.drag = null; - } - - // Resizing - else if (this.drag.mode == "resize") { - this.drag.target - .releasePointerCapture(this.drag.pointerId); - this.setBounds( - this.drag.startX , this.drag.startY, - this.drag.startWidth, this.drag.startHeight - ); - this.drag = null; - } - - break; - - // Transfer focus to another element - case "Tab": - - default: return; - } - - // The event was handled - e.stopPropagation(); - e.preventDefault(); - } - - // Window pointer down - onWindowPointerDown(e) { - this.focus(e); - e.stopPropagation(); - e.preventDefault(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a DOM element to this component's element - append(child) { - let element = child instanceof Element ? child : child.element; - this.client.append(element); - } - - // Position the window in the center of the parent Desktop - center() { - if (!this.parent) - return; - let bndParent = this.parent.getBounds(); - let bndWindow = this .getBounds(); - this.setLocation( - Math.max(Math.floor((bndParent.width - bndWindow.width ) / 2), 0), - Math.max(Math.floor((bndParent.height - bndWindow.height) / 2), 0) - ); - } - - // Programmatically close the window - close() { - this.event("close"); - } - - // Add a DOM element to the beginning of this component's children - prepend(child) { - let element = child instanceof Element ? child : child.element; - this.element.prepend(element); - } - - // Specify a new position and size for the window - setBounds(x, y, width, height) { - this.setSize(width, height); - this.setLocation(x, y); - } - - // Specify the over text for the close button - setCloseToolTip(key) { - this.closeToolTip = key; - this.translate(); - } - - // Specify a new position for the window - setLocation(x, y) { - Object.assign(this.element.style, { - left: Math.round(parseFloat(x) || 0) + "px", - top : Math.round(parseFloat(y) || 0) + "px" - }); - } - - // Specify a new size for the window - setSize(width, height) { - Object.assign(this.client.style, { - width : Math.max(Math.round(parseFloat(width ) || 0, 32)) + "px", - height: Math.max(Math.round(parseFloat(height) || 0, 32)) + "px" - }); - } - - // Specify the window title text - setTitle(key) { - this.title = key; - this.translate(); - } - - // Specify whether the component is visible - setVisible(visible) { - super.setVisible(visible); - if (!visible || this.firstShown) - return; - this.firstShown = true; - this.event("firstshow", this); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Ensure the window is partially visible within its desktop - contain() { - let valid = this.getValidLocations(); - this.setLocation(valid.x, valid.y); - } - - // Determine the range of valid window coordinates - getValidLocations(x, y) { - - // Measure the bounding boxes of the relevant elements - let bndClient = this.client .getBoundingClientRect(); - let bndWindow = this .getBounds (); - let bndTitleBar = this.titleBar.getBoundingClientRect(); - let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow; - - // Compute the minimum and maximum valid window coordinates - let ret = { - maxX: bndDesktop .width - bndTitleBar.height - - bndTitleBar.x + bndWindow .x, - maxY: bndDesktop .height - bndClient .y + - bndWindow .y, - minX: bndTitleBar.height - bndWindow .width + - bndWindow .right - bndTitleBar.right, - minY: 0 - }; - - // Compute the effective "best" window coordinates - ret.x = Math.max(ret.minX, Math.min(ret.maxX, - x === undefined ? bndWindow.x - bndDesktop.x : x)); - ret.y = Math.max(ret.minY, Math.min(ret.maxY, - y === undefined ? bndWindow.y - bndDesktop.y : y)); - return ret; - } - - // Update the global Toolkit object - static setToolkit(toolkit) { - Toolkit = toolkit; - } - - // Regenerate localized display text - translate() { - if (!this.titleText) - return; - this.titleText.innerText = this.gui.translate(this.title, this); - if (this.closeToolTip) - this.titleClose.setAttribute("title", - this.gui.translate(this.closeToolTip, this)); - else this.titleClose.removeAttribute("title"); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Produce a border element and add it to the window - border(edge) { - let border = this.borders[edge] = document.createElement("div"); - border.className = "tk tk-" + edge; - border.style.cursor = edge + "-resize"; - border.style.position = "absolute"; - this.contents.append(border); - border.addEventListener( - "pointerdown", e=>this.onBorderPointerDown(e, edge)); - border.addEventListener( - "pointermove", e=>this.onBorderPointerMove(e, edge)); - border.addEventListener( - "pointerup" , e=>this.onBorderPointerUp (e, edge)); - } - - // Compute client bounds when resizing on the east border - constrainE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let w = this.drag.startWidth + coords.x - this.drag.clickX; - w = Math.max(w, bndTitleBar.height * 4); - if (bndClient.x - bndDesktop.x < 0) - w = Math.max(w, bndDesktop.x - bndClient.x + bndTitleBar.height); - return w; - } - - // Compute client bounds when resizing on the north border - constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let delta = coords.y - this.drag.clickY; - let y = this.drag.startY + delta; - let h = this.drag.startHeight - delta; - let min = Math.max(0, bndClient.bottom - bndDesktop.bottom); - if (h < min) { - delta = min - h; - h += delta; - y -= delta; - } - if (y < 0) { - h += y; - y = 0; - } - return { - height: h, - y : y - }; - } - - // Compute client bounds when resizing on the south border - constrainS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - return Math.max(0, this.drag.startHeight+coords.y-this.drag.clickY); - } - - // Compute client bounds when resizing on the west border - constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let delta = coords.x - this.drag.clickX; - let x = this.drag.startX + delta; - let w = this.drag.startWidth - delta; - let min = bndTitleBar.height * 4; - if (bndClient.right - bndDesktop.right > 0) { - min = Math.max(min, bndClient.right - - bndDesktop.right + bndTitleBar.height); - } - if (w < min) { - delta = min - w; - w += delta; - x -= delta; - } - return { - x : x, - width: w - }; - } - - // Resize on the east border - resizeE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - this.setSize( - this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), - this.drag.startHeight - ); - } - - // Resize on the north border - resizeN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let con = this.constrainN(coords, - bndClient, bndDesktop, bndWindow, bndTitleBar); - this.setBounds( - this.drag.startX , con.y, - this.drag.startWidth, con.height - ); - } - - // Resize on the northeast border - resizeNE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let con = this.constrainN(coords, - bndClient, bndDesktop, bndWindow, bndTitleBar); - this.setBounds( - this.drag.startX, con.y, - this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), - con.height - ); - } - - // Resize on the northwest border - resizeNW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let conN = this.constrainN(coords, - bndClient, bndDesktop, bndWindow, bndTitleBar); - let conW = this.constrainW(coords, - bndClient, bndDesktop, bndWindow, bndTitleBar); - this.setBounds(conW.x, conN.y, conW.width, conN.height); - } - - // Resize on the south border - resizeS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - this.setSize( - this.drag.startWidth, - this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), - ); - } - - // Resize on the southeast border - resizeSE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - this.setSize( - this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), - this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar) - ); - } - - // Resize on the southwest border - resizeSW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let con = this.constrainW(coords, - bndClient, bndDesktop, bndWindow, bndTitleBar); - this.setBounds( - con.x , this.drag.startY, - con.width, - this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar) - ); - } - - // Resize on the west border - resizeW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { - let con = this.constrainW(coords, - bndClient, bndDesktop, bndWindow, bndTitleBar); - this.setBounds( - con.x , this.drag.startY, - con.width, this.drag.startHeight - ); - } - -}; - - - -/////////////////////////////////////////////////////////////////////////////// -// Desktop // -/////////////////////////////////////////////////////////////////////////////// - -// Parent container for encapsulating groups of Windows -class Desktop extends Component { - - ///////////////////////// Initialization Methods ////////////////////////// - - constructor(gui, options) { - super(gui, options, { - className: "tk tk-desktop", - role : "group", - tagName : "div", - style : { - position: "relative" - } - }); - - // Configure event handlers - this.addEventListener("resize", e=>this.onResize(e)); - } - - - - ///////////////////////////// Event Handlers ////////////////////////////// - - // Element resized - onResize(e) { - for (let wnd of this.children) - wnd.contain(); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Re-order windows to bring a particular one to the foreground - bringToFront(wnd) { - - // The window is not a child of this Desktop - let index = this.children.indexOf(wnd); - if (index == -1) - return; - - // The window is already in the foreground - let afters = this.children.slice(index + 1).map(c=>c.element); - if (afters.length == 0) - return; - - // Record scroll pane positions - let scrolls = []; - for (let after of afters) - for (let scroll of - after.querySelectorAll(".tk-scrollpane > .tk-viewport")) { - scrolls.push({ - element: scroll, - left : scroll.scrollLeft, - top : scroll.scrollTop - }); - } - - // Update window collection - wnd.element.before(... this.children.slice(index+1).map(c=>c.element)); - this.children.splice(index, 1); - this.children.push(wnd); - - // Restore scroll pane positions - for (let scroll of scrolls) { - Object.assign(scroll.element, { - scrollLeft: scroll.left, - scrollTop : scroll.top - }); - } - - } - - // Position a window in the center of the viewable area - center(wnd) { - - // The window is not a child of the desktop pane - if (this.children.indexOf(wnd) == -1) - return; - - let bndDesktop = this.getBounds(); - let bndWindow = wnd .getBounds(); - wnd.setLocation( - Math.max(0, Math.round((bndDesktop.width - bndWindow.width) / 2)), - Math.max(0, Math.round((bndDesktop.height-bndWindow.height) / 2)) - ); - } - -}; - - - -export { Desktop, Window }; diff --git a/core/bus.c b/core/bus.c index 71bfeefbaa5bf45f3ae1b3c2f4c967d8ac2b53e9..3e37709ec69e820089c46ed4d70c6ee2ea53c61c 100644 GIT binary patch delta 900 zcmc&y-AV#M6fOwTMUn2JpbiMB+oYzTr1XRLK_S{rQM0?F4!OHAJ8M~l576blNO!$S zH}x>h%>HPXMbNpKbIzXqzVH0JZM_#>o4MK2|Jon&TOEHeAgnks_Kr-05CEXM8QF-zS;sq$i59uG8?tXZ-bE9@Al#@WSb`T2z)CzrJh+1 zK2ncpu.irq[x] = 0; - - /* Other registers (the hardware does not do this) */ - for (x = 0; x < 32; x++) - sim->cpu.program[x] = 0x00000000; - sim->cpu.adtre = 0x00000000; - sim->cpu.eipc = 0x00000000; - sim->cpu.eipsw = 0x00000000; - sim->cpu.fepc = 0x00000000; - sim->cpu.fepsw = 0x00000000; - sim->cpu.sr29 = 0x00000000; - sim->cpu.sr31 = 0x00000000; -} - -/* Determine the number of clocks before a break condititon could occur */ -static uint32_t cpuUntil(VB *sim, uint32_t clocks) { - - /* Cannot break */ - if ( - sim->cpu.state == CPU_HALTED || - sim->cpu.state == CPU_FATAL || ( - sim->onException == NULL && - sim->onExecute == NULL && - sim->onFetch == NULL && - sim->onRead == NULL && - sim->onWrite == NULL - )) return clocks; - - /* Will not break before next operation */ - return sim->cpu.clocks < clocks ? sim->cpu.clocks : clocks; -} - - - -#endif /* WSAPI */ +static void cpuReset(VB *sim) \ No newline at end of file diff --git a/core/vb.c b/core/vb.c index 6547a24..cc9dfa7 100644 --- a/core/vb.c +++ b/core/vb.c @@ -284,25 +284,4 @@ int vbSetROM(VB *sim, void *rom, uint32_t size) { } /* Supply an SRAM buffer */ -int vbSetSRAM(VB *sim, void *sram, uint32_t size) { - - /* Check the buffer size */ - if (size == 0 || ((size - 1) & size) != 0) - return 0; - - /* Configure the SRAM buffer */ - sim->cart.sram = (uint8_t *) sram; - sim->cart.sramSize = size; - return 1; -} - -/* Specify a new value for a system register */ -uint32_t vbSetSystemRegister(VB *sim, int id, uint32_t value) { - return cpuSetSystemRegister(sim, id, value, 1); -} - -/* Write a data unit to the bus */ -void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) { - if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES) - busWrite(sim, address, type, value, debug); -} +int vbSetSRAM(VB *sim, void *sram, uint32_t size) \ No newline at end of file diff --git a/core/vb.h b/core/vb.h index f4d40be28467a0481f42fbfcdd9d91b0c35ce793..421c2a9ed5542281158140346b5baa7330941c8f 100644 GIT binary patch delta 1022 hcmX?TxWRJ6L5X^Xfd!-XkB0w1hQL6lzp{bxJOE2b2UGw6 delta 7 OcmdmBdC+jfK?wj3U;||U diff --git a/makefile b/makefile index 5006fc9642bf0c6d86fc777857509f5d93bbb9ae..96a41435f673f8cc60b8d8acdd5d01dda0ff250f 100644 GIT binary patch delta 202 WcmX@X)5*8NgRP!nV1X0m)&l@kF#{_A delta 7 OcmeC=JHfNTgAD)**aB(* diff --git a/wasm/wasm.c b/wasm/wasm.c deleted file mode 100644 index bfddec2..0000000 --- a/wasm/wasm.c +++ /dev/null @@ -1,126 +0,0 @@ -#include -#include -#include -#include -#include -#include - - - -/////////////////////////////// Module Commands /////////////////////////////// - -// Allocate and initialize multiple simulations -EMSCRIPTEN_KEEPALIVE VB** Create(int count) { - VB **sims = malloc(count * sizeof (void *)); - for (int x = 0; x < count; x++) - vbReset(sims[x] = malloc(sizeof (VB))); - return sims; -} - -// Delete a simulation -EMSCRIPTEN_KEEPALIVE void Destroy(VB *sim) { - free(&sim->cart.rom); - free(&sim->cart.sram); - free(sim); -} - -// Proxy for free() -EMSCRIPTEN_KEEPALIVE void Free(void *ptr) { - free(ptr); -} - -// Proxy for malloc() -EMSCRIPTEN_KEEPALIVE void* Malloc(int size) { - return malloc(size); -} - -// Determine the size in bytes of a pointer -EMSCRIPTEN_KEEPALIVE int PointerSize() { - return sizeof (void *); -} - -// Read multiple bytes from the bus -EMSCRIPTEN_KEEPALIVE void ReadBuffer( - VB* sim, uint8_t *dest, uint32_t address, uint32_t size) { - for (; size > 0; address++, size--, dest++) - *dest = vbRead(sim, address, VB_U8, 1); -} - -// Supply a ROM buffer -EMSCRIPTEN_KEEPALIVE int SetROM(VB *sim, uint8_t *rom, uint32_t size) { - uint8_t *prev = vbGetROM(sim, NULL); - int ret = vbSetROM(sim, rom, size); - if (ret) { - free(prev); - vbReset(sim); - } - return ret; -} - -// Write multiple bytes to the bus -EMSCRIPTEN_KEEPALIVE void WriteBuffer( - VB* sim, uint8_t *src, uint32_t address, uint32_t size) { - for (; size > 0; address++, size--, src++) - vbWrite(sim, address, VB_U8, *src, 1); -} - - - -////////////////////////////// Debugger Commands ////////////////////////////// - -// Attempt to execute until the following instruction -static uint32_t RunNextPC; -static int RunNextProcB(VB *sim, int fetch, VB_ACCESS *acc) { - if (fetch == 0 && vbGetProgramCounter(sim) == RunNextPC) - return 1; - acc->value = vbRead(sim, acc->address, acc->type, 0); - return 0; -} -static int RunNextProcA(VB *sim, VB_INSTRUCTION *inst) { - RunNextPC = vbGetProgramCounter(sim) + inst->size; - vbSetCallback(sim, VB_ONEXECUTE, NULL); - vbSetCallback(sim, VB_ONFETCH, &RunNextProcB); - return 0; -} -EMSCRIPTEN_KEEPALIVE void RunNext(VB *sim0, VB *sim1) { - uint32_t clocks = 400000; // 1/50s - VB *sims[2]; - - vbSetCallback(sim0, VB_ONEXECUTE, &RunNextProcA); - - if (sim1 != NULL) { - sims[0] = sim0; - sims[1] = sim1; - vbEmulateMulti(sims, 2, &clocks); - } - - else vbEmulate(sim0, &clocks); - - vbSetCallback(sim0, VB_ONFETCH, NULL); -} - -// Execute one instruction -static uint32_t SingleStepPC; -static int SingleStepProc(VB *sim, int fetch, VB_ACCESS *acc) { - if (fetch == 0 && vbGetProgramCounter(sim) != SingleStepPC) - return 1; - acc->value = vbRead(sim, acc->address, acc->type, 0); - return 0; -} -EMSCRIPTEN_KEEPALIVE void SingleStep(VB *sim0, VB *sim1) { - uint32_t clocks = 400000; // 1/50s - VB *sims[2]; - - SingleStepPC = vbGetProgramCounter(sim0); - vbSetCallback(sim0, VB_ONFETCH, &SingleStepProc); - - if (sim1 != NULL) { - sims[0] = sim0; - sims[1] = sim1; - vbEmulateMulti(sims, 2, &clocks); - } - - else vbEmulate(sim0, &clocks); - - vbSetCallback(sim0, VB_ONFETCH, NULL); -} diff --git a/web/App.js b/web/App.js new file mode 100644 index 0000000..915e120 --- /dev/null +++ b/web/App.js @@ -0,0 +1,680 @@ +import { Core } from /**/"./core/Core.js"; +import { Debugger } from /**/"./debugger/Debugger.js"; +import { Disassembler } from /**/"./core/Disassembler.js"; +import { Toolkit } from /**/"./toolkit/Toolkit.js"; + +// Front-end emulator application +class App extends Toolkit.App { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(bundle) { + super({ + style: { + display : "grid", + gridTemplateRows: "max-content auto" + }, + visibility: true, + visible : false + }); + + // Configure instance fields + this.bundle = bundle; + this.debugMode = true; + this.dualMode = false; + this.text = null; + } + + async init() { + + // Theme + Object.assign(document.body.style, { margin:"0", overflow:"hidden" }); + this.stylesheet(/**/"web/theme/kiosk.css", false); + this.stylesheet(/**/"web/theme/vbemu.css", false); + this._theme = "auto"; + this.themes = { + dark : this.stylesheet(/**/"web/theme/dark.css" ), + light : this.stylesheet(/**/"web/theme/light.css" ), + virtual: this.stylesheet(/**/"web/theme/virtual.css") + }; + + // Watch for dark mode preference changes + this.isDark = window.matchMedia("(prefers-color-scheme: dark)"); + this.isDark.addEventListener("change", e=>this.onDark()); + this.onDark(); + + // Locales + await this.addLocale(/**/"web/locale/en-US.json"); + for (let id of [].concat(navigator.languages, ["en-US"])) { + if (this.setLocale(id)) + break; + } + this.setTitle("{app.title}", true); + + // Element + document.body.append(this.element); + window.addEventListener("resize", e=>{ + this.element.style.height = window.innerHeight + "px"; + this.element.style.width = window.innerWidth + "px"; + }); + window.dispatchEvent(new Event("resize")); + this.addEventListener("keydown", e=>this.onKeyDown(e)); + + // Menus + this.menuBar = new Toolkit.MenuBar(this); + this.menuBar.setLabel("{menu._}", true); + this.add(this.menuBar); + this.initFileMenu (); + this.initEmulationMenu(); + this.initDebugMenu (0, this.debugMode); + this.initDebugMenu (1, this.debugMode && this.dualMode); + this.initThemeMenu (); + + // Fallback for bubbled key events + document.body.addEventListener("focusout", e=>this.onBlur(e)); + window .addEventListener("keydown" , e=>this.onKey (e)); + window .addEventListener("keyup" , e=>this.onKey (e)); + + // Temporary: Faux game mode display + this.display = new Toolkit.Component(this, { + class : "tk display", + style : { position: "relative" }, + visible: !this.debugMode + }); + this.image1 = new Toolkit.Component(this, { style: { + background: "#000000", + position : "absolute" + }}); + this.display.add(this.image1); + this.image2 = new Toolkit.Component(this, { style: { + background: "#000000", + position : "absolute" + }}); + this.display.add(this.image2); + this.display.addEventListener("resize", e=>this.onDisplay()); + this.add(this.display); + + // Temporary: Faux debug mode display + this.desktop = new Toolkit.Desktop(this, { + visible: this.debugMode + }); + this.add(this.desktop); + + // Emulation core + this.core = await new Core().init(); + let sims = (await this.core.create(2)).sims; + this.core.onsubscription = (k,m)=>this.onSubscription(k, m); + + // Debugging managers + this.dasm = new Disassembler(); + this.debug = new Array(sims.length); + for (let x = 0; x < sims.length; x++) { + let dbg = this.debug[x] = new Debugger(this, sims[x], x); + if (x == 0 && !this.dualMode) { + dbg.cpu .substitute("#", ""); + dbg.memory.substitute("#", ""); + } + this.desktop.add(dbg.cpu); + this.desktop.add(dbg.memory); + } + + // Reveal the application + this.visible = true; + this.restoreFocus(); + } + + // Initialize File menu + initFileMenu() { + let bar = this.menuBar; + let item = bar.file = new Toolkit.MenuItem(this); + item.setText("{menu.file._}"); + bar.add(item); + + let menu = item.menu = new Toolkit.Menu(this); + + item = bar.file.loadROM0 = new Toolkit.MenuItem(this); + item.setText("{menu.file.loadROM}"); + item.substitute("#", this.dualMode ? " 1" : "", false); + item.addEventListener("action", e=>this.onLoadROM(0)); + menu.add(item); + + item = bar.file.loadROM1 = new Toolkit.MenuItem(this, + { visible: this.dualMode }); + item.setText("{menu.file.loadROM}"); + item.substitute("#", " 2", false); + item.addEventListener("action", e=>this.onLoadROM(1)); + menu.add(item); + + item = bar.file.dualMode = new Toolkit.MenuItem(this, + { checked: this.dualMode, type: "checkbox" }); + item.setText("{menu.file.dualMode}"); + item.addEventListener("action", e=>this.onDualMode()); + menu.add(item); + + item = bar.file.debugMode = new Toolkit.MenuItem(this, + { checked: this.debugMode, disabled: true, type: "checkbox" }); + item.setText("{menu.file.debugMode}"); + item.addEventListener("action", e=>this.onDebugMode()); + menu.add(item); + + menu.addSeparator(); + + item = new Toolkit.MenuItem(this); + item.setText("Export source...", false); + item.addEventListener("action", ()=>this.bundle.save()); + menu.add(item); + } + + // Initialize Emulation menu + initEmulationMenu() { + let bar = this.menuBar; + let item = bar.emulation = new Toolkit.MenuItem(this); + item.setText("{menu.emulation._}"); + bar.add(item); + + let menu = item.menu = new Toolkit.Menu(this); + + item = bar.emulation.run = + new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.emulation.run}"); + menu.add(item); + + item = bar.emulation.reset0 = new Toolkit.MenuItem(this); + item.setText("{menu.emulation.reset}"); + item.substitute("#", this.dualMode ? " 1" : "", false); + item.addEventListener("action", e=>this.onReset(0)); + menu.add(item); + + item = bar.emulation.reset1 = new Toolkit.MenuItem(this, + { visible: this.dualMode }); + item.setText("{menu.emulation.reset}"); + item.substitute("#", " 2", false); + item.addEventListener("action", e=>this.onReset(1)); + menu.add(item); + + item = bar.emulation.linkSims = new Toolkit.MenuItem(this, + { disabled: true, type: "checkbox", visible: this.dualMode }); + item.setText("{menu.emulation.linkSims}"); + menu.add(item); + } + + // Initialize Debug menu + initDebugMenu(index, visible) { + let bar = this.menuBar; + let item = bar["debug" + index] = new Toolkit.MenuItem(this, + { visible: visible }), top = item; + item.setText("{menu.debug._}"); + item.substitute("#", + index == 0 ? this.dualMode ? "1" : "" : " 2", false); + bar.add(item); + + let menu = item.menu = new Toolkit.Menu(this); + + item = top.console = new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.debug.console}"); + menu.add(item); + + item = top.memory = new Toolkit.MenuItem(this); + item.setText("{menu.debug.memory}"); + item.addEventListener("action", + e=>this.showWindow(this.debug[index].memory)); + menu.add(item); + + item = top.cpu = new Toolkit.MenuItem(this); + item.setText("{menu.debug.cpu}"); + item.addEventListener("action", + e=>this.showWindow(this.debug[index].cpu)); + menu.add(item); + + item=top.breakpoints = new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.debug.breakpoints}"); + menu.add(item); + + menu.addSeparator(); + + item = top.palettes = new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.debug.palettes}"); + menu.add(item); + + item = top.characters = new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.debug.characters}"); + menu.add(item); + + item = top.bgMaps = new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.debug.bgMaps}"); + menu.add(item); + + item = top.backgrounds = new Toolkit.MenuItem(this, { disabled:true }); + item.setText("{menu.debug.backgrounds}"); + menu.add(item); + + item = top.objects = new Toolkit.MenuItem(this, { disabled: true }); + item.setText("{menu.debug.objects}"); + menu.add(item); + + item = top.frameBuffers = new Toolkit.MenuItem(this, {disabled: true}); + item.setText("{menu.debug.frameBuffers}"); + menu.add(item); + } + + // Initialize Theme menu + initThemeMenu() { + let bar = this.menuBar; + let item = bar.theme = new Toolkit.MenuItem(this); + item.setText("{menu.theme._}"); + bar.add(item); + + let menu = item.menu = new Toolkit.Menu(this); + + item = bar.theme.auto = new Toolkit.MenuItem(this, + { checked: true, type: "checkbox" }); + item.setText("{menu.theme.auto}"); + item.theme = "auto"; + item.addEventListener("action", e=>this.theme = "auto"); + menu.add(item); + + item = bar.theme.light = new Toolkit.MenuItem(this, + { checked: false, type: "checkbox" }); + item.setText("{menu.theme.light}"); + item.theme = "light"; + item.addEventListener("action", e=>this.theme = "light"); + menu.add(item); + + item = bar.theme.dark = new Toolkit.MenuItem(this, + { checked: false, type: "checkbox" }); + item.setText("{menu.theme.dark}"); + item.theme = "dark"; + item.addEventListener("action", e=>this.theme = "dark"); + menu.add(item); + + item = bar.theme.light = new Toolkit.MenuItem(this, + { checked: false, type: "checkbox" }); + item.setText("{menu.theme.virtual}"); + item.theme = "virtual"; + item.addEventListener("action", e=>this.theme = "virtual"); + menu.add(item); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // All elements have lost focus + onBlur(e) { + if ( + e.relatedTarget == null || + e.relatedTarget == document.body + ) this.restoreFocus(); + } + + // Dark mode preference changed + onDark() { + if (this._theme != "auto") + return; + let isDark = this.isDark.matches; + this.themes.light.disabled = isDark; + this.themes.dark .disabled = !isDark; + } + + // Game mode display resized + onDisplay() { + let bounds = this.display.element.getBoundingClientRect(); + let width = Math.max(1, bounds.width); + let height = Math.max(1, bounds.height); + let scale, x1, y1, x2, y2; + + // Single mode + if (!this.dualMode) { + this.image2.visible = false; + scale = Math.max(1, Math.min( + Math.floor(width / 384), + Math.floor(height / 224) + )); + x1 = Math.max(0, Math.floor((width - 384 * scale) / 2)); + y1 = Math.max(0, Math.floor((height - 224 * scale) / 2)); + x2 = y2 = 0; + } + + // Dual mode + else { + this.image2.visible = true; + + // Horizontal orientation + if (true) { + scale = Math.max(1, Math.min( + Math.floor(width / 768), + Math.floor(height / 224) + )); + let gap = Math.max(0, width - 768 * scale); + gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0); + x1 = gap; + x2 = Math.max(384 * scale, width - 384 * scale - gap); + y1 = y2 = Math.max(0, Math.floor((height - 224 * scale) / 2)); + } + + // Vertical orientation + else { + scale = Math.max(1, Math.min( + Math.floor(width / 384), + Math.floor(height / 448) + )); + let gap = Math.max(0, height - 448 * scale); + gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0); + x1 = x2 = Math.max(0, Math.floor((width - 384 * scale) / 2)); + y1 = gap; + y2 = Math.max(224 * scale, height - 224 * scale - gap); + } + + } + + width = 384 * scale + "px"; + height = 224 * scale + "px"; + Object.assign(this.image1.element.style, + { left: x1+"px", top: y1+"px", width: width, height: height }); + Object.assign(this.image2.element.style, + { left: x2+"px", top: y2+"px", width: width, height: height }); + } + + // File -> Debug mode + onDebugMode() { + this.debugMode =!this.debugMode; + this.display.visible =!this.debugMode; + this.desktop.visible = this.debugMode; + this.configMenus(); + this.onDisplay(); + } + + // Emulation -> Dual mode + onDualMode() { + this.setDualMode(!this.dualMode); + this.configMenus(); + this.onDisplay(); + } + + // Key press + onKeyDown(e) { + + // Take no action + if (!e.altKey || e.key != "F10" || + this.menuBar.contains(document.activeElement)) + return; + + // Move focus into the menu bar + this.menuBar.focus(); + Toolkit.handle(e); + } + + // File -> Load ROM + async onLoadROM(index) { + + // Add a file picker to the document + let file = document.createElement("input"); + file.type = "file"; + file.style.position = "absolute"; + file.style.visibility = "hidden"; + document.body.appendChild(file); + + // Prompt the user to select a file + await new Promise(resolve=>{ + file.addEventListener("input", resolve); + file.click(); + }); + file.remove(); + + // No file was selected + file = file.files[0]; + if (!file) + return; + + // Load the file + let data = null; + try { data = new Uint8Array(await file.arrayBuffer()); } + catch { + alert(this.localize("{menu.file.loadROMError}")); + return; + } + + // Attempt to process the file as an ISX binary + try { data = Debugger.isx(data).toROM(); } catch { } + + // Error checking + if ( + data.length < 1024 || + data.length > 0x1000000 || + (data.length & data.length - 1) + ) { + alert(this.localize("{menu.file.loadROMInvalid}")); + return; + } + + // Load the ROM into simulation memory + let rep = await this.core.setROM(this.debug[index].sim, data, + { refresh: true }); + if (!rep.success) { + alert(this.localize("{menu.file.loadROMError}")); + return; + } + this.debug[index].followPC(0xFFFFFFF0); + } + + // Emulation -> Reset + async onReset(index) { + await this.core.reset(this.debug[index].sim, { refresh: true }); + this.debug[index].followPC(0xFFFFFFF0); + } + + // Core subscription + onSubscription(key, msg) { + let target = this.debug; // Handler object + for (let x = 1; x < key.length - 1; x++) + target = target[key[x]]; + target[key[key.length - 1]].call(target, msg); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Specify document title + setTitle(title, localize) { + this.setString("text", title, localize); + } + + // Specify the color theme + get theme() { return this._theme; } + set theme(theme) { + switch (theme) { + case "light": case "dark": case "virtual": + this._theme = theme; + for (let entry of Object.entries(this.themes)) + entry[1].disabled = entry[0] != theme; + break; + default: + this._theme = "auto"; + this.themes["virtual"].disabled = true; + this.onDark(); + } + for (let item of this.menuBar.theme.menu.children) + item.checked = item.theme == theme; + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Configure components for automatic localization, or localize a message + localize(a, b) { + + // Default behavior + if (a && a != this) + return super.localize(a, b); + + // Update localization strings + if (this.text != null) { + let text = this.text; + document.title = !text[1] ? text[0] : + this.localize(text[0], this.substitutions); + } + + } + + // Return focus to the most recent focused element + restoreFocus() { + + // Focus was successfully restored + if (super.restoreFocus()) + return true; + + // Select the foremost visible window + let wnd = this.desktop.getActiveWindow(); + if (wnd) { + wnd.focus(); + return true; + } + + // Select the menu bar + this.menuBar.focus(); + return true; + } + + // Perform a Run Next command on one of the simulations + runNext(index, options) { + let debugs = [ this.debug[index] ]; + if (this.dualMode) + debugs.push(this.debug[index ^ 1]); + + let ret = this.core.runNext(debugs.map(d=>d.sim), options); + + if (ret instanceof Promise) ret.then(msg=>{ + for (let x = 0; x < debugs.length; x++) + debugs[x].followPC(msg.pcs[x]); + }); + } + + // Perform a Single Step command on one of the simulations + singleStep(index, options) { + let debugs = [ this.debug[index] ]; + if (this.dualMode) + debugs.push(this.debug[index ^ 1]); + + let ret = this.core.singleStep(debugs.map(d=>d.sim), options); + + if (ret instanceof Promise) ret.then(msg=>{ + for (let x = 0; x < debugs.length; x++) + debugs[x].followPC(msg.pcs[x]); + }); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Configure menu item visibility + configMenus() { + let bar = this.menuBar; + bar.file.debugMode .checked = this.debugMode; + bar.file.loadROM1 .visible = this.dualMode; + bar.emulation.reset1 .visible = this.dualMode; + bar.emulation.linkSims.visible = this.dualMode; + bar.debug0 .visible = this.debugMode; + bar.debug1 .visible = this.debugMode && this.dualMode; + } + + // Specify whether dual mode is active + setDualMode(dualMode) { + + // Update state + if (dualMode == this.dualMode) + return; + this.dualMode = dualMode; + + // Working variables + let index = dualMode ? " 1" : ""; + + // Update menus + let bar = this.menuBar; + bar.file.loadROM0 .substitute("#", index, false); + bar.debug0 .substitute("#", index, false); + bar.emulation.reset0.substitute("#", index, false); + bar.file.dualMode .checked = this.dualMode; + + // Update sim 1 debug windows + let dbg = this.debug[0]; + dbg.cpu .substitute("#", index); + dbg.memory.substitute("#", index); + + // Re-show any sim 2 debug windows that were previously visible + if (dualMode) { + for (let wnd of this.desktop.children) { + if (wnd.index == 1 && wnd.wasVisible) + wnd.visible = true; + } + } + + // Hide any visible sim 2 debug windows + else for (let wnd of this.desktop.children) { + if (wnd.index == 0) + continue; + wnd.wasVisible = wnd.visible; + wnd.visible = false; + } + + } + + // Ensure a debugger window is visible + showWindow(wnd) { + let adjust = false; + + // The window is already visible + if (wnd.visible) { + + // The window is already in the foreground + if (wnd == this.desktop.getActiveWindow()) + ;//adjust = true; + + // Bring the window to the foreground + else this.desktop.bringToFront(wnd); + + } + + // The window is not visible + else { + adjust = !wnd.shown; + wnd.visible = true; + this.desktop.bringToFront(wnd); + } + + // Adjust the window position + if (adjust) { + let bounds = this.desktop.element.getBoundingClientRect(); + wnd.left = Math.max(0, (bounds.width - wnd.outerWidth ) / 2); + wnd.top = Math.max(0, (bounds.height - wnd.outerHeight) / 2); + } + + // Transfer focus into the window + wnd.focus(); + } + + // Install a stylesheet. Returns the resulting element + stylesheet(filename, disabled = true) { + let ret = document.createElement("link"); + ret.href = filename; + ret.rel = "stylesheet"; + ret.type = "text/css"; + ret.disabled = disabled; + document.head.append(ret); + return ret; + } + + + + ///////////////////////////// Program Methods ///////////////////////////// + + // Program entry point + static main(bundle) { + new App(bundle).init(); + } + +} + +export { App }; diff --git a/web/Bundle.java b/web/Bundle.java new file mode 100644 index 0000000..7f72e46 --- /dev/null +++ b/web/Bundle.java @@ -0,0 +1,203 @@ +import java.awt.image.*; +import java.io.*; +import java.nio.charset.*; +import java.util.*; +import javax.imageio.*; + +public class Bundle { + + /////////////////////////////// BundledFile /////////////////////////////// + + // Individual packaged resource file + static class BundledFile implements Comparable { + + // Instance fields + byte[] data; // File data loaded from disk + File file; // Source file + String filename; // Logical filename + + // Constructor + BundledFile(BundledFile parent, File file) { + + // Configure instance fields + this.file = file; + filename = parent == null ? "" : parent.filename + file.getName(); + + // Load file data if file + if (file.isFile()) { + try (var stream = new FileInputStream(file)) { + data = stream.readAllBytes(); + } catch (Exception e) { } + } + + // Update filename if directory + else if (parent != null) filename += "/"; + } + + // Comparator + public int compareTo(BundledFile o) { + return + filename.equals("web/_boot.js") ? -1 : + o.filename.equals("web/_boot.js") ? +1 : + filename.compareTo(o.filename) + ; + } + + // Produce a list of child files or directories + BundledFile[] listFiles(String name, boolean isDirectory) { + + // Produce a filtered list of files + var files = this.file.listFiles(f->{ + + // Does not satisfy the directory requirement + if (f.isDirectory() != isDirectory) + return false; + + // Current directory is not root + if (!filename.equals("")) + return true; + + // Filter specific files from being bundled + String filename = f.getName(); + return !( + filename.startsWith(".git" ) || + filename.startsWith(name + "_") && + filename.endsWith (".html" ) + ); + }); + + // Process all files for bundling + var ret = new BundledFile[files.length]; + for (int x = 0; x < files.length; x++) + ret[x] = new BundledFile(this, files[x]); + return ret; + } + + } + + + + ///////////////////////////// Program Methods ///////////////////////////// + + // Program entry point + public static void main(String[] args) { + String name = name(args[0]); + var files = listFiles(args[0]); + var prepend = prepend(name, files); + var bundle = bundle(prepend, files); + var image = image(bundle); + var url = url(image); + patch(name, url); + } + + // Produce a buffer of the bundled files + static byte[] bundle(byte[] prepend, BundledFile[] files) { + try (var stream = new ByteArrayOutputStream()) { + stream.write(prepend); + stream.write(files[0].data); // web/_boot.js + stream.write(0); + for (int x = 1; x < files.length; x++) + stream.write(files[x].data); + return stream.toByteArray(); + } catch (Exception e) { return null; } + } + + // Convert a bundle buffer into a PNG-encoded image buffer + static byte[] image(byte[] bundle) { + int width = (int) Math.ceil(Math.sqrt(bundle.length)); + int height = (bundle.length + width - 1) / width; + var pixels = new int[width * height]; + + // Encode the buffer as a pixel array + for (int x = 0; x < bundle.length; x++) { + int b = bundle[x] & 0xFF; + pixels[x] = 0xFF000000 | b << 16 | b << 8 | b; + } + + // Produce an image using the pixels + var image = new BufferedImage( + width, height, BufferedImage.TYPE_INT_RGB); + image.setRGB(0, 0, width, height, pixels, 0, width); + + // Encode the image as a PNG buffer + try (var stream = new ByteArrayOutputStream()) { + ImageIO.write(image, "png", stream); + return stream.toByteArray(); + } catch (Exception e) { return null; } + } + + // List all files + static BundledFile[] listFiles(String name) { + var dirs = new ArrayList(); + var files = new ArrayList(); + + // Propagate down the file system tree + dirs.add(new BundledFile(null, new File("."))); + while (!dirs.isEmpty()) { + var dir = dirs.remove(0); + for (var sub : dir.listFiles(name, true )) + dirs.add(sub ); + for (var file : dir.listFiles(name, false)) + files.add(file); + } + + // Return the set of files as a sorted array + Collections.sort(files); + return files.toArray(new BundledFile[files.size()]); + } + + // Generate a filename for the bundle + static String name(String name) { + var calendar = Calendar.getInstance(); + return String.format("%s_%04d%02d%02d", + name, + calendar.get(Calendar.YEAR), + calendar.get(Calendar.MONTH) + 1, + calendar.get(Calendar.DAY_OF_MONTH) + ); + } + + // Produce the output HTML from the template + static void patch(String name, String url) { + String markup = null; + try (var stream = new FileInputStream("web/template.html")) { + markup = new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (Exception e) { } + markup = markup.replace("src=\"\"", "src=\"" + url + "\""); + try (var stream = new FileOutputStream(name + ".html")) { + stream.write(markup.getBytes(StandardCharsets.UTF_8)); + } catch (Exception e) { } + } + + // Produce source data to prepend to web/_boot.js + static byte[] prepend(String name, BundledFile[] files) { + var ret = new StringBuilder(); + + // Arguments + ret.append("let " + + "buffer=arguments[0]," + + "image=arguments[1]," + + "name=\"" + name + "\"" + + ";"); + + // Bundle manifest + ret.append("let manifest=["); + for (var file : files) { + if (file != files[0]) + ret.append(","); + ret.append("[\"" + + file.filename + "\", " + file.data.length + "]"); + } + ret.append("];"); + + // Convert to byte array + return ret.toString().getBytes(StandardCharsets.UTF_8); + } + + // Convert an image buffer to a data URL + static String url(byte[] image) { + return "data:image/png;base64," + + Base64.getMimeEncoder(0, new byte[0]).encodeToString(image); + } + +} diff --git a/web/_boot.js b/web/_boot.js new file mode 100644 index 0000000..591d8ec --- /dev/null +++ b/web/_boot.js @@ -0,0 +1,310 @@ +// Running as an async function +// Prepended by Bundle.java: buffer, image, manifest, name + + + +/////////////////////////////////////////////////////////////////////////////// +// Bundle // +/////////////////////////////////////////////////////////////////////////////// + +// Resource manager for bundled files +class Bundle extends Array { + + //////////////////////////////// Constants //////////////////////////////// + + // .ZIP support + static CRC_LOOKUP = new Uint32Array(256); + + // Text processing + static DECODER = new TextDecoder(); + static ENCODER = new TextEncoder(); + + static initializer() { + + // Generate the CRC32 lookup table + for (let x = 0; x <= 255; x++) { + let l = x; + for (let j = 7; j >= 0; j--) + l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1))); + this.CRC_LOOKUP[x] = l; + } + + } + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(name, url, settings, isDebug) { + super(); + this.isDebug = isDebug; + this.name = name; + this.settings = settings; + this.url = url; + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Export all bundled resources to a .ZIP file + save() { + let centrals = new Array(this.length); + let locals = new Array(this.length); + let offset = 0; + let size = 0; + + // Encode file and directory entries + for (let x = 0; x < this.length; x++) { + let file = this[x]; + let sum = Bundle.crc32(file.data); + locals [x] = file.toZipHeader(sum); + centrals[x] = file.toZipHeader(sum, offset); + offset += locals [x].length; + size += centrals[x].length; + } + + // Encode end of central directory + let end = []; + Bundle.writeInt(end, 4, 0x06054B50); // Signature + Bundle.writeInt(end, 2, 0); // Disk number + Bundle.writeInt(end, 2, 0); // Central dir start disk + Bundle.writeInt(end, 2, this.length); // # central dir this disk + Bundle.writeInt(end, 2, this.length); // # central dir total + Bundle.writeInt(end, 4, size); // Size of central dir + Bundle.writeInt(end, 4, offset); // Offset of central dir + Bundle.writeInt(end, 2, 0); // .ZIP comment length + + // Prompt the user to save the resulting file + let a = document.createElement("a"); + a.download = this.name + ".zip"; + a.href = URL.createObjectURL(new Blob( + locals.concat(centrals).concat([Uint8Array.from(end)]), + { type: "application/zip" } + )); + a.style.visibility = "hidden"; + document.body.appendChild(a); + a.click(); + a.remove(); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Add a BundledFile to the collection + add(file) { + file.bundle = this; + this.push(this[file.name] = file); + } + + // Write a byte array into an output buffer + static writeBytes(data, bytes) { + for (let b of bytes) + data.push(b); + } + + // Write an integer into an output buffer + static writeInt(data, size, value) { + for (; size > 0; size--, value >>= 8) + data.push(value & 0xFF); + } + + // Write a string of text as bytes into an output buffer + static writeString(data, text) { + this.writeBytes(data, this.ENCODER.encode(text)); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Calculate the CRC32 checksum for a byte array + static crc32(data) { + let c = 0xFFFFFFFF; + for (let x = 0; x < data.length; x++) + c = ((c >>> 8) ^ this.CRC_LOOKUP[(c ^ data[x]) & 0xFF]); + return ~c & 0xFFFFFFFF; + } + +} +Bundle.initializer(); + + + +/////////////////////////////////////////////////////////////////////////////// +// BundledFile // +/////////////////////////////////////////////////////////////////////////////// + +// Individual file in the bundled data +class BundledFile { + + //////////////////////////////// Constants //////////////////////////////// + + // MIME types + static MIMES = { + ".css" : "text/css;charset=UTF-8", + ".frag" : "text/plain;charset=UTF-8", + ".js" : "text/javascript;charset=UTF-8", + ".json" : "application/json;charset=UTF-8", + ".png" : "image/png", + ".svg" : "image/svg+xml;charset=UTF-8", + ".txt" : "text/plain;charset=UTF-8", + ".vert" : "text/plain;charset=UTF-8", + ".wasm" : "application/wasm", + ".woff2": "font/woff2" + }; + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(name, buffer, offset, length) { + + // Configure instance fields + this.data = buffer.slice(offset, offset + length); + this.name = name; + + // Resolve the MIME type + let index = name.lastIndexOf("."); + this.mime = index != -1 && BundledFile.MIMES[name.substring(index)] || + "application/octet-stream"; + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Represent the file with a blob URL + toBlobURL() { + return this.blobURL || (this.blobURL = URL.createObjectURL( + new Blob([ this.data ], { type: this.mime }))); + } + + // Encode the file data as a data URL + toDataURL() { + return "data:" + this.mime + ";base64," + btoa( + Array.from(this.data).map(b=>String.fromCharCode(b)).join("")); + } + + // Pre-process URLs in a bundled file's contents + toProcURL(asDataURL = false) { + + // The URL has already been computed + if (this.url) + return this.url; + + // Working variables + let content = this.toString(); + let pattern = /\/\*\*?\*\//g; + let parts = content.split(pattern); + let ret = [ parts.shift() ]; + + // Process all URLs prefixed with /**/ or /***/ + for (let part of parts) { + let start = part.indexOf("\""); + let end = part.indexOf("\"", start + 1); + let filename = part.substring(start + 1, end); + let asData = pattern.exec(content)[0] == "/***/"; + + // Relative to current file + if (filename.startsWith(".")) { + let path = this.name.split("/"); + path.pop(); // Current filename + + // Navigate to the path of the target file + for (let dir of filename.split("/")) { + switch (dir) { + case "..": path.pop(); // Fallthrough + case "." : break; + default : path.push(dir); + } + } + + // Produce the fully-qualified filename + filename = path.join("/"); + } + + // Append the file as a data URL + let file = this.bundle[filename]; + ret.push( + part.substring(0, start + 1), + file[ + file.mime.startsWith("text/javascript") || + file.mime.startsWith("text/css") ? + "toProcURL" : asData ? "toDataURL" : "toBlobURL" + ](asData), + part.substring(end) + ); + } + + // Represent the transformed source as a URL + return this.url = asDataURL ? + "data:" + this.mime + ";base64," + btoa(ret.join("")) : + URL.createObjectURL(new Blob(ret, { type: this.mime })) + ; + } + + // Decode the file data as a UTF-8 string + toString() { + return Bundle.DECODER.decode(this.data); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Produce a .ZIP header for export + toZipHeader(crc32, offset) { + let central = offset !== undefined; + let ret = []; + if (central) { + Bundle.writeInt (ret, 4, 0x02014B50); // Signature + Bundle.writeInt (ret, 2, 20); // Version created by + } else + Bundle.writeInt (ret, 4, 0x04034B50); // Signature + Bundle.writeInt (ret, 2, 20); // Version required + Bundle.writeInt (ret, 2, 0); // Bit flags + Bundle.writeInt (ret, 2, 0); // Compression method + Bundle.writeInt (ret, 2, 0); // Modified time + Bundle.writeInt (ret, 2, 0); // Modified date + Bundle.writeInt (ret, 4, crc32); // Checksum + Bundle.writeInt (ret, 4, this.data.length); // Compressed size + Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size + Bundle.writeInt (ret, 2, this.name.length); // Filename length + Bundle.writeInt (ret, 2, 0); // Extra field length + if (central) { + Bundle.writeInt (ret, 2, 0); // File comment length + Bundle.writeInt (ret, 2, 0); // Disk number start + Bundle.writeInt (ret, 2, 0); // Internal attributes + Bundle.writeInt (ret, 4, 0); // External attributes + Bundle.writeInt (ret, 4, offset); // Relative offset + } + Bundle.writeString (ret, this.name); // Filename + if (!central) + Bundle.writeBytes(ret, this.data); // File data + return Uint8Array.from(ret); + } + +} + + + +/////////////////////////////////////////////////////////////////////////////// +// Boot Program // +/////////////////////////////////////////////////////////////////////////////// + +// Produce the application bundle +let bundle = new Bundle(name, image.src, image.getAttribute("settings") || "", + location.protocol != "file:" && location.hash == "#debug"); +for (let x=0,offset=buffer.indexOf(0)-manifest[0][1]; xthis.init(m.data); + } + + async init(main) { + + // Configure message ports + this.core = this.port; + this.core.onmessage = m=>this.onCore(m.data); + this.main = main; + this.main.onmessage = m=>this.onMain(m.data); + + // Notify main thread + this.port.postMessage(0); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Produce output samples (called by the user agent) + process(inputs, outputs, parameters) { + let output = outputs[0]; + let length = output [0].length; + let empty = null; + + // Process all samples + for (let x = 0; x < length;) { + + // No bufferfed samples are available + if (this.buffers.length == 0) { + for (; x < length; x++) + output[0] = output[1] = 0; + break; + } + + // Transfer samples from the oldest buffer + let y, buffer = this.buffers[0]; + for (y = this.offset; x < length && y < buffer.length; x++, y+=2) { + output[0][x] = buffer[y ]; + output[1][x] = buffer[y + 1]; + } + + // Advance to the next buffer + if ((this.offset = y) == buffer.length) { + if (empty == null) + empty = []; + empty.push(this.buffers.shift()); + this.offset = 0; + } + + } + + // Return emptied sample buffers to the core thread + if (empty != null) + this.core.postMessage(empty, empty.map(e=>e.buffer)); + + return true; + } + + + + ///////////////////////////// Message Methods ///////////////////////////// + + // Message received from core thread + onCore(msg) { + } + + // Message received from main thread + onMain(msg) { + } + +} +registerProcessor("AudioThread", AudioThread); diff --git a/web/core/Core.js b/web/core/Core.js new file mode 100644 index 0000000..b441a47 --- /dev/null +++ b/web/core/Core.js @@ -0,0 +1,293 @@ +// Interface between application and WebAssembly worker thread +class Core { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor() { + + // Configure instance fields + this.promises = []; + + } + + async init(coreUrl, wasmUrl, audioUrl) { + + // Open audio output stream + this.audio = new AudioContext({ + latencyHint: "interactive", + sampleRate : 41700 + }); + await this.audio.suspend(); + + // Launch the audio thread + await this.audio.audioWorklet.addModule( + Core.url(audioUrl, "AudioThread.js", /***/"./AudioThread.js")); + let node = new AudioWorkletNode(this.audio, "AudioThread", { + numberOfInputs : 0, + numberOfOutputs : 1, + outputChannelCount: [2] + }); + node.connect(this.audio.destination); + + // Attach a second MessagePort to the audio thread + let channel = new MessageChannel(); + this.audio.port = channel.port1; + await new Promise(resolve=>{ + node.port.onmessage = resolve; + node.port.postMessage(channel.port2, [channel.port2]); + }); + this.audio.port.onmessage = m=>this.onAudio(m.data); + + // Launch the core thread + this.core = new Worker( + Core.url(wasmUrl, "CoreThread.js", /***/"./CoreThread.js")); + await new Promise(resolve=>{ + this.core.onmessage = resolve; + this.core.postMessage({ + audio : node.port, + wasmUrl: Core.url(wasmUrl, "core.wasm", /***/"./core.wasm") + }, [node.port]); + }); + this.core.onmessage = m=>this.onCore(m.data); + + return this; + } + + + + ///////////////////////////// Static Methods ////////////////////////////// + + // Select a URL in the same path as the current script + static url(arg, name, bundled) { + + // The input argument was provided + if (arg) + return arg; + + // Running from a bundle distribution + if (bundled.startsWith("blob:") || bundled.startsWith("data:")) + return bundled; + + // Compute the URL for the given filename + let url = new URL(import.meta.url).pathname; + return url.substring(0, url.lastIndexOf("/") + 1) + name; + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Message received from audio thread + onAudio(msg) { + } + + // Message received from core thread + onCore(msg) { + + // Process subscriptions + if (msg.subscriptions && this.onsubscription instanceof Function) { + for (let sub of msg.subscriptions) { + let key = sub.subscription; + delete sub.subscription; + this.onsubscription(key, sub, this); + } + delete msg.subscriptions; + } + + // The main thread is waiting on a reply + if (msg.isReply) { + delete msg.isReply; + + // For "create", produce sim objects + if (msg.isCreate) { + delete msg.isCreate; + msg.sims = msg.sims.map(s=>({ pointer: s })); + } + + // Notify the caller + this.promises.shift()(msg); + } + + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Create and initialize simulations + create(count, options) { + return this.message({ + command: "create", + count : count + }, [], options); + } + + // Delete a simulation + delete(sim, options) { + return this.message({ + command: "delete", + sim : sim.pointer + }, [], options); + } + + // Retrieve the value of all CPU registers + getAllRegisters(sim, options) { + return this.message({ + command: "getAllRegisters", + sim : sim.pointer + }, [], options); + } + + // Retrieve the value of PC + getProgramCounter(sim, options) { + return this.message({ + command: "getProgramCounter", + sim : sim.pointer + }, [], options); + } + + // Retrieve the value of a system register + getSystemRegister(sim, id, options) { + return this.message({ + command: "getSystemRegister", + id : id, + sim : sim.pointer + }, [], options); + } + + // Read multiple bytes from memory + read(sim, address, length, options) { + return this.message({ + command: "read", + address: address, + length : length, + sim : sim.pointer + }, [], options); + } + + // Refresh subscriptions + refresh(subscriptions = null, options) { + return this.message({ + command : "refresh", + subscriptions: subscriptions + }, [], options); + } + + // Simulate a hardware reset + reset(sim, options) { + return this.message({ + command: "reset", + sim : sim.pointer + }, [], options); + } + + // Execute until the next current instruction + runNext(sims, options) { + return this.message({ + command: "runNext", + sims : Array.isArray(sims) ? + sims.map(s=>s.pointer) : [ sims.pointer ] + }, [], options); + } + + // Specify a value for the program counter + setProgramCounter(sim, value, options) { + return this.message({ + command: "setProgramCounter", + sim : sim.pointer, + value : value + }, [], options); + } + + // Specify a value for a program register + setProgramRegister(sim, index, value, options) { + return this.message({ + command: "setProgramRegister", + index : index, + sim : sim.pointer, + value : value + }, [], options); + } + + // Specify a cartridge ROM buffer + setROM(sim, data, options = {}) { + data = data.slice(); + return this.message({ + command: "setROM", + data : data, + reset : !("reset" in options) || !!options.reset, + sim : sim.pointer + }, [data.buffer], options); + } + + // Specify a value for a system register + setSystemRegister(sim, id, value, options) { + return this.message({ + command: "setSystemRegister", + id : id, + sim : sim.pointer, + value : value + }, [], options); + } + + // Execute the current instruction + singleStep(sims, options) { + return this.message({ + command: "singleStep", + sims : Array.isArray(sims) ? + sims.map(s=>s.pointer) : [ sims.pointer ] + }, [], options); + } + + // Cancel a subscription + unsubscribe(subscription, options) { + return this.message({ + command : "unsubscribe", + subscription: subscription + }, [], options); + } + + // Write multiple bytes to memory + write(sim, address, data, options) { + data = data.slice(); + return this.message({ + address: address, + command: "write", + data : data, + sim : sim.pointer + }, [data.buffer], options); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Send a message to the core thread + message(msg, transfers, options = {}) { + + // Configure options + if (!(options instanceof Object)) + options = { reply: options }; + if (!("reply" in options) || options.reply) + msg.reply = true; + if ("refresh" in options) + msg.refresh = options.refresh; + if ("subscription" in options) + msg.subscription = options.subscription; + if ("tag" in options) + msg.tag = options.tag; + + // Send the command to the core thread + return msg.reply ? + new Promise(resolve=>{ + this.promises.push(resolve); + this.core.postMessage(msg, transfers); + }) : + this.core.postMessage(msg, transfers); + ; + + } + +} + +export { Core }; diff --git a/web/core/CoreThread.js b/web/core/CoreThread.js new file mode 100644 index 0000000..484b095 --- /dev/null +++ b/web/core/CoreThread.js @@ -0,0 +1,330 @@ +"use strict"; + +// Dedicated emulation thread +class CoreThread { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor() { + + // Configure instance fields + this.subscriptions = new Map(); + + // Wait for initializer message from parent thread + onmessage = m=>this.init(m.data.audio, m.data.wasmUrl); + } + + async init(audio, wasmUrl) { + + // Configure message ports + this.audio = audio; + this.audio.onmessage = m=>this.onAudio (m.data); + this.main = globalThis; + this.main .onmessage = m=>this.onMessage(m.data); + + // Load and instantiate the WebAssembly module + this.wasm = (await WebAssembly.instantiateStreaming( + fetch(wasmUrl), { + env: { emscripten_notify_memory_growth: ()=>this.onGrowth() } + })).instance; + this.onGrowth(); + this.pointerSize = this.PointerSize(); + this.pointerType = this.pointerSize == 8 ? Uint64Array : Uint32Array; + + // Notify main thread + this.main.postMessage(0); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Message received from audio thread + onAudio(frames) { + + // Audio processing was suspended + if (frames == 0) { + return; + } + + // Wait for more frames + this.audio.postMessage(0); + } + + // Emscripten has grown the linear memory + onGrowth() { + Object.assign(this, this.wasm.exports); + } + + // Message received from main thread + onMessage(msg) { + + // Subscribe to the command + if (msg.subscription && msg.command != "refresh") + this.subscriptions.set(CoreThread.key(msg.subscription), msg); + + // Process the command + let rep = this[msg.command](msg); + + // Do not send a reply + if (!msg.reply) + return; + + // Configure the reply + if (!rep) + rep = {}; + if (msg.reply) + rep.isReply = true; + if ("tag" in msg) + rep.tag = msg.tag; + + // Send the reply to the main thread + let transfers = rep.transfers; + if (transfers) + delete rep.transfers; + this.main.postMessage(rep, transfers || []); + + // Refresh subscriptions + if (msg.refresh && msg.command != "refresh") { + let subs = {}; + if (Array.isArray(msg.refresh)) + subs.subscriptions = msg.refresh; + this.refresh(subs); + } + + } + + + + //////////////////////////////// Commands ///////////////////////////////// + + // Create and initialize a new simulation + create(msg) { + let sims = new Array(msg.count); + for (let x = 0; x < msg.count; x++) + sims[x] = this.Create(); + return { + isCreate: true, + sims : sims + }; + } + + // Delete all memory used by a simulation + delete(msg) { + this.Delete(msg.sim); + } + + // Retrieve the values of all CPU registers + getAllRegisters(msg) { + let program = new Int32Array (32); + let system = new Uint32Array(32); + for (let x = 0; x < 32; x++) { + program[x] = this.vbGetProgramRegister(msg.sim, x); + system [x] = this.vbGetSystemRegister (msg.sim, x); + } + return { + pc : this.vbGetProgramCounter(msg.sim) >>> 0, + program : program, + system : system, + transfers: [ program.buffer, system.buffer ] + }; + } + + // Retrieve the value of PC + getProgramCounter(msg) { + return { value: this.vbGetProgramCounter(msg.sim) >>> 0 }; + } + + // Retrieve the value of a system register + getSystemRegister(msg) { + return { value: this.vbGetSystemRegister(msg.sim, msg.id) >>> 0 }; + } + + // Read multiple bytes from memory + read(msg) { + let buffer = this.malloc(msg.length); + this.vbReadEx(msg.sim, msg.address, buffer.pointer, msg.length); + let data = buffer.slice(); + this.free(buffer); + return { + address : msg.address, + data : data, + transfers: [data.buffer] + }; + } + + // Process subscriptions + refresh(msg) { + let subscriptions = []; + let transfers = []; + + // Select the key set to refresh + let keys = Array.isArray(msg.subscriptions) ? + msg.subscriptions.map(s=>CoreThread.key(s)) : + this.subscriptions.keys() + ; + + // Process all subscriptions + for (let key of keys) { + + // Process the subscription + let sub = this.subscriptions.get(key); + let rep = this[sub.command](sub); + + // There is no result + if (!rep) + continue; + + // Add the result to the response + rep.subscription = sub.subscription; + if ("tag" in sub) + rep.tag = sub.tag; + subscriptions.push(rep); + + // Add the transfers to the response + if (!rep.transfers) + continue; + transfers = transfers.concat(rep.transfers); + delete rep.transfers; + } + + // Do not send a reply + if (subscriptions.length == 0 && !msg.reply) + return; + + // Send the response to the main thread + this.main.postMessage({ + isReply : !!msg.reply, + subscriptions: subscriptions.sort(CoreThread.REFRESH_ORDER) + }, transfers); + } + + // Simulate a hardware reset + reset(msg) { + this.vbReset(msg.sim); + } + + // Execute until the next current instruction + runNext(msg) { + let sims = this.malloc(msg.sims.length, true); + for (let x = 0; x < msg.sims.length; x++) + sims[x] = msg.sims[x]; + this.RunNext(sims.pointer, msg.sims.length); + this.free(sims); + + let pcs = new Array(msg.sims.length); + for (let x = 0; x < msg.sims.length; x++) + pcs[x] = this.vbGetProgramCounter(msg.sims[x]) >>> 0; + + return { pcs: pcs }; + } + + // Specify a value for the program counter + setProgramCounter(msg) { + return { value: this.vbSetProgramCounter(msg.sim, msg.value) >>> 0 }; + } + + // Specify a value for a program register + setProgramRegister(msg) { + return {value:this.vbSetProgramRegister(msg.sim,msg.index,msg.value)}; + } + + // Specify a cartridge ROM buffer + setROM(msg) { + let prev = this.vbGetROM(msg.sim, 0); + let success = true; + + // Specify a new ROM + if (msg.data != null) { + let data = this.malloc(msg.data.length); + for (let x = 0; x < data.length; x++) + data[x] = msg.data[x]; + success = !this.vbSetROM(msg.sim, data.pointer, data.length); + } + + // Operation was successful + if (success) { + + // Delete the previous ROM + this.Free(prev); + + // Reset the simulation + if (msg.reset) + this.vbReset(msg.sim); + } + + return { success: success }; + } + + // Specify a value for a system register + setSystemRegister(msg) { + return {value:this.vbSetSystemRegister(msg.sim,msg.id,msg.value)>>>0}; + } + + // Execute the current instruction + singleStep(msg) { + let sims = this.malloc(msg.sims.length, true); + for (let x = 0; x < msg.sims.length; x++) + sims[x] = msg.sims[x]; + this.SingleStep(sims.pointer, msg.sims.length); + this.free(sims); + + let pcs = new Array(msg.sims.length); + for (let x = 0; x < msg.sims.length; x++) + pcs[x] = this.vbGetProgramCounter(msg.sims[x]) >>> 0; + + return { pcs: pcs }; + } + + // Delete a subscription + unsubscribe(msg) { + this.subscriptions.delete(CoreThread.key(msg.subscription)); + } + + // Write multiple bytes to memory + write(msg) { + let data = this.malloc(msg.data.length); + for (let x = 0; x < data.length; x++) + data[x] = msg.data[x]; + this.vbWriteEx(msg.sim, msg.address, data.pointer, data.length); + this.free(data); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Delete a byte array in WebAssembly memory + free(buffer) { + this.Free(buffer.pointer); + } + + // Format a subscription key as a string + static key(subscription) { + return subscription.map(k=>k.toString()).join("\n"); + } + + // Allocate a byte array in WebAssembly memory + malloc(length, pointers = false) { + let size = pointers ? length * this.pointerSize : length; + return this.map(this.Malloc(size), length, pointers); + } + + // Map a typed array into WebAssembly memory + map(address, length, pointers = false) { + let ret = new (pointers ? this.pointerType : Uint8Array) + (this.memory.buffer, address, length); + ret.pointer = address; + return ret; + } + + // Comparator for subscriptions within the refresh command + static REFRESH_ORDER(a, b) { + a = a.subscription[0]; + b = b.subscription[0]; + return a < b ? -1 : a > b ? 1 : 0; + } + +} + +new CoreThread(); diff --git a/web/core/Disassembler.js b/web/core/Disassembler.js new file mode 100644 index 0000000..9c2a459 --- /dev/null +++ b/web/core/Disassembler.js @@ -0,0 +1,542 @@ +// Machine code to human readable text converter +class Disassembler { + + //////////////////////////////// Constants //////////////////////////////// + + // Default settings + static DEFAULTS = { + condCL : "L", // Use C/NC or L/NL for conditions + condEZ : "E", // Use E/NE or Z/NZ for conditions + condNames : true, // Use condition names + condUppercase: false, // Condition names uppercase + hexPrefix : "0x", // Hexadecimal prefix + hexSuffix : "", // Hexadecimal suffix + hexUppercase : true, // Hexadecimal uppercase + instUppercase: true, // Mnemonics uppercase + jumpAddress : true, // Jump/branch shows target address + memInside : false, // Use [reg1 + disp] notation + opDestFirst : false, // Destination operand first + proNames : true, // Use program register names + proUppercase : false, // Program register names uppercase + splitBcond : false, // BCOND condition as an operand + splitSetf : true, // SETF condition as an operand + sysNames : true, // Use system register names + sysUppercase : false // System register names uppercase + }; + + + + /////////////////////////// Disassembly Lookup //////////////////////////// + + // Opcode descriptors + static OPDEFS = [ + [ "MOV" , [ "opReg1" , "opReg2" ] ], // 000000 + [ "ADD" , [ "opReg1" , "opReg2" ] ], + [ "SUB" , [ "opReg1" , "opReg2" ] ], + [ "CMP" , [ "opReg1" , "opReg2" ] ], + [ "SHL" , [ "opReg1" , "opReg2" ] ], + [ "SHR" , [ "opReg1" , "opReg2" ] ], + [ "JMP" , [ "opReg1Ind" ] ], + [ "SAR" , [ "opReg1" , "opReg2" ] ], + [ "MUL" , [ "opReg1" , "opReg2" ] ], + [ "DIV" , [ "opReg1" , "opReg2" ] ], + [ "MULU" , [ "opReg1" , "opReg2" ] ], + [ "DIVU" , [ "opReg1" , "opReg2" ] ], + [ "OR" , [ "opReg1" , "opReg2" ] ], + [ "AND" , [ "opReg1" , "opReg2" ] ], + [ "XOR" , [ "opReg1" , "opReg2" ] ], + [ "NOT" , [ "opReg1" , "opReg2" ] ], + [ "MOV" , [ "opImm5S", "opReg2" ] ], // 010000 + [ "ADD" , [ "opImm5S", "opReg2" ] ], + null, // SETF: special + [ "CMP" , [ "opImm5S", "opReg2" ] ], + [ "SHL" , [ "opImm5U", "opReg2" ] ], + [ "SHR" , [ "opImm5U", "opReg2" ] ], + [ "CLI" , [ ] ], + [ "SAR" , [ "opImm5U", "opReg2" ] ], + [ "TRAP" , [ "opImm5U" ] ], + [ "RETI" , [ ] ], + [ "HALT" , [ ] ], + null, // Invalid + [ "LDSR" , [ "opReg2" , "opSys" ] ], + [ "STSR" , [ "opSys" , "opReg2" ] ], + [ "SEI" , [ ] ], + null, // Bit string: special + null, // BCOND: special // 100000 + null, // BCOND: special + null, // BCOND: special + null, // BCOND: special + null, // BCOND: special + null, // BCOND: special + null, // BCOND: special + null, // BCOND: special + [ "MOVEA", [ "opImm16U" , "opReg1", "opReg2" ] ], + [ "ADDI" , [ "opImm16S" , "opReg1", "opReg2" ] ], + [ "JR" , [ "opDisp26" ] ], + [ "JAL" , [ "opDisp26" ] ], + [ "ORI" , [ "opImm16U" , "opReg1", "opReg2" ] ], + [ "ANDI" , [ "opImm16U" , "opReg1", "opReg2" ] ], + [ "XORI" , [ "opImm16U" , "opReg1", "opReg2" ] ], + [ "MOVHI", [ "opImm16U" , "opReg1", "opReg2" ] ], + [ "LD.B" , [ "opReg1Disp", "opReg2" ] ], // 110000 + [ "LD.H" , [ "opReg1Disp", "opReg2" ] ], + null, // Invalid + [ "LD.W" , [ "opReg1Disp", "opReg2" ] ], + [ "ST.B" , [ "opReg2" , "opReg1Disp" ] ], + [ "ST.H" , [ "opReg2" , "opReg1Disp" ] ], + null, // Invalid + [ "ST.W" , [ "opReg2" , "opReg1Disp" ] ], + [ "IN.B" , [ "opReg1Disp", "opReg2" ] ], + [ "IN.H" , [ "opReg1Disp", "opReg2" ] ], + [ "CAXI" , [ "opReg1Disp", "opReg2" ] ], + [ "IN.W" , [ "opReg1Disp", "opReg2" ] ], + [ "OUT.B", [ "opReg2" , "opReg1Disp" ] ], + [ "OUT.H", [ "opReg2" , "opReg1Disp" ] ], + null, // Floating-point/Nintendo: special + [ "OUT.W", [ "opReg2" , "opReg1Disp" ] ] + ]; + + // Bit string sub-opcode descriptors + static BITSTRING = [ + "SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD", + null , null , null , null , + "ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" , + "ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" , + null , null , null , null , + null , null , null , null , + null , null , null , null , + null , null , null , null + ]; + + // Floating-point/Nintendo sub-opcode descriptors + static FLOATENDO = [ + [ "CMPF.S" , [ "opReg1", "opReg2" ] ], + null, // Invalid + [ "CVT.WS" , [ "opReg1", "opReg2" ] ], + [ "CVT.SW" , [ "opReg1", "opReg2" ] ], + [ "ADDF.S" , [ "opReg1", "opReg2" ] ], + [ "SUBF.S" , [ "opReg1", "opReg2" ] ], + [ "MULF.S" , [ "opReg1", "opReg2" ] ], + [ "DIVF.S" , [ "opReg1", "opReg2" ] ], + [ "XB" , [ "opReg2" ] ], + [ "XH" , [ "opReg2" ] ], + [ "REV" , [ "opReg1", "opReg2" ] ], + [ "TRNC.SW", [ "opReg1", "opReg2" ] ], + [ "MPYHW" , [ "opReg1", "opReg2" ] ], + null, null, null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null, + null, null, null, null, null, null, null, null + ]; + + // Condition mnemonics + static CONDITIONS = [ + "V" , "C" , "E" , "NH", "N", "T", "LT", "LE", + "NV", "NC", "NE", "H" , "P", "F", "GE", "GT" + ]; + + // Program register names + static PRONAMES = [ + "r0" , "r1" , "hp" , "sp" , "gp" , "tp" , "r6" , "r7" , + "r8" , "r9" , "r10", "r11", "r12", "r13", "r14", "r15", + "r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23", + "r24", "r25", "r26", "r27", "r28", "r29", "r30", "lp" + ]; + + // System register names + static SYSNAMES = [ + "EIPC", "EIPSW", "FEPC", "FEPSW", "ECR", "PSW", "PIR", "TKCW", + "8" , "9" , "10" , "11" , "12" , "13" , "14" , "15" , + "16" , "17" , "18" , "19" , "20" , "21" , "22" , "23" , + "CHCW", "ADTRE", "26" , "27" , "28" , "29" , "30" , "31" + ]; + + + + ///////////////////////////// Static Methods ////////////////////////////// + + // Determine the bounds of a data buffer to represent all lines of output + static dataBounds(address, line, length) { + let before = 10; // Number of lines before the first line of output + let max = 4; // Maximum number of bytes that can appear on a line + + // The reference line is before the preferred earliest line + if (line < -before) { + length = (length - line) * max; + } + + // The reference line is before the first line + else if (line < 0) { + address -= (line + before) * max; + length = (length + before) * max; + } + + // The reference line is at or after the first line + else { + address -= (line + before) * max; + length = (Math.max(length, line) + before) * max; + } + + return { + address: (address & ~1) >>> 0, + length : length + }; + } + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor() { + Object.assign(this, Disassembler.DEFAULTS); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Disassemble a region of memory + disassemble(data, dataAddress, refAddress, refLine, length, pc = null) { + let pcOffset = pc === null ? -1 : pc - dataAddress >>> 0; + + // Locate the offset of the first line of output in the buffer + let offset = 0; + for (let + addr = dataAddress, + circle = refLine > 0 ? new Array(refLine) : null, + index = 0, + more = [], + remain = null + ;;) { + + // Determine the size of the current line + if (more.length == 0) + this.more(more, data, offset); + let size = more.shift(); + + // The current line contains the reference address + if (refAddress - addr >>> 0 < size) { + + // The next item in the buffer is the first line of output + if (refLine > 0) { + offset = circle[index]; + break; + } + + // This line is the first line of output + if (refLine == 0) + break; + + // Count more lines for the first line of output + remain = refLine; + } + + // Record the offset of the current instruction + if (refLine > 0) { + circle[index] = offset; + index = (index + 1) % circle.length; + } + + // Advance to the next line + let sizeToPC = pcOffset - offset >>> 0; + if (offset != pcOffset && sizeToPC < size) { + size = sizeToPC; + more.splice(); + } + addr = addr + size >>> 0; + offset += size; + if (remain !== null && ++remain == 0) + break; // The next line is the first line of output + } + + // Process all lines of output + let lines = new Array(length); + for (let + addr = dataAddress + offset, + more = [], + x = 0; + x < length; x++ + ) { + + // Determine the size of the current line + if (more.length == 0) + this.more(more, data, offset, pcOffset); + let size = more.shift(); + + // Add the line to the response + lines[x] = this.format({ + rawAddress: addr, + rawBytes : data.slice(offset, offset + size) + }); + + // Advance to the next line + let sizeToPC = pcOffset - offset >>> 0; + if (offset != pcOffset && sizeToPC < size) { + size = sizeToPC; + more.splice(); + } + addr = addr + size >>> 0; + offset += size; + } + + return lines; + } + + + + /////////////////////////// Formatting Methods //////////////////////////// + + // Format a line as human-readable text + format(line) { + let canReverse = true; + let opcode = line.rawBytes[1] >>> 2; + let opdef; + let code = [ + line.rawBytes[1] << 8 | line.rawBytes[0], + line.rawBytes.length == 2 ? null : + line.rawBytes[3] << 8 | line.rawBytes[2] + ]; + + // BCOND + if ((opcode & 0b111000) == 0b100000) { + let cond = code[0] >>> 9 & 15; + opdef = + cond == 13 ? [ "NOP", [ ] ] : + this.splitBcond ? [ "BCOND", [ "opBCond", "opDisp9" ] ] : + [ + cond == 5 ? "BR" : "B" + this.condition(cond, true), + [ "opDisp9" ] + ] + ; + canReverse = false; + } + + // Processing by opcode + else switch (opcode) { + + // SETF + case 0b010010: + opdef = !this.splitSetf ? + [ + "SETF" + Disassembler.CONDITIONS[code[0] & 15], + [ "opReg2" ] + ] : + [ "SETF", [ "opCond", "opReg2" ] ] + ; + break; + + // Bit string + case 0b011111: + opdef = Disassembler.BITSTRING[code[0] & 31]; + if (opdef != null) + opdef = [ opdef, [] ]; + break; + + // Floating-point/Nintendo + case 0b111110: + opdef = Disassembler.FLOATENDO[code[1] >>> 10]; + break; + + // All others + default: opdef = Disassembler.OPDEFS[opcode]; + } + + // The opcode is undefined + if (opdef == null) + opdef = [ "---", [] ]; + + // Format the line's display text + line.address = this.hex(line.rawAddress, 8, false); + line.bytes = new Array(line.rawBytes.length); + line.mnemonic = this.instUppercase ? opdef[0] : opdef[0].toLowerCase(); + line.operands = new Array(opdef[1].length); + for (let x = 0; x < line.bytes.length; x++) + line.bytes[x] = this.hex(line.rawBytes[x], 2, false); + for (let x = 0; x < line.operands.length; x++) + line.operands[x] = this[opdef[1][x]](line, code); + if (this.opDestFirst && canReverse) + line.operands.reverse(); + + return line; + } + + // Format a condition operand in a BCOND instruction + opBCond(line, code) { + return this.condition(code[0] >>> 9 & 15); + } + + // Format a condition operand in a SETF instruction + opCond(line, code) { + return this.condition(code[0] & 15); + } + + // Format a 9-bit displacement operand + opDisp9(line, code) { + let disp = code[0] << 23 >> 23; + return this.jump(line.rawAddress, disp); + } + + // Format a 26-bit displacement operand + opDisp26(line, code) { + let disp = (code[0] << 16 | code[1]) << 6 >> 6; + return this.jump(line.rawAddress, disp); + } + + // Format a 5-bit signed immediate operand + opImm5S(line, code) { + return (code[0] & 31) << 27 >> 27; + } + + // Format a 5-bit unsigned immediate operand + opImm5U(line, code) { + return code[0] & 31; + } + + // Format a 16-bit signed immediate operand + opImm16S(line, code) { + let ret = code[1] << 16 >> 16; + return ( + ret < -256 ? "-" + this.hex(-ret) : + ret > 256 ? this.hex( ret) : + ret + ); + } + + // Format a 16-bit unsigned immediate operand + opImm16U(line, code) { + return this.hex(code[1], 4); + } + + // Format a Reg1 operand + opReg1(line, code) { + return this.programRegister(code[0] & 31); + } + + // Format a disp[reg1] operand + opReg1Disp(line, code) { + let disp = code[1] << 16 >> 16; + let reg1 = this.programRegister(code[0] & 31); + + // Do not print the displacement + if (disp == 0) + return "[" + reg1 + "]"; + + // Format the displacement amount + disp = + disp < -256 ? "-" + this.hex(-disp) : + disp > 256 ? this.hex( disp) : + disp.toString() + ; + + // [reg1 + disp] notation + if (this.memInside) { + return "[" + reg1 + (disp.startsWith("-") ? + " - " + disp.substring(1) : + " + " + disp + ) + "]"; + } + + // disp[reg1] notation + return disp + "[" + reg1 + "]"; + } + + // Format a [Reg1] operand + opReg1Ind(line, code) { + return "[" + this.programRegister(code[0] & 31) + "]"; + } + + // Format a Reg2 operand + opReg2(line, code) { + return this.programRegister(code[0] >> 5 & 31); + } + + // Format a system register operand + opSys(line, code) { + return this.systemRegister(code[0] & 31); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Select the mnemonic for a condition + condition(index, forceUppercase = false) { + if (!this.condNames) + return index.toString(); + let ret = + index == 1 ? this.condCL : + index == 2 ? this.condEZ : + index == 9 ? "N" + this.condCL : + index == 10 ? "N" + this.condEZ : + Disassembler.CONDITIONS[index] + ; + if (!forceUppercase && !this.condUppercase) + ret = ret.toLowerCase(); + return ret; + } + + // Format a number as a hexadecimal string + hex(value, digits = null, decorated = true) { + value = value.toString(16); + if (this.hexUppercase) + value = value.toUpperCase(); + if (digits != null) + value = value.padStart(digits, "0"); + if (decorated) { + value = this.hexPrefix + value + this.hexSuffix; + if (this.hexPrefix == "" && "0123456789".indexOf(value[0]) == -1) + value = "0" + value; + } + return value; + } + + // Format a jump or branch destination + jump(address, disp) { + return ( + this.jumpAddress ? + this.hex(address + disp >>> 0, 8, false) : + disp < -256 ? "-" + this.hex(-disp) : + disp > 256 ? "+" + this.hex( disp) : + disp.toString() + ); + } + + // Determine the number of bytes in the next line(s) of disassembly + more(more, data, offset) { + + // Error checking + if (offset + 1 >= data.length) + throw new Error("Disassembly error: Unexpected EoF"); + + // Determine the instruction's size from its opcode + let opcode = data[offset + 1] >>> 2; + more.push( + opcode < 0b101000 || // 16-bit instruction + opcode == 0b110010 || // Illegal opcode + opcode == 0b110110 // Illegal opcode + ? 2 : 4); + } + + // Format a program register + programRegister(index) { + let ret = this.proNames ? Disassembler.PRONAMES[index] : "r" + index; + if (this.proUppercase) + ret = ret.toUpperCase(); + return ret; + } + + // Format a system register + systemRegister(index) { + let ret = this.sysNames ? + Disassembler.SYSNAMES[index] : index.toString(); + if (!this.sysUppercase && this.sysNames) + ret = ret.toLowerCase(); + return ret; + } + +} + +export { Disassembler }; diff --git a/web/core/wasm.c b/web/core/wasm.c new file mode 100644 index 0000000..31fc35c --- /dev/null +++ b/web/core/wasm.c @@ -0,0 +1,78 @@ +#undef VBAPI +#include +#include +#include + + + +/////////////////////////////// Module Commands /////////////////////////////// + +// Create and initialize a new simulation +EMSCRIPTEN_KEEPALIVE VB* Create() { + VB *vb = malloc(sizeof (VB)); + vbInit(vb); + return vb; +} + +// Delete all memory used by a simulation +EMSCRIPTEN_KEEPALIVE void Delete(VB *vb) { + free(vb->cart.ram); + free(vb->cart.rom); + free(vb); +} + +// Proxy for free() +EMSCRIPTEN_KEEPALIVE void Free(void *ptr) { + free(ptr); +} + +// Proxy for malloc() +EMSCRIPTEN_KEEPALIVE void* Malloc(int size) { + return malloc(size); +} + +// Size in bytes of a pointer +EMSCRIPTEN_KEEPALIVE int PointerSize() { + return sizeof (void *); +} + + + +////////////////////////////// Debugger Commands ////////////////////////////// + +// Execute until the following instruction +uint32_t RunNextAddress; +static int RunNextFetch(VB *vb, int fetch, VBAccess *access) { + return access->address == RunNextAddress; +} +static int RunNextExecute(VB *vb, VBInstruction *inst) { + RunNextAddress = inst->address + inst->size; + vbSetOnExecute(vb, NULL); + vbSetOnFetch(vb, &RunNextFetch); + return 0; +} +EMSCRIPTEN_KEEPALIVE void RunNext(VB **vbs, int count) { + uint32_t clocks = 20000000; // 1s + vbSetOnExecute(vbs[0], &RunNextExecute); + vbEmulateEx (vbs, count, &clocks); + vbSetOnExecute(vbs[0], NULL); + vbSetOnFetch (vbs[0], NULL); +} + +// Execute the current instruction +static int SingleStepBreak; +static int SingleStepFetch(VB *vb, int fetch, VBAccess *access) { + if (fetch != 0) + return 0; + if (SingleStepBreak == 1) + return 1; + SingleStepBreak = 1; + return 0; +} +EMSCRIPTEN_KEEPALIVE void SingleStep(VB **vbs, int count) { + uint32_t clocks = 20000000; // 1s + SingleStepBreak = vbs[0]->cpu.stage == 0 ? 0 : 1; + vbSetOnFetch(vbs[0], &SingleStepFetch); + vbEmulateEx (vbs, count, &clocks); + vbSetOnFetch(vbs[0], NULL); +} diff --git a/web/debugger/CPU.js b/web/debugger/CPU.js new file mode 100644 index 0000000..f95d009 --- /dev/null +++ b/web/debugger/CPU.js @@ -0,0 +1,1442 @@ +import { Disassembler } from /**/"../core/Disassembler.js"; +import { Toolkit } from /**/"../toolkit/Toolkit.js"; +let register = Debugger => { + +// CPU disassembler and register state window +class CPU extends Toolkit.Window { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(debug, index) { + super(debug.app, { + class: "tk window cpu" + }); + + // Configure instance fields + this.debug = debug; + this.height = 300; + this.index = index; + this.shown = false; + this.width = 400; + + // Window + this.setTitle("{debug.cpu._}", true); + this.substitute("#", " " + (index + 1)); + if (index == 1) + this.element.classList.add("two"); + this.addEventListener("close" , e=>this.visible = false); + this.addEventListener("visibility", e=>this.onVisibility(e)); + + // Client area + Object.assign(this.client.style, { + display : "grid", + gridAutoRows : "100%", + gridTemplateColumns: "auto" + }); + + // Disassembler + this.disassembler = new DisassemblerPane(this); + this.lastFocus = this.disassembler.view; + + // Register lists + this.registers = new RegisterPane(this); + + // Main content area + this.add(new Toolkit.SplitPane(debug.app, { + orientation: "right", + primary : this.registers, + secondary : this.disassembler + })); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Window key press + onKeyDown(e) { + super.onKeyDown(e); + + // Error checking + if (e.altKey || e.shiftKey) + return; + + // Processing by key: CTRL down + if (e.ctrlKey) switch (e.key) { + case "b": case "B": + this.disassembler.bytesColumn = !this.disassembler.bytesColumn; + break; + case "f": case "F": + this.disassembler.fitColumns(); + break; + case "g": case "G": + this.disassembler.goto(); + return; + } + + // Processing by key: CTRL up + else switch (e.key) { + case "F10": + this.debug.app.runNext(this.index, { refresh: true }); + break; + case "F11": + this.debug.app.singleStep(this.index, { refresh: true }); + break; + default: return; + } + + Toolkit.handle(e); + } + + // Window visibility + onVisibility(e) { + let firstShow = !this.shown && e.visible; + + // Configure instance fields + this.shown = this.shown || e.visible; + + // Window visible for the first time + if (firstShow) { + this.disassembler.firstShow(); + this.registers .firstShow(); + } + + // Configure subscriptions + if (!e.visible) { + if (this.registers) + this.registers .unsubscribe(); + if (this.disassembler) + this.disassembler.unsubscribe(); + } else { + this.registers .fetch(); + this.disassembler.fetch(); + } + + } + +} + + + +/////////////////////////////////////////////////////////////////////////////// + + + +class DisassemblerPane extends Toolkit.ScrollPane { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(cpu) { + super(cpu.debug.app, { + class : "tk scroll-pane scr-dasm", + overflowX: "auto", + overflowY: "hidden" + }); + + // Configure instance fields + this._bytesColumn = true; + this.columnWidths = [ 0, 0, 0, 0, 0, 0 ]; + this.cpu = cpu; + this.delta = 0; + this.dasm = null; + this.lines = []; + this.pending = false; + this.subscription = [ 1, cpu.index, "cpu", "disassembler", "refresh" ], + this.viewAddress = 0xFFFFFFF0; + this.viewLine = 10; + + // Client area + let view = this.view = new Toolkit.Component(cpu.debug.app, { + class : "tk mono disassembler", + role : "application", + tabIndex: "0", + style : { + display : "grid", + height : "100%", + minWidth: "100%", + overflow: "hidden", + position: "relative", + width : "max-content" + } + }); + view.setLabel("{debug.cpu.disassembler}", true); + view.setRoleDescription("{debug.cpu.disassembler}", true); + view.addEventListener("keydown", e=>this.viewKeyDown(e)); + view.addEventListener("resize" , e=>this.viewResize ( )); + view.addEventListener("wheel" , e=>this.viewWheel (e)); + + // Label for measuring text dimensions in the disassembler + this.sizer = new Toolkit.Label(cpu.debug.app, { + class : "tk label mono", + visible : false, + visibility: true, + style: { + position: "absolute" + } + }); + this.sizer.setText("\u00a0", false); //   + view.append(this.sizer); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Column resized + colResize() { + if (this.lines.length == 0) + return; + for (let x = 0; x < this.columnWidths.length; x++) { + let elm = this.lines[0].all[x]; + let width = elm.getBoundingClientRect().width; + if (width <= this.columnWidths[x]) + continue; + this.columnWidths[x] = width; + elm.style.minWidth = width + "px"; + } + } + + // Key press + viewKeyDown(e) { + + // Error checking + if (e.altKey || e.ctrlKey || e.shiftKey) + return; + + // Processing by key + switch (e.key) { + case "ArrowDown": + this.fetch(-1); + break; + case "ArrowLeft": + this.scrollLeft -= this.hscroll.unitIncrement; + break; + case "ArrowRight": + this.scrollLeft += this.hscroll.unitIncrement; + break; + case "ArrowUp": + this.fetch(+1); + break; + case "PageDown": + this.fetch(-this.tall(true)); + break; + case "PageUp": + this.fetch(+this.tall(true)); + break; + default: return; + } + + Toolkit.handle(e); + } + + // Resize + viewResize() { + + // Error checking + if (!this.sizer) + return; + + // Working variables + let tall = this.tall(false) + 1; + let grew = this.lines.length < tall; + + // Process all new lines + for (let y = this.lines.length; y < tall; y++) { + let first = y == 0 ? " first" : ""; + let resizer = y != 0 ? null : + new ResizeObserver(()=>this.colResize()); + + let line = { + lblAddress : document.createElement("div"), + lblBytes : [], + lblMnemonic: document.createElement("div"), + lblOperands: document.createElement("div"), + spacer : document.createElement("div") + }; + + // Address label + line.lblAddress.className = "addr" + first; + if (y == 0) + resizer.observe(line.lblAddress); + this.view.append(line.lblAddress); + + // Byte labels + for (let x = 0; x < 4; x++) { + let lbl = line.lblBytes[x] = document.createElement("div"); + lbl.className = "byte" + first + (x == 0 ? " b0" : ""); + if (y == 0) { + lbl.style.minWidth = "0px"; + resizer.observe(lbl); + } + this.view.append(lbl); + } + + // Mnemonic label + line.lblMnemonic.className = "inst" + first; + if (y == 0) + resizer.observe(line.lblMnemonic); + this.view.append(line.lblMnemonic); + + // Operand label + line.lblOperands.className = "ops" + first; + this.view.append(line.lblOperands); + + // Growing spacer + line.spacer.className = "spacer" + first; + this.view.append(line.spacer); + + // All elements + line.all = line.lblBytes.concat([ + line.lblAddress, + line.lblMnemonic, + line.lblOperands, + line.spacer + ]); + + this.lines.push(line); + } + + // Remove lines that are no longer visible + while (tall < this.lines.length) { + let line = this.lines[tall]; + line.lblAddress .remove(); + line.lblMnemonic.remove(); + line.lblOperands.remove(); + line.spacer .remove(); + for (let lbl of line.lblBytes) + lbl.remove(); + this.lines.splice(tall, 1); + } + + // Configure scroll bar + this.hscroll.unitIncrement = + this.sizer.element.getBoundingClientRect().height; + + // Update components + if (grew) + this.fetch(); + else this.refresh(); + } + + // Mouse wheel + viewWheel(e) { + + // Error checking + if (e.altKey || e.ctrlKey || e.shiftKey) + return; + + // Always handle the event + Toolkit.handle(e); + + // Determine how many full lines were scrolled + let scr = Debugger.linesScrolled(e, + this.sizer.element.getBoundingClientRect().height, + this.tall(true), + this.delta + ); + this.delta = scr.delta; + scr.lines = Math.max(-3, Math.min(3, scr.lines)); + + // No lines were scrolled + if (scr.lines == 0) + return; + + // Scroll the view + this.fetch(-scr.lines); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Display the bytes column in the disassembler + get bytesColumn() { return this._bytesColumn; } + set bytesColumn(show) { + show = !!show; + if (show == this._bytesColumn) + return; + this._bytesColumn = show; + this.refresh(); + } + + // Retrieve a disassembly from the simulation state + async fetch(viewScroll = 0) { + + // Select the parameters for the simulation fetch + let params = { + viewAddress: this.viewAddress, + viewLine : this.viewLine + viewScroll, + viewLength : this.tall(false) + 20, + viewScroll : viewScroll + }; + if (this.pending instanceof Object) { + params.viewLine += this.pending.viewScroll; + params.viewScroll += this.pending.viewScroll; + } + let bounds = Disassembler.dataBounds( + params.viewAddress, params.viewLine, params.viewLength); + params.dataAddress = bounds.address; + params.dataLength = bounds.length; + + // A communication with the core thread is already underway + if (this.pending) { + this.pending = params; + this.refresh(); + return; + } + + // Retrieve data from the simulation state + this.pending = params; + for (let data=null, promise=null; this.pending instanceof Object;) { + + // Wait for a transaction to complete + if (promise != null) { + this.pending = true; + data = await promise; + promise = null; + } + + // Initiate a new transaction + if (this.pending instanceof Object) { + params = this.pending; + let options = { tag: params }; + if (this.cpu.isVisible()) + options.subscription = this.subscription; + promise = this.cpu.debug.core.read(this.cpu.debug.sim, + params.dataAddress, params.dataLength, options); + } + + // Process the result of a transaction + if (data != null) { + this.refresh(data); + data = null; + } + + }; + this.pending = false; + } + + // Component is being displayed for the first time + firstShow() { + this.viewLine = Math.floor(this.tall(true) / 3) + 10; + } + + // Shrink all columns to fit the current view + fitColumns() { + if (this.lines.length == 0) + return; + for (let elm of this.lines[0].all) + elm.style.removeProperty("min-width"); + for (let x = 0; x < this.columnWidths.length; x++) + this.columnWidths[x] = 0; + this.refresh(); + } + + // Ensure PC is visible in the view + followPC(pc = null) { + let tall = this.tall(false); + let count = !this.dasm ? 0 : Math.min(this.dasm.length - 10, tall); + + // Determine whether PC already is visible + for (let x = 0; x < count; x++) { + let line = this.dasm[x + 10]; + if (pc - line.rawAddress >>> 0 < line.bytes.length) + return; // PC is already visible + } + + // Request a new view containing PC + this.viewAddress = pc; + this.viewLine = Math.floor(tall / 3) + 10; + if (this.cpu.isVisible()) + this.fetch(); + } + + // Prompt the user to navigate to a new editing address + goto() { + + // Retrieve the value from the user + let addr = prompt(this.app.localize("{debug.cpu.goto}")); + if (addr === null) + return; + addr = parseInt(addr.trim(), 16); + if ( + !Number.isInteger(addr) || + addr < 0 || + addr > 4294967295 + ) return; + + // Navigate to the given address + this.viewAddress = addr; + this.viewLine = Math.floor(this.tall(true) / 3) + 10; + this.fetch(); + } + + // Update localization strings + localize() { + this.localizeRoleDescription(); + this.localizeLabel(); + } + + // Update disassembler + refresh(msg = null) { + let tall = this.tall(false); + + // Receiving data from the simulation state + if (msg != null) { + + // Disassemble the retrieved data + this.dasm = this.cpu.debug.dasm.disassemble( + msg.data, msg.address, + msg.tag.viewAddress, msg.tag.viewLine, msg.tag.viewLength, + this.cpu.registers.pc + ); + + // Configure the view + this.viewAddress = this.dasm[10].rawAddress; + this.viewLine = 10; + if (this.pending instanceof Object) + this.viewLine += this.pending.viewScroll; + } + + // Determine an initial number of visible byte columns + let showBytes = 0; + if (this.bytesColumn) + for (let x = 0; x<4 && this.columnWidths[x]!=0; x++, showBytes++); + + // Process all lines + let index = 20 - this.viewLine; + for (let y = 0; y < this.lines.length; y++, index++) { + let line = this.lines[y]; + let method = "remove"; + + // There is no data for this line + if (index < 0 || this.dasm == null || index >= this.dasm.length) { + line.lblAddress .innerText = "--------"; + line.lblBytes[0].innerText = "--"; + line.lblMnemonic.innerText = "---"; + line.lblOperands.innerText = ""; + for (let x = 1; x < 4; x++) + line.lblBytes[x].innerText = ""; + if (this.bytesColumn) + showBytes = Math.max(showBytes, 1); + } + + // Present the disassembled line + else { + let dasm = this.dasm[index]; + line.lblAddress .innerText = dasm.address; + line.lblMnemonic.innerText = dasm.mnemonic; + line.lblOperands.innerText = dasm.operands.join(", "); + for (let x = 0; x < 4; x++) + line.lblBytes[x].innerText = dasm.bytes[x] || ""; + if (this.cpu.registers.pc == dasm.rawAddress) + method = "add"; + if (this.bytesColumn) + showBytes = Math.max(showBytes, dasm.bytes.length); + } + + // Configure whether PC is on this line + for (let elm of line.all) + elm.classList[method]("pc"); + } + + // Configure which byte columns are visible + for (let line of this.lines) { + for (let x = 0; x < 4; x++) { + line.lblBytes[x].style + [x < showBytes ? "removeProperty" : "setProperty"] + ("display", "none") + ; + } + } + + // Configure layout + this.view.element.style.gridTemplateColumns = + "repeat(" + (showBytes + 3) + ", max-content) auto"; + } + + // Stop receiving updates from the simulation + unsubscribe() { + this.cpu.debug.core.unsubscribe(this.subscription, false); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Measure the number of lines visible in the view + tall(fully = null) { + return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"]( + this.view .element.getBoundingClientRect().height / + this.sizer.element.getBoundingClientRect().height + )); + } + +} + + + +/////////////////////////////////////////////////////////////////////////////// + + + +// Entry in the register lists +class Register { + + //////////////////////////////// Constants //////////////////////////////// + + // Register types + static PROGRAM = 0; + static PLAIN = 1; + static CHCW = 2; + static ECR = 3; + static PSW = 4; + static PIR = 5; + static TKCW = 6; + + // Program register formats + static HEX = 0; + static SIGNED = 1; + static UNSIGNED = 2; + static FLOAT = 3; + + // Expansion controls by register type + static FIELDS = { + [this.CHCW]: [ + [ "check", "ICE", 1 ] + ], + [this.ECR]: [ + [ "texth", "FECC", 16, 16 ], + [ "texth", "EICC", 0, 16 ] + ], + [this.PIR]: [ + [ "texth", "PT", 0, 16 ] + ], + [this.PSW]: [ + [ "check", "CY" , 3 ], + [ "check", "FRO", 9 ], + [ "check", "OV" , 2 ], + [ "check", "FIV", 8 ], + [ "check", "S" , 1 ], + [ "check", "FZD", 7 ], + [ "check", "Z" , 0 ], + [ "check", "FOV", 6 ], + [ "check", "NP" , 15 ], + [ "check", "FUD", 5 ], + [ "check", "EP" , 14 ], + [ "check", "FPR", 4 ], + [ "check", "ID" , 12 ], + [ "textd", "I" , 16, 4 ], + [ "check", "AE" , 13 ] + ], + [this.TKCW]: [ + [ "check", "FIT", 7 ], + [ "check", "FUT", 4 ], + [ "check", "FZT", 6 ], + [ "check", "FPT", 3 ], + [ "check", "FVT", 5 ], + [ "check", "OTM", 8 ], + [ "check", "RDI", 2 ], + [ "textd", "RD" , 0, 2 ] + ] + }; + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(registers, key, type) { + let app = registers.cpu.debug.app; + + // Configure instance fields + this.controls = []; + this.dasm = registers.cpu.debug.app.dasm; + this.debug = registers.cpu.debug; + this.expansion = null; + this.registers = registers; + this.target = registers; + this.type = type; + + // Resolve the register reference + key = key.slice(); + while (key.length != 1) + this.target = this.target[key.shift()]; + this.key = key[0]; + + // Main controls + this.main = new Toolkit.Component(app, { + class: "main", + style: { + alignItems : "center", + display : "grid", + gridTemplateColumns: "max-content auto" + } + }); + + // Expand/collapse button + this.btnExpand = new Toolkit.Component(app, { + class: "tk expand", + style: { + alignItems : "center", + display : "grid", + gridTemplateColumns: "max-content auto" + } + }); + this.main.add(this.btnExpand); + + // Expand/collapse icon + this.icon = new Toolkit.Component(app, { class: "icon" }); + this.btnExpand.add(this.icon); + + // Register name + this.label = new Toolkit.Label(app, { + class: "label", + id : Toolkit.id() + }); + this.btnExpand.add(this.label); + + // Value text box + this.txtValue = new Toolkit.TextBox(app, { + class : "tk text-box mono", + spellcheck: "false", + size : "1", + value : "00000000" + }); + this.txtValue.setLabel(this.label); + this.txtValue.addEventListener("action" , e=>this.valAction (e)); + this.txtValue.addEventListener("keydown", e=>this.valKeyDown(e)); + this.main.add(this.txtValue); + + // Expansion area + if (type == Register.PROGRAM) + this.initProgram(app); + else this.initSystem(app, Register.FIELDS[type]); + if (this.expansion != null) { + + // Expand/collapse button + this.btnExpand.element.setAttribute("aria-expanded", "false" ); + this.btnExpand.element.setAttribute("tabindex" , "0" ); + this.btnExpand.element.setAttribute("role" , "button"); + this.btnExpand.element + .setAttribute("aria-controls", this.expansion.element.id); + + // Expansion area + this.btnExpand.addEventListener("keydown", + e=>this.expKeyDown (e)); + this.btnExpand.addEventListener("pointerdown", + e=>this.expPointerDown(e)); + this.expansion.visible = false; + } + + // Update controls + this.refresh(); + + // PSW is initially expanded + if (type == Register.PSW && key == 5) + this.expanded = true; + + // System registers after PSW are initially hidden + else if (key != "pc" && key != 5 && type != Register.PROGRAM) { + this.main.style.position = "absolute"; + this.main.style.visibility = "hidden"; + } + + } + + // Expansion controls for program registers + initProgram(app) { + + // Expansion area + let exp = this.expansion = new Toolkit.Component(app, { + class: "expansion", + id : Toolkit.id(), + style: { + display : "inline-grid", + gridTemplateColumns: "max-content", + } + }); + exp.localize = ()=>exp.localizeLabel(); + exp.setLabel("{debug.cpu.format}", true); + + // Radio group + let group = new Toolkit.RadioGroup(app); + this.format = Register.HEX; + + // Hex radio button + let opt = new Toolkit.Radio(app, { + checked: true, + group : group + }); + opt.setText("{debug.cpu.hex}", true); + opt.addEventListener("input", e=>this.onProgram(Register.HEX)); + exp.add(opt); + + // Signed radio button + opt = new Toolkit.Radio(app, { group: group }); + opt.setText("{debug.cpu.signed}", true); + opt.addEventListener("input", e=>this.onProgram(Register.SIGNED)); + exp.add(opt); + + // Unsigned radio button + opt = new Toolkit.Radio(app, { group: group }); + opt.setText("{debug.cpu.unsigned}", true); + opt.addEventListener("input", e=>this.onProgram(Register.UNSIGNED)); + exp.add(opt); + + // Float radio button + opt = new Toolkit.Radio(app, { group: group }); + opt.setText("{debug.cpu.float}", true); + opt.addEventListener("input", e=>this.onProgram(Register.FLOAT)); + exp.add(opt); + } + + // Expansion controls for system registers + initSystem(app, fields) { + + // No expansion area + if (!fields) + return; + + // Expansion area + let exp = this.expansion = new Toolkit.Component(app, { + class: "expansion", + id : Toolkit.id(), + style: { + display : "inline-grid", + gridTemplateColumns: "max-content max-content", + } + }); + + // Process all controls + for (let field of fields) { + + // Bit check box + if (field[0] == "check") { + let box = new Toolkit.Checkbox(app); + box.setText(field[1], false); + box.bit = field[2]; + box.addEventListener("input", e=>this.onBit(e)); + exp.append(box.element); + if (this.type == Register.PIR || this.type == Register.TKCW) + box.disabled = true; + this.controls.push(box); + } + + // Decimal text box + else if (field[0] == "textd") { + + // Containing element for outer layout purposes + let div = document.createElement("div"); + div.className = "text-dec"; + Object.assign(div.style, { + alignItems : "center", + display : "inline-grid", + gridTemplateColumns: "max-content auto" + }); + + // Text box + let txt = new Toolkit.TextBox(app, { + id : Toolkit.id(), + spellcheck: false, + value : "0", + style : { + maxWidth: "2em" + } + }); + txt.bit = field[2]; + txt.bits = field[3]; + txt.isHex = false; + txt.addEventListener("action", e=>this.onText(e)); + + // Label + let lbl = new Toolkit.Label(app, { + htmlFor: txt.element.id, + id : Toolkit.id(), + tag : "label" + }); + lbl.setText(field[1], false); + + txt.setLabel(lbl); + + // Disable all fields + if (this.type == Register.PIR || this.type == Register.TKCW) { + lbl.disabled = true; + txt.disabled = true; + } + + // Output control + this.controls.push(txt); + div.append(lbl.element); + div.append(txt.element); + exp.append(div); + } + + // Hexadecimal text box + else if (field[0] == "texth") { + + // Text box + let txt = new Toolkit.TextBox(app, { + class : "tk text-box mono", + id : Toolkit.id(), + spellcheck: false, + value : "0", + style : { + maxWidth: "3em" + } + }); + txt.bit = field[2]; + txt.bits = field[3]; + txt.isHex = true; + txt.addEventListener("action", e=>this.onText(e)); + + // Label + let lbl = new Toolkit.Label(app, { + htmlFor: txt.element.id, + id : Toolkit.id(), + tag : "label" + }); + lbl.setText(field[1], false); + + txt.setLabel(lbl); + + // Disable all fields + if (this.type == Register.PIR || this.type == Register.TKCW) { + lbl.disabled = true; + txt.disabled = true; + } + + // Output control + this.controls.push(txt); + exp.append(lbl.element); + exp.append(txt.element); + } + + } + + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Expand/collapse button key press + expKeyDown(e) { + if ( + !(e.altKey || e.ctrlKey || e.shiftKey) && + (e.key == " " || e.key == "Enter") + ) this.expanded = !this.expanded; + } + + // Expand/collapse button pointer down + expPointerDown(e) { + this.btnExpand.element.focus(); + if (e.button != 0) + return; + Toolkit.handle(e); + this.expanded = !this.expanded; + } + + // Program register format changed + onProgram(format) { + this.format = format; + this.txtValue.element.classList + [format == Register.HEX ? "add" : "remove"]("mono"); + this.formatValue(); + } + + // Bit check box input + onBit(e) { + let oldValue = this.target[this.key]; + let target = e.target.component; + let mask = 1 << target.bit; + + // Cannot change the value + if (e.disabled) + return; + + // Update the value + this.setValue(target.checked ? oldValue | mask : oldValue & ~mask); + } + + // Text box commit + onText(e) { + let oldValue = this.target[this.key]; + let target = e.target.component; + let newValue = parseInt(target.value, target.isHex ? 16 : 10); + + // Cannot change the value + if (e.disabled) + return; + + // The provided value is invalid + if (!Number.isInteger(newValue)) { + this.refresh(); + return; + } + + // Update the value + let mask = (1 << target.bits) - 1 << target.bit; + this.setValue(oldValue & ~mask | newValue << target.bit & mask); + } + + // Value text box commit + valAction(e) { + let text = this.txtValue.value; + let value = null; + + Toolkit.handle(e); + + // Program register with non-default format + if (this.type == Register.PROGRAM && this.format != Register.HEX) { + switch (this.format) { + case Register.SIGNED: + value = parseInt(text); + if ( + !Number.isInteger(value) || + value < -2147483648 || + value > +2147483647 + ) value = null; + break; + case Register.UNSIGNED: + value = parseInt(text); + if ( + !Number.isInteger(value) || + value < 0 || + value > 4294967295 + ) value = null; + break; + case Register.FLOAT: + value = parseFloat(text); + value = + !Number.isFinite(value) || + value < Debugger.ixf(0xFF7FFFFF) || + value > Debugger.ixf(0x7F7FFFFF) + ? null : Debugger.fxi(value) >>> 0; + break; + } + } + + // Default hexadecimal format + else { + value = parseInt(text, 16); + if ( + !Number.isInteger(value) || + value < 0 || + value > 4294967295 + ) value = null; + } + + // Apply the new value + if (value === null) + this.formatValue(); + else this.setValue(value); + } + + // Value text box key press + valKeyDown(e) { + if (e.altKey || e.ctrlKey || e.shiftKey || e.key != "Escape") + return; + Toolkit.handle(e); + this.txtValue.value = + this.debug.dasm.hex(this.target[this.key], 8, false); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // The expansion area is visible + get expanded() { + return this.btnExpand.element.getAttribute("aria-expanded") == "true"; + } + set expanded(expanded) { + expanded = !!expanded; + if (this.expansion == null || expanded == this.expanded) + return; + this.btnExpand.element.setAttribute("aria-expanded", expanded); + this.expansion.visible = expanded; + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Update controls from simulation state + refresh() { + + // Name label + this.label.setText( + this.key == "pc" ? "PC" : + this.type != Register.PROGRAM ? Disassembler.SYSNAMES[this.key] : + this.dasm.programRegister(this.key) + ); + + // Value text box + let value = this.target[this.key]; + this.formatValue(); + + // Expansion controls + for (let ctrl of this.controls) { + + // Bit check box + if (ctrl instanceof Toolkit.Checkbox) + ctrl.checked = !!(value >> ctrl.bit & 1); + + // Decimal text box + else if (ctrl instanceof Toolkit.TextBox && !ctrl.isHex) + ctrl.value = value >> ctrl.bit & (1 << ctrl.bits) - 1; + + // Hexadecimal text box + else if (ctrl instanceof Toolkit.TextBox && ctrl.isHex) { + ctrl.value = this.dasm.hex( + value >> ctrl.bit & (1 << ctrl.bits) - 1, + Math.ceil(ctrl.bits / 4), + false); + } + + } + + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Format the value as a string in the text box + formatValue() { + let text = ""; + let value = this.target[this.key]; + + // Program register with non-default format + if (this.type == Register.PROGRAM && this.format != Register.HEX) { + switch (this.format) { + case Register.SIGNED: + text = (value >> 0).toString(); + break; + case Register.UNSIGNED: + text = (value >>> 0).toString(); + break; + case Register.FLOAT: + value = Debugger.ixf(value); + if (Number.isFinite(value)) { + text = value.toFixed(100); + if (/[^0-9\-\.]/.test(text)) + text = value.toFixed(6); + if (text.indexOf(".") != -1) { + text = text.replace(/0+$/, "") + .replace(/\.$/, ".0"); + } else text += ".0"; + } else if (!Number.isNaN(value)) { + text = + (value == Number.NEGATIVE_INFINITY ? "-" : "") + + this.debug.app.localize("{debug.cpu.infinity}") + ; + } else text = "NaN"; + break; + } + } + + // Default hexadecimal format + else text = this.dasm.hex(value >>> 0, 8, false); + + // Update the text + this.txtValue.value = text; + } + + // Specify a new register value + async setValue(value) { + + // Update the value in the simulation state + let sim = this.debug.sim; + let result = await ( + this.key == "pc" ? this.debug.core + .setProgramCounter(sim, value, { + refresh: [ this.registers.cpu.disassembler.subscription ] + }) : + this.type != Register.PROGRAM ? this.debug.core + .setSystemRegister(sim, this.key, value) : + this.debug.core + .setProgramRegister(sim, this.key, value) + ); + + // Update the value in the debugger window + this.target[this.key] = result.value; + + // Update the register controls + this.refresh(); + } + +} + + + + +/////////////////////////////////////////////////////////////////////////////// + + + +// Register list manager +class RegisterPane extends Toolkit.SplitPane { + + //////////////////////////////// Constants //////////////////////////////// + + // System register templates + static SYSTEMS = [ + [ [ "pc" ], Register.PLAIN ], + [ [ "system", 5 ], Register.PSW ], + [ [ "system", 25 ], Register.PLAIN ], + [ [ "system", 24 ], Register.CHCW ], + [ [ "system", 4 ], Register.ECR ], + [ [ "system", 0 ], Register.PLAIN ], + [ [ "system", 1 ], Register.PSW ], + [ [ "system", 2 ], Register.PLAIN ], + [ [ "system", 3 ], Register.PSW ], + [ [ "system", 6 ], Register.PIR ], + [ [ "system", 7 ], Register.TKCW ], + [ [ "system", 29 ], Register.PLAIN ], + [ [ "system", 30 ], Register.PLAIN ], + [ [ "system", 31 ], Register.PLAIN ] + ]; + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(cpu) { + super(cpu.debug.app, { + orientation: "top", + style: { + overflow: "visible" + } + }); + + // Configure instance fields + this.cpu = cpu; + this.list = []; + this.pc = 0xFFFFFFF0; + this.program = new Array(32); + this.pending = false; + this.subscription = [ 0, cpu.index, "cpu", "registers", "refresh" ], + this.system = new Array(32); + + // Initialize regsiters + for (let x = 0; x < 32; x++) + this.program[x] = this.system[x] = 0; + + // System registers list + this.lstSystem = new Toolkit.Component(cpu.debug.app, { + class: "tk registers", + style: { + minHeight: "100%", + minWidth : "100%", + width : "max-content" + } + }); + + // System registers scroll pane + this.scrSystem = new Toolkit.ScrollPane(cpu.debug.app, { + class : "tk scroll-pane scr-system", + overflowX: "auto", + overflowY: "scroll", + view : this.lstSystem, + style : { + position: "relative" + } + }); + this.primary = this.scrSystem; + + // Program registers list + this.lstProgram = new Toolkit.Component(cpu.debug.app, { + class: "tk registers", + style: { + minHeight: "100%", + minWidth : "100%", + width : "max-content" + } + }); + + // Program registers scroll pane + this.scrProgram = new Toolkit.ScrollPane(cpu.debug.app, { + class : "tk scroll-pane scr-program", + overflowX: "auto", + overflowY: "scroll", + view : this.lstProgram + }); + this.secondary = this.scrProgram; + + // Configure register lists + for (let sys of RegisterPane.SYSTEMS) + this.addRegister(new Register(this, sys[0], sys[1])); + for (let x = 0; x < 32; x++) { + this.addRegister(new Register(this, + [ "program", x ], Register.PROGRAM)); + } + + // Value text box measurer + let text = []; + for (let c of "0123456789abcdefABCDEF") + text.push(c.repeat(8)); + this.sizer = new Toolkit.Label(cpu.app, { + class: "tk text-box mono", + style: { + position : "absolute", + visibility: "hidden" + } + }); + this.sizer.setText(text.join("\n"), false); + this.list[0].main.element.after(this.sizer.element); + + // Monitor the bounds of the register names column + let resizer = new ResizeObserver(()=>this.regResize()); + for (let reg of this.list) + resizer.observe(reg.label.element); + + // Monitor the bounds of the value text boxes + this.list[0].txtValue.addEventListener("resize", e=>this.valResize(e)); + this.sizer .addEventListener("resize", e=>this.sizResize(e)); + } + + // Add a Register object to a list + addRegister(reg) { + this.list.push(reg); + + // Add the main element to the appropriate list container + let list = this[reg.type == Register.PROGRAM ? + "lstProgram" : "lstSystem"]; + list.add(reg.main); + + // Add the expansion element + if (reg.expansion != null) + list.add(reg.expansion); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Register label resized + regResize() { + let max = 0; + let widths = new Array(this.list.length); + + // Measure the widths of all labels + for (let x = 0; x < this.list.length; x++) { + widths[x] = Math.ceil(this.list[x].label.element + .getBoundingClientRect().width); + max = Math.max(max, widths[x]); + } + + // Ensure all labels share the same maximum width + for (let x = 0; x < this.list.length; x++) { + if (widths[x] < max) + this.list[x].label.element.style.minWidth = max + "px"; + } + + } + + // Sizer resized + sizResize(e) { + let width = Math.ceil(e.target.getBoundingClientRect().width) + "px"; + for (let reg of this.list) + reg.txtValue.style.minWidth = width; + } + + // Value text box resized + valResize(e) { + let height = Math.ceil(e.target.getBoundingClientRect().height); + this.scrSystem .vscroll.unitIncrement = height; + this.scrProgram.vscroll.unitIncrement = height; + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Retrieve registers from the simulation state + async fetch() { + + // Select the parameters for the simulation fetch + let params = {}; + + // A communication with the core thread is already underway + if (this.pending) { + this.pending = params; + this.refresh(); + return; + } + + // Retrieve data from the simulation state + this.pending = params; + for (let data = null, promise = null; this.pending instanceof Object;){ + + // Wait for a transaction to complete + if (promise != null) { + this.pending= true; + data = await promise; + promise = null; + } + + // Initiate a new transaction + if (this.pending instanceof Object) { + params = this.pending; + let options = {}; + if (this.isVisible()) + options.subscription = this.subscription; + promise = this.cpu.debug.core.getAllRegisters( + this.cpu.debug.sim, options); + } + + // Process the result of a transaction + if (data != null) { + this.refresh(data); + data = null; + } + + }; + this.pending = false; + } + + // Component is being displayed for the first time + firstShow() { + + // Retrieve the desired dimensions of the system registers list + let bounds = this.scrSystem.element.getBoundingClientRect(); + for (let reg of this.list) { + let style = reg.main.style; + if (style.visibility) { + style.removeProperty("position"); + style.removeProperty("visibility"); + } + } + + // Prepare the initial dimensions of the register lists + this .element.style.width = Math.ceil(bounds.width ) + "px"; + this.scrSystem.element.style.height = Math.ceil(bounds.height) + "px"; + } + + // Update register lists + refresh(msg = null) { + + // Receiving data from the simulation state + if (msg != null) { + this.pc = msg.pc; + this.program.splice(0, this.program.length, ... msg.program); + this.system .splice(0, this.system .length, ... msg.system ); + } + + // Update register controls + for (let reg of this.list) + reg.refresh(); + } + + // Stop receiving updates from the simulation + unsubscribe() { + this.cpu.debug.core.unsubscribe(this.subscription, false); + } + +} + +Debugger.CPU = CPU; +} + +export { register }; diff --git a/web/debugger/Debugger.js b/web/debugger/Debugger.js new file mode 100644 index 0000000..1ea7d7d --- /dev/null +++ b/web/debugger/Debugger.js @@ -0,0 +1,98 @@ +import { ISX } from /**/"./ISX.js"; + +// Debug mode UI manager +class Debugger { + + ///////////////////////////// Static Methods ////////////////////////////// + + // Data type conversions + static F32 = new Float32Array(1); + static U32 = new Uint32Array(this.F32.buffer); + + // Reinterpret a float32 as a u32 + static fxi(x) { + this.F32[0] = x; + return this.U32[0]; + } + + // Process file data as ISM + static isx(data) { + return new ISX(data); + } + + // Reinterpret a u32 as a float32 + static ixf(x) { + this.U32[0] = x; + return this.F32[0]; + } + + // Compute the number of lines scrolled by a WheelEvent + static linesScrolled(e, lineHeight, pageLines, delta) { + let ret = { + delta: delta, + lines: 0 + }; + + // No scrolling occurred + if (e.deltaY == 0); + + // Scrolling by pixel + else if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) { + ret.delta += e.deltaY; + ret.lines = Math.sign(ret.delta) * + Math.floor(Math.abs(ret.delta) / lineHeight); + ret.delta -= ret.lines * lineHeight; + } + + // Scrolling by line + else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) + ret.lines = Math.trunc(e.deltaY); + + // Scrolling by page + else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) + ret.lines = Math.trunc(e.deltaY) * pageLines; + + // Unknown scrolling mode + else ret.lines = 3 * Math.sign(e.deltaY); + + return ret; + } + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, sim, index) { + + // Configure instance fields + this.app = app; + this.core = app.core; + this.dasm = app.dasm; + this.sim = sim; + + // Configure debugger windows + this.cpu = new Debugger.CPU (this, index); + this.memory = new Debugger.Memory(this, index); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Ensure PC is visible in the disassembler + followPC(pc = null) { + this.cpu.disassembler.followPC(pc); + } + + // Format a number as hexadecimal + hex(value, digits = null, decorated = true) { + return this.dasm.hex(value, digits, decorated); + } + +} + +// Register component classes +(await import(/**/"./CPU.js" )).register(Debugger); +(await import(/**/"./Memory.js")).register(Debugger); + +export { Debugger }; diff --git a/web/debugger/ISX.js b/web/debugger/ISX.js new file mode 100644 index 0000000..44ee8da --- /dev/null +++ b/web/debugger/ISX.js @@ -0,0 +1,177 @@ +// Debug manager for Intelligent Systems binaries +class ISX { + + ///////////////////////// Initialization Methods ////////////////////////// + + // Throws on decoding error + constructor(data) { + + // Configure instance fields + this.data = data; + this.offset = 0; + this.ranges = []; + this.symbols = []; + this.codes = []; + + // Skip any header that may be present + if (data.length >= 32 && this.readInt(3) == 0x585349) + this.offset = 32; + else this.offset = 0; + + // Process all records + while (this.offset < this.data.length) { + switch (this.readInt(1)) { + + // Virtual Boy records + case 0x11: this.code (); break; + case 0x13: this.range (); break; + case 0x14: this.symbol(); break; + + // System records + case 0x20: + case 0x21: + case 0x22: + let length = this.readInt(4); + this.offset += length; + break; + + // Other records + default: throw "ISX decode error"; + } + } + + // Cleanup instance fields + delete this.data; + delete this.decoder; + delete this.offset; + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Produce a .vb format ROM file from the ISX code segments + toROM() { + let head = 0x00000000; + let tail = 0x01000000; + + // Inspect all code segments + for (let code of this.codes) { + let start = code.address & 0x00FFFFFF; + let end = start + code.data.length; + + // Segment begins in the first half of ROM + if (start < 0x00800000) { + + // Segment ends in the second half of ROM + if (end > 0x00800000) { + head = tail = 0; + break; + } + + // Segment ends in the first half of ROM + else if (end > head) + head = end; + } + + // Segment begins in the second half of ROM + else if (start < tail) + tail = start; + } + + // Prepare the output buffer + let min = head + 0x01000000 - tail; + let size = 1; + for (; size < min; size <<= 1); + let rom = new Uint8Array(size); + + // Output all code segments + for (let code of this.codes) { + let dest = code.address & rom.length - 1; + for (let src = 0; src < code.data.length; src++, dest++) + rom[dest] = code.data[src]; + } + + return rom; + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Process a code record + code() { + let address = this.readInt(4); + let length = this.readInt(4); + let data = this.readBytes(length); + if ( + length == 0 || + length > 0x01000000 || + (address & 0x07000000) != 0x07000000 || + (address & 0x07000000) + length > 0x08000000 + ) throw "ISX decode error"; + this.codes.push({ + address: address, + data : data + }); + } + + // Process a range record + range() { + let count = this.readInt(2); + while (count--) { + let start = this.readInt(4); + let end = this.readInt(4); + let type = this.readInt(1); + this.ranges.push({ + end : end, + start: start, + type : type + }); + } + } + + // Process a symbol record + symbol() { + let count = this.readInt(2); + while (count--) { + let length = this.readInt(1); + let name = this.readString(length); + let flags = this.readInt(2); + let address = this.readInt(4); + this.symbols.push({ + address: address, + flags : flags, + name : name + }); + } + } + + // Read a byte buffer + readBytes(size) { + if (this.offset + size > this.data.length) + throw "ISX decode error"; + let ret = this.data.slice(this.offset, this.offset + size); + this.offset += size; + return ret; + } + + // Read an integer + readInt(size) { + if (this.offset + size > this.data.length) + throw "ISX decode error"; + let ret = new Uint32Array(1); + for (let shift = 0; size > 0; size--, shift += 8) + ret[0] |= this.data[this.offset++] << shift; + return ret[0]; + } + + // Read a text string + readString(size) { + return (this.decoder = this.decoder || new TextDecoder() + ).decode(this.readBytes(size)); + } + +} + +export { ISX }; diff --git a/web/debugger/Memory.js b/web/debugger/Memory.js new file mode 100644 index 0000000..7834b81 --- /dev/null +++ b/web/debugger/Memory.js @@ -0,0 +1,558 @@ +import { Toolkit } from /**/"../toolkit/Toolkit.js"; +let register = Debugger => Debugger.Memory = + +// Debugger memory window +class Memory extends Toolkit.Window { + + //////////////////////////////// Constants //////////////////////////////// + + // Bus indexes + static MEMORY = 0; + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(debug, index) { + super(debug.app, { + class: "tk window memory" + }); + + // Configure instance fields + this.data = null, + this.dataAddress = null, + this.debug = debug; + this.delta = 0; + this.editDigit = null; + this.height = 300; + this.index = index; + this.lines = []; + this.pending = false; + this.shown = false; + this.subscription = [ 0, index, "memory", "refresh" ]; + this.width = 400; + + // Available buses + this.buses = [ + { + index : Memory.MEMORY, + editAddress: 0x05000000, + viewAddress: 0x05000000 + } + ]; + this.bus = this.buses[Memory.MEMORY]; + + // Window + this.setTitle("{debug.memory._}", true); + this.substitute("#", " " + (index + 1)); + if (index == 1) + this.element.classList.add("two"); + this.addEventListener("close" , e=>this.visible = false); + this.addEventListener("visibility", e=>this.onVisibility(e)); + + // Client area + Object.assign(this.client.style, { + display : "grid", + gridTemplateRows: "max-content auto" + }); + + // Bus drop-down + this.drpBus = new Toolkit.DropDown(debug.app); + this.drpBus.setLabel("{debug.memory.bus}", true); + this.drpBus.setTitle("{debug.memory.bus}", true); + this.drpBus.add("{debug.memory.busMemory}", true, + this.buses[Memory.MEMORY]); + this.drpBus.addEventListener("input", e=>this.busInput()); + this.add(this.drpBus); + + // Hex editor + this.hexEditor = new Toolkit.Component(debug.app, { + class : "tk mono hex-editor", + role : "application", + tabIndex: "0", + style : { + display : "grid", + gridTemplateColumns: "repeat(17, max-content)", + height : "100%", + minWidth : "100%", + overflow : "hidden", + position : "relative", + width : "max-content" + } + }); + this.hexEditor.localize = ()=>{ + this.hexEditor.localizeRoleDescription(); + this.hexEditor.localizeLabel(); + }; + this.hexEditor.setLabel("{debug.memory.hexEditor}", true); + this.hexEditor.setRoleDescription("{debug.memory.hexEditor}", true); + this.hexEditor.addEventListener("keydown", e=>this.hexKeyDown(e)); + this.hexEditor.addEventListener("resize" , e=>this.hexResize ( )); + this.hexEditor.addEventListener("wheel" , e=>this.hexWheel (e)); + this.hexEditor.addEventListener( + "pointerdown", e=>this.hexPointerDown(e)); + this.lastFocus = this.hexEditor; + + // Label for measuring text dimensions + this.sizer = new Toolkit.Label(debug.app, { + class : "tk label mono", + visible : false, + visibility: true, + style: { + position: "absolute" + } + }); + this.sizer.setText("\u00a0", false); //   + this.hexEditor.append(this.sizer); + + // Hex editor scroll pane + this.scrHex = new Toolkit.ScrollPane(debug.app, { + overflowX: "auto", + overflowY: "hidden", + view : this.hexEditor + }); + this.add(this.scrHex); + + // Hide the bus drop-down: Virtual Boy only has one bus + this.drpBus.visible = false; + this.client.style.gridTemplateRows = "auto"; + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Bus drop-down selection + busInput() { + + // An edit is in progress + if (this.editDigit !== null) + this.commit(false); + + // Switch to the new bus + this.bus = this.drpBus.value; + this.fetch(); + } + + // Hex editor key press + hexKeyDown(e) { + + // Error checking + if (e.altKey) + return; + + // Processing by key, Ctrl pressed + if (e.ctrlKey) switch (e.key) { + case "g": case "G": + Toolkit.handle(e); + this.goto(); + return; + default: return; + } + + // Processing by key, scroll lock off + if (!e.getModifierState("ScrollLock")) switch (e.key) { + case "ArrowDown": + this.commit(); + this.setEditAddress(this.bus.editAddress + 16); + Toolkit.handle(e); + return; + case "ArrowLeft": + this.commit(); + this.setEditAddress(this.bus.editAddress - 1); + Toolkit.handle(e); + return; + case "ArrowRight": + this.commit(); + this.setEditAddress(this.bus.editAddress + 1); + Toolkit.handle(e); + return; + case "ArrowUp": + this.commit(); + this.setEditAddress(this.bus.editAddress - 16); + Toolkit.handle(e); + return; + case "PageDown": + this.commit(); + this.setEditAddress(this.bus.editAddress + this.tall(true)*16); + Toolkit.handle(e); + return; + case "PageUp": + this.commit(); + this.setEditAddress(this.bus.editAddress - this.tall(true)*16); + Toolkit.handle(e); + return; + } + + // Processing by key, scroll lock on + else switch (e.key) { + case "ArrowDown": + this.bus.viewAddress += 16; + this.fetch(); + Toolkit.handle(e); + return; + case "ArrowLeft": + this.scrHex.scrollLeft -= this.scrHex.hscroll.unitIncrement; + Toolkit.handle(e); + return; + case "ArrowRight": + this.scrHex.scrollLeft += this.scrHex.hscroll.unitIncrement; + Toolkit.handle(e); + return; + case "ArrowUp": + this.bus.viewAddress -= 16; + this.fetch(); + Toolkit.handle(e); + return; + case "PageDown": + this.bus.viewAddress += this.tall(true) * 16; + this.fetch(); + Toolkit.handle(e); + return; + case "PageUp": + this.bus.viewAddress -= this.tall(true) * 16; + this.fetch(); + Toolkit.handle(e); + return; + } + + // Processing by key, editing + switch (e.key) { + + case "0": case "1": case "2": case "3": case "4": + case "5": case "6": case "7": case "8": case "9": + case "a": case "A": case "b": case "B": case "c": + case "C": case "d": case "D": case "e": case "E": + case "f": case "F": + let digit = parseInt(e.key, 16); + if (this.editDigit === null) { + this.editDigit = digit; + this.setEditAddress(this.bus.editAddress); + } else { + this.editDigit = this.editDigit << 4 | digit; + this.commit(); + this.setEditAddress(this.bus.editAddress + 1); + } + break; + + // Commit the current edit + case "Enter": + if (this.editDigit === null) + break; + this.commit(); + this.setEditAddress(this.bus.editAddress + 1); + break; + + // Cancel the current edit + case "Escape": + if (this.editDigit === null) + return; + this.editDigit = null; + this.setEditAddress(this.bus.editAddress); + break; + + default: return; + } + + Toolkit.handle(e); + } + + // Hex editor pointer down + hexPointerDown(e) { + + // Error checking + if (e.button != 0) + return; + + // Working variables + let cols = this.lines[0].lblBytes.map(l=>l.getBoundingClientRect()); + let y = Math.max(0, Math.floor((e.clientY-cols[0].y)/cols[0].height)); + let x = 15; + + // Determine which column is closest to the touch point + if (e.clientX < cols[15].right) { + for (let l = 0; l < 15; l++) { + if (e.clientX > (cols[l].right + cols[l + 1].x) / 2) + continue; + x = l; + break; + } + } + + // Update the selection address + let address = this.bus.viewAddress + y * 16 + x >>> 0; + if (this.editDigit !== null && address != this.bus.editAddress) + this.commit(); + this.setEditAddress(address); + } + + // Hex editor resized + hexResize() { + let tall = this.tall(false); + let grew = this.lines.length < tall; + + // Process all visible lines + for (let y = this.lines.length; y < tall; y++) { + let line = { + lblAddress: document.createElement("div"), + lblBytes : [] + }; + + // Address label + line.lblAddress.className = "addr" + (y == 0 ? " first" : ""); + this.hexEditor.append(line.lblAddress); + + // Byte labels + for (let x = 0; x < 16; x++) { + let lbl = line.lblBytes[x] = document.createElement("div"); + lbl.className = "byte b" + x + (y == 0 ? " first" : ""); + this.hexEditor.append(lbl); + } + + this.lines.push(line); + } + + // Remove lines that are no longer visible + while (tall < this.lines.length) { + let line = this.lines[tall]; + line.lblAddress.remove(); + for (let lbl of line.lblBytes) + lbl.remove(); + this.lines.splice(tall, 1); + } + + // Configure scroll bar + this.scrHex.hscroll.unitIncrement = + this.sizer.element.getBoundingClientRect().height; + + // Update components + if (grew) + this.fetch(); + else this.refresh(); + } + + // Hex editor mouse wheel + hexWheel(e) { + + // Error checking + if (e.altKey || e.ctrlKey || e.shiftKey) + return; + + // Always handle the event + Toolkit.handle(e); + + // Determine how many full lines were scrolled + let scr = Debugger.linesScrolled(e, + this.sizer.element.getBoundingClientRect().height, + this.tall(true), + this.delta + ); + this.delta = scr.delta; + scr.lines = Math.max(-3, Math.min(3, scr.lines)); + + // No lines were scrolled + if (scr.lines == 0) + return; + + // Scroll the view + this.bus.viewAddress = this.bus.viewAddress + scr.lines * 16 >>> 0; + this.fetch(); + } + + // Window visibility + onVisibility(e) { + this.shown = this.shown || e.visible; + if (!e.visible) + this.debug.core.unsubscribe(this.subscription, false); + else this.fetch(); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Prompt the user to navigate to a new editing address + goto() { + + // Retrieve the value from the user + let addr = prompt(this.app.localize("{debug.memory.goto}")); + if (addr === null) + return; + addr = parseInt(addr.trim(), 16); + if ( + !Number.isInteger(addr) || + addr < 0 || + addr > 4294967295 + ) return; + + // Commit an outstanding edit + if (this.editDigit !== null && this.bus.editAddress != addr) + this.commit(); + + // Navigate to the given address + this.setEditAddress(addr, 1/3); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Write the edited value to the simulation state + commit(refresh = true) { + + // Error checking + if (this.editDigit === null) + return; + + // The edited value is in the bus's data buffer + if (this.data != null) { + let offset = this.bus.editAddress - this.dataAddress >>> 0; + if (offset < this.data.length) + this.data[offset] = this.editDigit; + } + + // Write one byte to the simulation state + let data = new Uint8Array(1); + data[0] = this.editDigit; + this.editDigit = null; + this.debug.core.write(this.debug.sim, this.bus.editAddress, + data, { refresh: refresh }); + } + + // Retrieve data from the simulation state + async fetch() { + + // Select the parameters for the simulation fetch + let params = { + address: this.bus.viewAddress - 10 * 16, + length : (this.tall(false) + 20) * 16 + }; + + // A communication with the core thread is already underway + if (this.pending) { + this.pending = params; + this.refresh(); + return; + } + + // Retrieve data from the simulation state + this.pending = params; + for (let data=null, promise=null; this.pending instanceof Object;) { + + // Wait for a transaction to complete + if (promise != null) { + this.pending = true; + data = await promise; + promise = null; + } + + // Initiate a new transaction + if (this.pending instanceof Object) { + params = this.pending; + let options = {}; + if (this.isVisible()) + options.subscription = this.subscription; + promise = this.debug.core.read(this.debug.sim, + params.address, params.length, options); + } + + // Process the result of a transaction + if (data != null) { + this.refresh(data); + data = null; + } + + }; + this.pending = false; + } + + // Update hex editor + refresh(msg = null) { + + // Receiving data from the simulation state + if (msg != null) { + this.data = msg.data; + this.dataAddress = msg.address; + } + + // Process all lines + for (let y = 0; y < this.lines.length; y++) { + let address = this.bus.viewAddress + y * 16 >>> 0; + let line = this.lines[y]; + + // Address label + line.lblAddress.innerText = this.debug.hex(address, 8, false); + + // Process all bytes + for (let x = 0; x < 16; x++) { + let label = line.lblBytes[x]; + let text = "--"; + + // Currently editing this byte + if (address+x==this.bus.editAddress && this.editDigit!==null) { + text = this.debug.hex(this.editDigit, 1, false); + } + + // Bus data exists + else if (this.data != null) { + let offset = address - this.dataAddress + x >>> 0; + + // The byte is contained in the bus data buffer + if (offset >= 0 && offset < this.data.length) + text = this.debug.hex(this.data[offset], 2, false); + } + + label.innerText = text; + label.classList[address + x == this.bus.editAddress ? + "add" : "remove"]("edit"); + } + + } + } + + // Specify the address of the hex editor's selection + setEditAddress(address, auto = false) { + let col = this.lines[0].lblBytes[address&15].getBoundingClientRect(); + let port = this.scrHex.viewport.element.getBoundingClientRect(); + let row = (address & ~15) >>> 0; + let scr = this.scrHex.scrollLeft; + let tall = this.tall(true, 0); + + // Ensure the data row is fully visible + if (row - this.bus.viewAddress >>> 0 >= tall * 16) { + if (!auto) { + this.bus.viewAddress = + this.bus.viewAddress - row >>> 0 <= + row - (this.bus.viewAddress + tall * 16) >>> 0 + ? row : row - (tall - 1) * 16; + } else this.bus.viewAddress = row - Math.floor(tall * auto) * 16; + this.fetch(); + } + + // Ensure the column is fully visible + this.scrHex.scrollLeft = + Math.min( + Math.max( + scr, + scr + col.right - port.right + ), + scr - port.x + col.x + ) + ; + + // Refresh the display; + this.bus.editAddress = address; + this.refresh(); + } + + // Measure the number of lines visible in the view + tall(fully = null, plus = 1) { + return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"]( + this.scrHex.viewport.element.getBoundingClientRect().height / + this.sizer .element.getBoundingClientRect().height + )) + plus; + } + +} + +export { register }; diff --git a/web/locale/en-US.json b/web/locale/en-US.json new file mode 100644 index 0000000..9a855f0 --- /dev/null +++ b/web/locale/en-US.json @@ -0,0 +1,77 @@ +{ + "id" : "en-US", + "name": "English (US)", + + "app": { + "title": "Virtual Boy Emulator" + }, + + "menu._": "Main menu", + + "menu.file": { + "_" : "File", + "loadROM" : "Load ROM{#}...", + "loadROMError" : "Error loading ROM file", + "loadROMInvalid": "The selected file is not a Virtual Boy ROM.", + "dualMode" : "Dual mode", + "debugMode" : "Debug mode" + }, + + "menu.emulation": { + "_" : "Emulation", + "run" : "Run", + "pause" : "Pause", + "reset" : "Reset{#}", + "linkSims": "Link sims" + }, + + "menu.debug": { + "_" : "Debug{#}", + "backgrounds" : "Backgrounds", + "bgMaps" : "BG maps", + "breakpoints" : "Breakpoints", + "characters" : "Characters", + "console" : "Console", + "cpu" : "CPU", + "frameBuffers": "Frame buffers", + "memory" : "Memory", + "objects" : "Objects", + "palettes" : "Palettes" + }, + + "menu.theme": { + "_" : "Theme", + "auto" : "Auto", + "dark" : "Dark", + "light" : "Light", + "virtual": "Virtual" + }, + + "window": { + "close": "Close" + }, + + "debug.cpu": { + "_" : "CPU{#}", + "disassembler" : "Disassembler", + "float" : "Float", + "format" : "Format", + "goto" : "Enter the address to seek to:", + "hex" : "Hex", + "infinity" : "Infinity", + "programRegisters": "Program registers", + "signed" : "Signed", + "systemRegisters" : "System registers", + "unsigned" : "Unsigned", + "value" : "Value" + }, + + "debug.memory": { + "_" : "Memory{#}", + "bus" : "Bus", + "busMemory": "Memory", + "goto" : "Enter the address to seek to:", + "hexEditor": "Hex editor" + } + +} diff --git a/web/template.html b/web/template.html new file mode 100644 index 0000000..69f9ab8 --- /dev/null +++ b/web/template.html @@ -0,0 +1 @@ +Virtual Boy Emulator \ No newline at end of file diff --git a/app/theme/check.svg b/web/theme/check.svg similarity index 100% rename from app/theme/check.svg rename to web/theme/check.svg diff --git a/app/theme/close.svg b/web/theme/close.svg similarity index 100% rename from app/theme/close.svg rename to web/theme/close.svg diff --git a/app/theme/collapse.svg b/web/theme/collapse.svg similarity index 100% rename from app/theme/collapse.svg rename to web/theme/collapse.svg diff --git a/web/theme/dark.css b/web/theme/dark.css new file mode 100644 index 0000000..b1785fb --- /dev/null +++ b/web/theme/dark.css @@ -0,0 +1,28 @@ +:root { + --tk-control : #333333; + --tk-control-active : #555555; + --tk-control-border : #cccccc; + --tk-control-highlight : #444444; + --tk-control-shadow : #9b9b9b; + --tk-control-text : #cccccc; + --tk-desktop : #111111; + --tk-selected : #008542; + --tk-selected-blur : #325342; + --tk-selected-blur-text : #ffffff; + --tk-selected-text : #ffffff; + --tk-splitter-focus : #008542c0; + --tk-window : #222222; + --tk-window-blur-close : #d9aeae; + --tk-window-blur-close-text : #eeeeee; + --tk-window-blur-title : #9fafb9; + --tk-window-blur-title2 : #c0b0a0; + --tk-window-blur-title-text : #444444; + --tk-window-close : #ee9999; + --tk-window-close-focus : #99ee99; + --tk-window-close-focus-text: #333333; + --tk-window-close-text : #ffffff; + --tk-window-text : #cccccc; + --tk-window-title : #80ccff; + --tk-window-title2 : #ffb894; + --tk-window-title-text : #000000; +} diff --git a/app/theme/expand.svg b/web/theme/expand.svg similarity index 100% rename from app/theme/expand.svg rename to web/theme/expand.svg diff --git a/app/theme/inconsolata.woff2 b/web/theme/inconsolata.woff2 similarity index 100% rename from app/theme/inconsolata.woff2 rename to web/theme/inconsolata.woff2 diff --git a/web/theme/kiosk.css b/web/theme/kiosk.css new file mode 100644 index 0000000..1684037 --- /dev/null +++ b/web/theme/kiosk.css @@ -0,0 +1,548 @@ +:root { + --tk-font-dialog : "Roboto", sans-serif; + --tk-font-mono : "Inconsolata SemiExpanded Medium", monospace; + --tk-font-size : 12px; +} + +@font-face { + font-family: "Roboto"; + src : /**/url("./roboto.woff2") format("woff2"); +} + +@font-face { + font-family: "Inconsolata SemiExpanded Medium"; + src : /**/url("./inconsolata.woff2") format("woff2"); +} + +body { + background: var(--tk-control); +} + +.tk { + box-sizing : border-box; + font-family: var(--tk-font-dialog); + font-size : var(--tk-font-size); + line-height: 1em; + margin : 0; + outline : none; /* User agent focus indicator */ + padding : 0; +} + +table.tk { + border : none; + border-spacing: 0; +} + +.tk.mono { + font-family: var(--tk-font-mono); +} + +.tk::selection, +.tk *::selection { + background: var(--tk-selected); + color : var(--tk-selected-text); +} + +.tk:not(:focus-within)::selection, +.tk *:not(:focus-within)::selection { + background: var(--tk-selected-blur); + color : var(--tk-selected-blur-text); +} + +.tk.display { + background: var(--tk-desktop); +} + +.tk.desktop { + background: var(--tk-desktop); +} + + + +/********************************** Button ***********************************/ + +.tk.button { + align-items : stretch; + display : inline-grid; + grid-template-columns: auto; + justify-content : stretch; + padding : 0 1px 1px 0; +} + +.tk.button .label { + align-items : center; + background : var(--tk-control); + border : 1px solid var(--tk-control-border); + box-shadow : 1px 1px 0 var(--tk-control-border); + color : var(--tk-control-text); + display : grid; + grid-template-columns: auto; + justify-content : center; + padding : 2px; +} + +.tk.button:focus .label { + background: var(--tk-control-active); +} + +.tk.button.pushed { + padding: 1px 0 0 1px; +} + +.tk.button.pushed .label { + box-shadow: none; +} + +.tk.button[aria-disabled="true"] .label { + color : var(--tk-control-shadow); + border : 1px solid var(--tk-control-shadow); + box-shadow: 1px 1px 0 var(--tk-control-shadow); +} + + + +/********************************* Checkbox **********************************/ + +.tk.checkbox { + column-gap: 2px; +} + +.tk.checkbox .box { + border: 1px solid var(--tk-control-shadow); + color : var(--tk-control-text); +} + +.tk.checkbox:focus .box { + background: var(--tk-control-active); +} + +.tk.checkbox .box:before { + background : transparent; + content : ""; + display : block; + height : 10px; + mask : /**/url("./check.svg") center no-repeat; + -webkit-mask: /**/url("./check.svg") center no-repeat; + width : 10px; +} + +.tk.checkbox[aria-checked="true"] .box:before { + background: currentcolor; +} + +.tk.checkbox.pushed .box:before { + background: var(--tk-control-shadow); +} + +.tk.checkbox[aria-disabled="true"] .box { + background: var(--tk-control); + color : var(--tk-control-shadow); +} +.tk.checkbox[aria-disabled="true"] .label { + color: var(--tk-control-shadow); +} + + + +/********************************* DropDown **********************************/ + +.tk.drop-down { + background: var(--tk-window); + border : 1px solid var(--tk-control-shadow); + color : var(--tk-window-text); + padding : 2px; +} + +.tk.drop-down:focus { + background: var(--tk-control-active); +} + +.tk.drop-down[aria-disabled="true"] { + color: var(--tk-control-shadow); +} + + + +/*********************************** Menus ***********************************/ + +.tk.menu-bar { + background : var(--tk-control); + border-bottom: 1px solid var(--tk-control-border); + color : var(--tk-control-text); + cursor : default; + padding : 2px; + position : relative; +} + +.tk.menu { + background: var(--tk-control); + border : 1px solid var(--tk-control-border); + box-shadow: 1px 1px 0 var(--tk-control-border); + color : var(--tk-control-text); + margin : -1px 0 0 1px; + padding : 2px; +} + +.tk.menu-item[aria-disabled="true"] { + color: var(--tk-control-shadow); +} + +.tk.menu-item > * { + align-items: center; + border : 1px solid transparent; + column-gap : 4px; + display : flex; + margin : 0 1px 1px 0; + padding : 2px; + user-select: none; +} + +.tk.menu-item .icon { + box-sizing: border-box; + height : 1em; + width : 1em; +} + +.tk.menu-item .icon:before { + content: ""; + display: block; + height : 100%; + width : 100%; +} + +.tk.menu-bar > .menu-item .icon, +.tk.menu:not(.icons) > .menu-item .icon { + display: none; +} + +.tk.menu-item.checkbox .icon { + border: 1px solid currentcolor; +} + +.tk.menu-item.checkbox[aria-checked="true"] .icon:before { + background : currentcolor; + mask : /**/url("./check.svg") center no-repeat; + -webkit-mask: /**/url("./check.svg") center no-repeat; +} + +.tk.menu-item .label { + flex-grow: 1; +} + +.tk.menu-item:not([aria-expanded="true"], + [aria-disabled="true"], .pushed):hover > *, +.tk.menu-item:not([aria-expanded="true"], .pushed):focus > * { + border : 1px solid var(--tk-control-shadow); + box-shadow: 1px 1px 0 var(--tk-control-shadow); +} + +.tk.menu-item:focus > * { + background: var(--tk-control-active); +} + +.tk.menu-item.pushed > *, +.tk.menu-item[aria-expanded="true"] > * { + background: var(--tk-control-active); + border : 1px solid var(--tk-control-shadow); + box-shadow: none; + margin : 1px 0 0 1px; +} + +.tk.menu > [role="separator"] { + border : solid var(--tk-control-shadow); + border-width: 1px 0 0 0; + margin : 4px 2px; +} + + + +/*********************************** Radio ***********************************/ + +.tk.radio { + column-gap: 2px; +} + +.tk.radio .box { + border : 1px solid var(--tk-control-shadow); + border-radius: 50%; + color : var(--tk-control-text); + margin : 1px; +} + +.tk.radio:focus .box { + background: var(--tk-control-active); +} + +.tk.radio .box:before { + background : transparent; + border-radius: 50%; + content : ""; + display : block; + height : 4px; + margin : 2px; + width : 4px; +} + +.tk.radio[aria-checked="true"] .box:before { + background: currentcolor; +} + +.tk.radio.pushed .box:before { + background: var(--tk-control-shadow); +} + +.tk.radio[aria-disabled="true"] .box { + background: var(--tk-control); + color : var(--tk-control-shadow); +} +.tk.radio[aria-disabled="true"] .label { + color: var(--tk-control-shadow); +} + + + +/********************************* ScrollBar *********************************/ + +.tk.scroll-bar { + border : 1px solid var(--tk-control-shadow); + box-sizing: border-box; +} + +.tk.scroll-bar .unit-less, +.tk.scroll-bar .unit-more { + background: var(--tk-control); + border : 0 solid var(--tk-control-shadow); + color : var(--tk-control-text); + height : 11px; + width : 11px; +} +.tk.scroll-bar[aria-orientation="horizontal"] .unit-less { + border-right-width: 1px; +} +.tk.scroll-bar[aria-orientation="horizontal"] .unit-more { + border-left-width: 1px; +} +.tk.scroll-bar[aria-orientation="vertical"] .unit-less { + border-bottom-width: 1px; +} +.tk.scroll-bar[aria-orientation="vertical"] .unit-more { + border-top-width: 1px; +} + +.tk.scroll-bar .unit-less:before, +.tk.scroll-bar .unit-more:before { + background : currentColor; + content : ""; + display : block; + height : 100%; + mask : /**/url("./scroll.svg") center no-repeat; + -webkit-mask: /**/url("./scroll.svg") center no-repeat; + width : 100%; +} + +.tk.scroll-bar .unit-less.pushed:before, +.tk.scroll-bar .unit-more.pushed:before { + mask-size : 9px; + -webkit-mask-size: 9px; +} + +.tk.scroll-bar[aria-orientation="horizontal"] .unit-less:before { + transform: rotate(-90deg); +} +.tk.scroll-bar[aria-orientation="horizontal"] .unit-more:before { + transform: rotate(90deg); +} +.tk.scroll-bar[aria-orientation="vertical"] .unit-more:before { + transform: rotate(180deg); +} + +.tk.scroll-bar .track { + background: var(--tk-control-highlight); +} + +.tk.scroll-bar .thumb { + background: var(--tk-control); + box-shadow: 0 0 0 1px var(--tk-control-shadow); +} + +.tk.scroll-bar .block-less.pushed, +.tk.scroll-bar .block-more.pushed { + background: var(--tk-control-shadow); + opacity : 0.5; +} + +.tk.scroll-bar:focus .unit-less, +.tk.scroll-bar:focus .unit-more, +.tk.scroll-bar:focus .thumb { + background: var(--tk-control-active); +} + +.tk.scroll-bar[aria-disabled="true"] .unit-less, +.tk.scroll-bar[aria-disabled="true"] .unit-more, +.tk.scroll-bar.unneeded .unit-less, +.tk.scroll-bar.unneeded .unit-more, +.tk.scroll-bar[aria-disabled="true"] .thumb { + color: var(--tk-control-shadow); +} + +.tk.scroll-bar.unneeded .thumb { + visibility: hidden; +} + + + +/******************************** ScrollPane *********************************/ + +.tk.scroll-pane { + border: 1px solid var(--tk-control-shadow); +} + +.tk.scroll-pane > .scroll-bar[aria-orientation="horizontal"] { + border-width: 1px 1px 0 0; +} +.tk.scroll-pane:not(.vertical) > .scroll-bar[aria-orientation="horizontal"] { + border-width: 1px 0 0 0; +} +.tk.scroll-pane > .scroll-bar[aria-orientation="vertical"] { + border-width: 0 0 1px 1px; +} +.tk.scroll-pane:not(.horizontal) > .scroll-bar[aria-orientation="vertical"] { + border-width: 0 0 0 1px; +} + +.tk.scroll-pane > .viewport, +.tk.scroll-pane > .corner { + background: var(--tk-control); +} + + + +/********************************* SplitPane *********************************/ + +.tk.split-pane > [role="separator"]:focus { + background: var(--tk-splitter-focus); +} + +.tk.split-pane > .horizontal[role="separator"] { + width: 3px; +} +.tk.split-pane > .vertical[role="separator"] { + height: 3px; +} + + + +/********************************** TextBox **********************************/ + +.tk.text-box { + background : var(--tk-window); + border : 1px solid var(--tk-control-border); + color : var(--tk-window-text); + line-height: 1em; + height : calc(1em + 2px); + padding : 0; + margin : 0; + min-width : 0; +} + +.tk.text-box.[aria-disabled="true"] { + background: var(--tk-control-shadow); + color : var(--tk-window-text); +} + + + +/********************************** Windows **********************************/ + +.tk.window { + background: var(--tk-control); + border : 1px solid var(--tk-control-shadow); + box-shadow: 1px 1px 0 var(--tk-control-shadow); +} + +.tk.window:focus-within { + border : 1px solid var(--tk-control-border); + box-shadow: 1px 1px 0 var(--tk-control-border); +} + +.tk.window > .nw1 { left : -2px; top : -2px; width : 8px; height: 3px; } +.tk.window > .nw2 { left : -2px; top : 1px; width : 3px; height: 5px; } +.tk.window > .n { left : 6px; top : -2px; right : 6px; height: 3px; } +.tk.window > .ne1 { right: -2px; top : -2px; width : 8px; height: 3px; } +.tk.window > .ne2 { right: -2px; top : 1px; width : 3px; height: 5px; } +.tk.window > .w { left : -2px; top : 6px; bottom: 6px; width : 3px; } +.tk.window > .e { right: -2px; top : 6px; bottom: 6px; width : 3px; } +.tk.window > .sw1 { left : -2px; bottom: -2px; width : 8px; height: 3px; } +.tk.window > .sw2 { left : -2px; bottom: 1px; width : 3px; height: 5px; } +.tk.window > .s { left : 6px; bottom: -2px; right : 6px; height: 3px; } +.tk.window > .se1 { right: -2px; bottom: -2px; width : 8px; height: 3px; } +.tk.window > .se2 { right: -2px; bottom: 1px; width : 3px; height: 5px; } + +.tk.window > .title { + align-items : center; + background : var(--tk-window-blur-title); + border-bottom: 1px solid var(--tk-control-shadow); + color : var(--tk-window-blur-title-text); + padding : 1px; + user-select : none; +} + +.tk.window:focus-within > .title { + background: var(--tk-window-title); + color : var(--tk-window-title-text); +} + +.tk.window.two > .title { + background: var(--tk-window-blur-title2); +} + +.tk.window.two:focus-within > .title { + background: var(--tk-window-title2); +} + +.tk.window > .title .text { + cursor : default; + font-weight : bold; + overflow : hidden; + text-align : center; + text-overflow: ellipsis; + white-space : nowrap; +} + +.tk.window > .title .close-button { + background: var(--tk-window-blur-close); + border : 1px solid var(--tk-control-shadow); + box-sizing: border-box; + color : var(--tk-window-close-text); + height : 13px; + width : 13px; +} + +.tk.window:focus-within > .title .close-button { + background: var(--tk-window-close); +} + +.tk.window > .title .close-button:focus { + background: var(--tk-window-close-focus); + color : var(--tk-window-close-focus-text); + outline : 1px solid var(--tk-control); +} + +.tk.window > .title .close-button:before { + background : currentcolor; + content : ""; + display : block; + height : 11px; + mask : /**/url("./close.svg") center no-repeat; + -webkit-mask: /**/url("./close.svg") center no-repeat; + width : 11px; +} + +.tk.window > .title .close-button.pushed:before { + mask-size : 9px; + -webkit-mask-size: 9px; +} + +.tk.window > .client { + overflow: hidden; +} diff --git a/web/theme/light.css b/web/theme/light.css new file mode 100644 index 0000000..9239e4f --- /dev/null +++ b/web/theme/light.css @@ -0,0 +1,28 @@ +:root { + --tk-control : #eeeeee; + --tk-control-active : #cccccc; + --tk-control-border : #000000; + --tk-control-highlight : #f8f8f8; + --tk-control-shadow : #6c6c6c; + --tk-control-text : #000000; + --tk-desktop : #cccccc; + --tk-selected : #008542; + --tk-selected-blur : #5e7d70; + --tk-selected-blur-text : #ffffff; + --tk-selected-text : #ffffff; + --tk-splitter-focus : #008542c0; + --tk-window : #ffffff; + --tk-window-blur-close : #d9aeae; + --tk-window-blur-close-text : #eeeeee; + --tk-window-blur-title : #aac4d5; + --tk-window-blur-title2 : #dbc4b8; + --tk-window-blur-title-text : #444444; + --tk-window-close : #ee9999; + --tk-window-close-focus : #99ee99; + --tk-window-close-focus-text: #333333; + --tk-window-close-text : #ffffff; + --tk-window-text : #000000; + --tk-window-title : #80ccff; + --tk-window-title2 : #ffb894; + --tk-window-title-text : #000000; +} diff --git a/app/theme/radio.svg b/web/theme/radio.svg similarity index 100% rename from app/theme/radio.svg rename to web/theme/radio.svg diff --git a/app/theme/roboto.woff2 b/web/theme/roboto.woff2 similarity index 100% rename from app/theme/roboto.woff2 rename to web/theme/roboto.woff2 diff --git a/app/theme/scroll.svg b/web/theme/scroll.svg similarity index 100% rename from app/theme/scroll.svg rename to web/theme/scroll.svg diff --git a/web/theme/vbemu.css b/web/theme/vbemu.css new file mode 100644 index 0000000..eed45e5 --- /dev/null +++ b/web/theme/vbemu.css @@ -0,0 +1,223 @@ +/******************************** CPU Window *********************************/ + +.tk.window.cpu .client { + padding: 1px; +} + +.tk.window.cpu .scr-dasm { + border-right-width: 0; + box-shadow : 1px 0 0 var(--tk-control-shadow); +} + +.tk.window.cpu .scr-system { + border-width: 1px 1px 0 0; + box-shadow : -0.5px 0.5px 0 0.5px var(--tk-control-shadow); +} + +.tk.window.cpu .scr-program { + border-width: 0 1px 1px 0; + box-shadow : -0.5px -0.5px 0 0.5px var(--tk-control-shadow); +} + +.tk.window.cpu .disassembler { + background : var(--tk-window); + color : var(--tk-window-text); +} + +.tk.window.cpu .disassembler div { + cursor : default; + line-height: calc(1em + 2px); + user-select: none; +} + +.tk.window.cpu .disassembler .addr { + margin-left: 2px; +} + +.tk.window.cpu .disassembler .spacer { + margin-right: 2px; +} + +.tk.window.cpu .disassembler .byte { + margin-left: 0.5em; + text-align : center; +} + +.tk.window.cpu .disassembler .byte.b0, +.tk.window.cpu .disassembler .inst, +.tk.window.cpu .disassembler .ops { + margin-left: 1em; +} + + +.tk.window.cpu .disassembler .pc { + background: var(--tk-selected-blur); + box-shadow: 0 1px 0 var(--tk-selected-blur), + 0 -1px 0 var(--tk-selected-blur); + color : var(--tk-selected-blur-text); +} +.tk.window.cpu .disassembler:focus-within .pc { + background: var(--tk-selected); + box-shadow: 0 1px 0 var(--tk-selected), + 0 -1px 0 var(--tk-selected); + color : var(--tk-selected-text); +} + +.tk.window.cpu .disassembler .pc.addr { + box-shadow: 0 0 0 1px var(--tk-selected-blur); +} +.tk.window.cpu .disassembler:focus-within .pc.addr { + box-shadow: 0 0 0 1px var(--tk-selected); +} + +.tk.window.cpu .disassembler .pc:is(.byte) { + box-shadow: 0 0 0 1px var(--tk-selected-blur), + calc(-0.5em + 1px) 0 0 1px var(--tk-selected-blur); +} +.tk.window.cpu .disassembler:focus-within .pc:is(.byte) { + box-shadow: 0 0 0 1px var(--tk-selected), + calc(-0.5em + 1px) 0 0 1px var(--tk-selected); +} + +.tk.window.cpu .disassembler .pc:is(.byte.b0, .inst, .ops) { + box-shadow: 0 0 0 1px var(--tk-selected-blur), + calc(-1em + 1px) 0 0 1px var(--tk-selected-blur); +} +.tk.window.cpu .disassembler:focus-within .pc:is(.byte.b0, .inst, .ops) { + box-shadow: 0 0 0 1px var(--tk-selected), + calc(-1em + 1px) 0 0 1px var(--tk-selected); +} + +.tk.window.cpu .disassembler .spacer.pc { + box-shadow: 1px 1px 0 var(--tk-selected-blur), + 1px -1px 0 var(--tk-selected-blur); +} +.tk.window.cpu .disassembler:focus-within .spacer.pc { + box-shadow: 1px 1px 0 var(--tk-selected), + 1px -1px 0 var(--tk-selected); +} + +.tk.window.cpu .registers { + background: var(--tk-window); + color : var(--tk-window-text); +} + +.tk.window.cpu .registers > * { + padding: 0 1px; +} +.tk.window.cpu .registers > *:first-child { + padding-top: 1px; +} +.tk.window.cpu .registers > *:last-child { + padding-bottom: 1px; +} + +.tk.window.cpu .registers .icon { + border-radius: 2px; + margin : 0 1px 1px 0; +} + +.tk.window.cpu .registers .expand:focus .icon { + background: var(--tk-control-active); +} + +.tk.window.cpu .registers .expand .icon:before { + content: ""; + display: block; + height : 11px; + width : 11px; +} + +.tk.window.cpu .registers .expand[aria-expanded="false"] .icon:before { + background : currentcolor; + mask : /**/url("./expand.svg") center no-repeat; + -webkit-mask: /**/url("./expand.svg") center no-repeat; +} + +.tk.window.cpu .registers .expand[aria-expanded="true"] .icon:before { + background : currentcolor; + mask : /**/url("./collapse.svg") center no-repeat; + -webkit-mask: /**/url("./collapse.svg") center no-repeat; +} + +.tk.window.cpu .registers .expansion { + gap : 1px 1em; + padding: 2px 2px 2px 1.4em; +} + +.tk.window.cpu .registers .main { + column-gap: 0.5em; +} + +.tk.window.cpu .registers .text-box { + background: transparent; + border : none; + padding : 0 1px; +} + +.tk.window.cpu .registers .text-box:focus { + outline: 1px solid var(--tk-selected); +} + +.tk.window.cpu .registers .text-dec { + column-gap: 2px; +} + +.tk.window.cpu .registers .text-dec .label { + text-align: center; + min-width : 13px; +} + +.tk.window.cpu .registers *[aria-disabled="true"]:is(.label, .text-box) { + color: var(--tk-control-shadow); +} + + + +/******************************* Memory Window *******************************/ + +.tk.window.memory .client { + gap : 1px; + padding: 1px; +} + +.tk.window.memory .hex-editor { + align-items: center; + background : var(--tk-window); + color : var(--tk-window-text); +} + +.tk.window.memory .hex-editor div { + cursor : default; + line-height: calc(1em + 2px); + user-select: none; +} + +.tk.window.memory .hex-editor .addr { + margin-left: 2px; +} + +.tk.window.memory .hex-editor .byte { + margin-left: 0.5em; + text-align : center; +} + +.tk.window.memory .hex-editor .b0, +.tk.window.memory .hex-editor .b8 { + margin-left: 1em; +} + +.tk.window.memory .hex-editor .b15 { + margin-right: 2px; +} + +.tk.window.memory .hex-editor .edit { + background: var(--tk-selected-blur); + color : var(--tk-selected-blur-text); + outline : 1px solid var(--tk-selected-blur); +} +.tk.window.memory .hex-editor:focus-within .edit { + background: var(--tk-selected); + color : var(--tk-selected-text); + outline : 1px solid var(--tk-selected); +} diff --git a/web/theme/virtual.css b/web/theme/virtual.css new file mode 100644 index 0000000..a697a8f --- /dev/null +++ b/web/theme/virtual.css @@ -0,0 +1,63 @@ +:root { + --tk-control : #000000; + --tk-control-active : #550000; + --tk-control-border : #ff0000; + --tk-control-highlight : #550000; + --tk-control-shadow : #aa0000; + --tk-control-text : #ff0000; + --tk-desktop : #000000; + --tk-selected : #aa0000; + --tk-selected-blur : #550000; + --tk-selected-blur-text : #ff0000; + --tk-selected-text : #000000; + --tk-splitter-focus : #ff0000aa; + --tk-window : #000000; + --tk-window-blur-close : #000000; + --tk-window-blur-close-text : #aa0000; + --tk-window-blur-title : #000000; + --tk-window-blur-title2 : #000000; + --tk-window-blur-title-text : #aa0000; + --tk-window-close : #550000; + --tk-window-close-focus : #ff0000; + --tk-window-close-focus-text: #550000; + --tk-window-close-text : #ff0000; + --tk-window-text : #ff0000; + --tk-window-title : #550000; + --tk-window-title2 : #550000; + --tk-window-title-text : #ff0000; +} + +input, select { + filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v"); +} + +.tk.scroll-bar .unit-less, +.tk.scroll-bar .unit-more, +.tk.scroll-bar .thumb { + background: #550000; +} +.tk.scroll-bar .track { + background: #000000; +} + +.tk.scroll-bar:focus, +.tk.scroll-bar:focus .unit-less, +.tk.scroll-bar:focus .unit-more, +.tk.scroll-bar:focus .thumb { + background : #aa0000; + border-color: #ff0000; + color : #000000; +} +.tk.scroll-bar:focus .track { + background: #550000; +} +.tk.scroll-bar:focus .thumb { + box-shadow: 0 0 0 1px #ff0000; +} + +.tk.window { + box-shadow: 1px 1px 0 #550000; +} +.tk.window:focus-within { + box-shadow: 1px 1px 0 #aa0000; +} diff --git a/web/toolkit/App.js b/web/toolkit/App.js new file mode 100644 index 0000000..8eafa70 --- /dev/null +++ b/web/toolkit/App.js @@ -0,0 +1,249 @@ +let register = Toolkit => Toolkit.App = + +// Root application container and localization manager +class App extends Toolkit.Component { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(options) { + super(null, Object.assign({ + tabIndex: -1 + }, options)); + + // Configure instance fields + this.components = new Set(); + this.dragElement = null; + this.lastFocus = null; + this.locale = null; + this.locales = new Map(); + + // Configure event handlers + this.addEventListener("focusin", e=>this.onFocus(e)); + this.addEventListener("keydown", e=>this.onKey (e)); + this.addEventListener("keyup" , e=>this.onKey (e)); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Child element focus gained + onFocus(e) { + + // Error checking + if (e.target != document.activeElement) + return; + + // Target is self + if (e.target == this.element) + return this.restoreFocus(); + + // Ensure the child is not contained in a MenuBar + for (let elm = e.target; elm != this.element; elm = elm.parentNode) { + if (elm.getAttribute("role") == "menubar") + return; + } + + // Track the (non-menu) element as the most recent focused component + this.lastFocus = e.target; + } + + // Key press, key release + onKey(e) { + if (this.dragElement == null || e.rerouted) + return; + this.dragElement.dispatchEvent(Object.assign(new Event(e.type), { + altKey : e.altKey, + ctrlKey : e.ctrlKey, + key : e.key, + rerouted: true, + shiftKey: e.shiftKey + })); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Install a locale from URL + async addLocale(url) { + let data; + + // Load the file as JSON, using UTF-8 with or without a BOM + try { data = JSON.parse(new TextDecoder().decode( + await (await fetch(url)).arrayBuffer() )); } + catch { return null; } + + // Error checking + if (!data.id || !data.name) + return null; + + // Flatten the object to keys + let locale = new Map(); + let entries = Object.entries(data); + let stack = []; + while (entries.length != 0) { + let entry = entries.shift(); + + // The value is a non-array object + if (entry[1] instanceof Object && !Array.isArray(entry[1])) { + entries = entries.concat(Object.entries(entry[1]) + .map(e=>[ entry[0] + "." + e[0], e[1] ])); + } + + // The value is a primitive or array + else locale.set(entry[0].toLowerCase(), entry[1]); + } + + this.locales.set(data.id, locale); + return data.id; + } + + // Specify a localization dictionary + setLocale(id) { + if (!this.locales.has(id)) + return false; + this.locale = this.locales.get(id); + for (let comp of this.components) + comp.localize(); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Begin dragging on an element + get drag() { return this.dragElement; } + set drag(event) { + + // Begin dragging + if (event) { + this.dragElement = event.currentTarget; + this.dragPointer = event.pointerId; + this.dragElement.setPointerCapture(event.pointerId); + } + + // End dragging + else { + if (this.dragElement) + this.dragElement.releasePointerCapture(this.dragPointer); + this.dragElement = null; + this.dragPointer = null; + } + + } + + // Configure components for automatic localization, or localize a message + localize(a, b) { + return a instanceof Object ? this.localizeComponents(a, b) : + this.localizeMessage(a, b); + } + + // Return focus to the most recent focused element + restoreFocus() { + + // Error checking + if (!this.lastFocus) + return false; + + // Unable to restore focus + if (!this.isVisible(this.lastFocus)) + return false; + + // Transfer focus to the most recent element + this.lastFocus.focus({ preventScroll: true }); + return true; + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Configure components for automatic localization + localizeComponents(comps, add) { + + // Process all components + for (let comp of (Array.isArray(comps) ? comps : [comps])) { + + // Error checking + if ( + !(comp instanceof Toolkit.Component) || + !(comp.localize instanceof Function) + ) continue; + + // Update the collection and component text + this.components[add ? "add" : "delete"](comp); + comp.localize(); + } + + } + + // Localize a message + localizeMessage(message, substs, circle = new Set()) { + let parts = []; + + // Separate the substitution keys from the literal text + for (let x = 0;;) { + + // Locate the start of the next substitution key + let y = message.indexOf("{", x); + let z = y == -1 ? -1 : message.indexOf("}", y + 1); + + // No substitution key or malformed substitution expression + if (z == -1) { + parts.push(message.substring(z == -1 ? x : y)); + break; + } + + // Append the literal text and the substitution key + parts.push(message.substring(x, y), message.substring(y + 1, z)); + x = z + 1; + } + + // Process all substitutions + for (let x = 1; x < parts.length; x += 2) { + let key = parts[x].toLowerCase(); + let value; + + // The substitution key is already in the recursion chain + if (circle.has(key)) { + parts[x] = "{\u21ba" + key.toUpperCase() + "}"; + continue; + } + + // Resolve the substitution key from the argument + if (substs && substs.has(key)) { + value = substs.get(key); + + // Do not recurse for this substitution + if (!value[1]) { + parts[x] = value[0]; + continue; + } + + // Substitution text + value = value[0]; + } + + // Resolve the substitution from the current locale + else if (this.locale && this.locale.has(key)) + value = this.locale.get(key); + + // A matching substitution key was not found + else { + parts[x] = "{\u00d7" + key.toUpperCase() + "}"; + continue; + } + + // Perform recursive substitution + circle.add(key); + parts[x] = this.localizeMessage(value, substs, circle); + circle.delete(key); + } + + return parts.join(""); + } + +}; + +export { register }; diff --git a/web/toolkit/Button.js b/web/toolkit/Button.js new file mode 100644 index 0000000..932e8f0 --- /dev/null +++ b/web/toolkit/Button.js @@ -0,0 +1,119 @@ +let register = Toolkit => Toolkit.Button = + +// Push button +class Button extends Toolkit.Component { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, options = {}) { + super(app, options = Object.assign({ + class : "tk button", + role : "button", + tabIndex: "0" + }, options)); + + // Configure options + if ("disabled" in options) + this.disabled = options.disabled; + this.doNotFocus = !("doNotFocus" in options) || options.doNotFocus; + + // Display text + this.content = new Toolkit.Label(app); + this.add(this.content); + + // Event handlers + this.addEventListener("keydown" , e=>this.onKeyDown (e)); + this.addEventListener("pointerdown", e=>this.onPointerDown(e)); + this.addEventListener("pointermove", e=>this.onPointerMove(e)); + this.addEventListener("pointerup" , e=>this.onPointerUp (e)); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Key press + onKeyDown(e) { + if ( + !(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) && + (e.key == " " || e.key == "Enter") + ) this.activate(); + } + + // Pointer down + onPointerDown(e) { + + // Gain focus + if ( + !this.doNotFocus && + this.isFocusable() && + this.element != document.activeElement + ) this.element.focus(); + else e.preventDefault(); + + // Do not drag + if ( + e.button != 0 || + this.disabled || + this.element.hasPointerCapture(e.pointerId) + ) return; + + // Begin dragging + this.element.setPointerCapture(e.pointerId); + this.element.classList.add("pushed"); + Toolkit.handle(e); + } + + // Pointer move + onPointerMove(e) { + + // Do not drag + if (!this.element.hasPointerCapture(e.pointerId)) + return; + + // Process dragging + this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed"); + Toolkit.handle(e); + } + + // Pointer up + onPointerUp(e) { + + // Do not activate + if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId)) + return; + + // End dragging + this.element.releasePointerCapture(e.pointerId); + this.element.classList.remove("pushed"); + Toolkit.handle(e); + + // Activate the button if applicable + if (this.isWithin(e)) + this.activate(); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Simulate a click on the button + activate() { + if (!this.disabled) + this.element.dispatchEvent(new Event("action")); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Update localization strings + localize() { + this.localizeText(this.content); + this.localizeLabel(); + this.localizeTitle(); + } + +} + +export { register }; diff --git a/web/toolkit/Checkbox.js b/web/toolkit/Checkbox.js new file mode 100644 index 0000000..5c5c933 --- /dev/null +++ b/web/toolkit/Checkbox.js @@ -0,0 +1,144 @@ +let register = Toolkit => Toolkit.Checkbox = + +// Check box +class Checkbox extends Toolkit.Component { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, options = {}) { + super(app, options = Object.assign({ + class : "tk checkbox", + role : "checkbox", + tabIndex: "0" + }, options, { style: Object.assign({ + alignItems : "center", + display : "inline-grid", + gridTemplateColumns: "max-content auto" + }, options.style || {}) })); + + // Configure element + this.element.setAttribute("aria-checked", "false"); + this.addEventListener("keydown" , e=>this.onKeyDown (e)); + this.addEventListener("pointerdown", e=>this.onPointerDown(e)); + this.addEventListener("pointermove", e=>this.onPointerMove(e)); + this.addEventListener("pointerup" , e=>this.onPointerUp (e)); + + // Icon area + this.box = document.createElement("div"); + this.box.className = "tk box"; + this.append(this.box); + + // Display text + this.uiLabel = new Toolkit.Label(app); + this.add(this.uiLabel); + + // Configure options + this.checked = options.checked; + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Key press + onKeyDown(e) { + if ( + !(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) && + (e.key == " " || e.key == "Enter") + ) this.setChecked(!this.checked); + } + + // Pointer down + onPointerDown(e) { + + // Gain focus + if (!this.disabled) + this.element.focus(); + else e.preventDefault(); + + // Do not drag + if ( + e.button != 0 || + this.disabled || + this.element.hasPointerCapture(e.pointerId) + ) return; + + // Begin dragging + this.element.setPointerCapture(e.pointerId); + this.element.classList.add("pushed"); + Toolkit.handle(e); + } + + // Pointer move + onPointerMove(e) { + + // Do not drag + if (!this.element.hasPointerCapture(e.pointerId)) + return; + + // Process dragging + this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed"); + Toolkit.handle(e); + } + + // Pointer up + onPointerUp(e) { + + // Do not activate + if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId)) + return; + + // End dragging + this.element.releasePointerCapture(e.pointerId); + this.element.classList.remove("pushed"); + Toolkit.handle(e); + + // Activate the check box if applicable + if (this.isWithin(e)) + this.setChecked(!this.checked); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // The check box is checked + get checked() { return this.element.getAttribute("aria-checked")=="true"; } + set checked(checked) { + checked = !!checked; + if (checked == this.checked) + return; + this.element.setAttribute("aria-checked", checked); + } + + // Specify the display text + setText(text, localize) { + this.uiLabel.setText(text, localize); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Update localization strings + localize() { + this.uiLabel.localize(); + this.localizeTitle(); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Specify the checked state + setChecked(checked) { + checked = !!checked; + if (checked == this.checked) + return; + this.checked = checked; + this.element.dispatchEvent(new Event("input")); + } + +} + +export { register }; diff --git a/web/toolkit/Component.js b/web/toolkit/Component.js new file mode 100644 index 0000000..1c62414 --- /dev/null +++ b/web/toolkit/Component.js @@ -0,0 +1,473 @@ +let register = Toolkit => Toolkit.Component = + +// Base class from which all toolkit components are derived +class Component { + + //////////////////////////////// Constants //////////////////////////////// + + // Non-attributes + static NON_ATTRIBUTES = new Set([ + "checked", "disabled", "doNotFocus", "group", "hover", "max", "min", + "name", "orientation", "overflowX", "overflowY", "tag", "text", + "value", "view", "visibility", "visible" + ]); + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, options = {}) { + + // Configure element + this.element = document.createElement(options.tag || "div"); + this.element.component = this; + for (let entry of Object.entries(options)) { + if ( + Toolkit.Component.NON_ATTRIBUTES.has(entry[0]) || + entry[0] == "type" && options.tag != "input" + ) continue; + if (entry[0] == "style" && entry[1] instanceof Object) + Object.assign(this.element.style, entry[1]); + else this.element.setAttribute(entry[0], entry[1]); + } + + // Configure instance fields + this._isLocalized = false; + this.app = app; + this.display = options.style && options.style.display; + this.style = this.element.style; + this.text = null; + this.visibility = !!options.visibility; + this.visible = !("visible" in options) || options.visible; + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Add a child component + add(comp) { + + // Error checking + if ( + !(comp instanceof Toolkit.Component) || + comp instanceof Toolkit.App || + comp.app != (this.app || this) + ) return false; + + // No components have been added yet + if (!this.children) + this.children = []; + + // The child already has a parent: remove it + if (comp.parent) { + comp.parent.children.splice( + comp.parent.children.indexOf(comp), 1); + } + + // Add the component to self + this.children.push(comp); + this.append(comp.element); + comp.parent = this; + return true; + } + + // Register an event listener on the element + addEventListener(type, listener) { + + // No event listeners have been registered yet + if (!this.listeners) + this.listeners = new Map(); + if (!this.listeners.has(type)) + this.listeners.set(type, []); + + // The listener has already been registered for this event + let listeners = this.listeners.get(type); + if (listeners.indexOf(listener) != -1) + return listener; + + // Resize events are implemented by a ResizeObserver + if (type == "resize") { + if (!this.resizeObserver) { + this.resizeObserver = new ResizeObserver(()=> + this.element.dispatchEvent(new Event("resize"))); + this.resizeObserver.observe(this.element); + } + } + + // Visibility events are implemented by an IntersectionObserver + else if (type == "visibility") { + if (!this.visibilityObserver) { + this.visibilityObserver = new IntersectionObserver( + ()=>this.element.dispatchEvent(Object.assign( + new Event("visibility"), + { visible: this.isVisible() } + )), + { root: document.body } + ); + this.visibilityObserver.observe(this.element); + } + } + + // Register the listener with the element + listeners.push(listener); + this.element.addEventListener(type, listener); + return listener; + } + + // Component cannot be interacted with + get disabled() { return this.element.hasAttribute("disabled"); } + set disabled(disabled) { this.setDisabled(disabled); } + + // Move focus into the component + focus() { + this.element.focus({ preventScroll: true }); + } + + // Specify whether the component is localized + get isLocalized() { return this._isLocalized; } + set isLocalized(isLocalized) { + if (isLocalized == this._isLocalized) + return; + this._isLocalized = isLocalized; + (this instanceof Toolkit.App ? this : this.app) + .localize(this, isLocalized); + } + + // Determine whether an element is actually visible + isVisible(element = this.element) { + if (!document.body.contains(element)) + return false; + for (; element instanceof Element; element = element.parentNode) { + let style = getComputedStyle(element); + if (style.display == "none" || style.visibility == "hidden") + return false; + } + return true; + } + + // Produce an ordered list of registered event listeners for an event type + listEventListeners(type) { + return this.listeners && this.listeners.has(type) && + this.listeners.get(type).list.slice() || []; + } + + // Remove a child component + remove(comp) { + if (comp.parent != this || !this.children) + return false; + let index = this.children.indexOf(comp); + if (index == -1) + return false; + this.children.splice(index, 1); + comp.element.remove(); + comp.parent = null; + return true; + } + + // Unregister an event listener from the element + removeEventListener(type, listener) { + + // Not listening to events of the specified type + if (!this.listeners || !this.listeners.has(type)) + return listener; + + // Listener is not registered + let listeners = this.listeners.get(type); + let index = listeners.indexOf(listener); + if (index == -1) + return listener; + + // Unregister the listener + this.element.removeEventListener(listener); + listeners.splice(index, 1); + + // Delete the ResizeObserver + if ( + type == "resize" && + listeners.list.length == 0 && + this.resizeObserver + ) { + this.resizeObserver.disconnect(); + delete this.resizeObserver; + } + + // Delete the IntersectionObserver + else if ( + type == "visibility" && + listeners.list.length == 0 && + this.visibilityObserver + ) { + this.visibilityObserver.disconnect(); + delete this.visibilityObserver; + } + + return listener; + } + + // Specify accessible name + setLabel(text, localize) { + + // Label is another component + if ( + text instanceof Toolkit.Component || + text instanceof HTMLElement + ) { + this.element.setAttribute("aria-labelledby", + (text.element || text).id); + this.setString("label", null, false); + } + + // Label is the given text + else { + this.element.removeAttribute("aria-labelledby"); + this.setString("label", text, localize); + } + + } + + // Specify role description text + setRoleDescription(text, localize) { + this.setString("roleDescription", text, localize); + } + + // Specify inner text + setText(text, localize) { + this.setString("text", text, localize); + } + + // Specify tooltip text + setTitle(text, localize) { + this.setString("title", text, localize); + } + + // Specify substitution text + substitute(key, text = null, recurse = false) { + if (text === null) { + if (this.substitutions.has(key)) + this.substitutions.delete(key); + } else this.substitutions.set(key, [ text, recurse ]); + if (this.localize instanceof Function) + this.localize(); + } + + // Determine whether the element wants to be visible + get visible() { + let style = this.element.style; + return style.display != "none" && style.visibility != "hidden"; + } + + // Specify whether the element is visible + set visible(visible) { + visible = !!visible; + + // Visibility is not changing + if (visible == this.visible) + return; + + let comps = [ this ].concat( + Array.from(this.element.querySelectorAll("*")) + .map(c=>c.component) + ).filter(c=> + c instanceof Toolkit.Component && + c.listeners && + c.listeners.has("visibility") + ) + ; + let prevs = comps.map(c=>c.isVisible()); + + // Allow the component to be shown + if (visible) { + if (!this.visibility) { + if (this.display) + this.element.style.display = this.display; + else this.element.style.removeProperty("display"); + } else this.element.style.removeProperty("visibility"); + } + + // Prevent the component from being shown + else { + this.element.style.setProperty( + this.visibility ? "visibility" : "display", + this.visibility ? "hidden" : "none" + ); + } + + for (let x = 0; x < comps.length; x++) { + let comp = comps[x]; + visible = comp.isVisible(); + if (visible == prevs[x]) + continue; + comp.element.dispatchEvent(Object.assign( + new Event("visibility"), + { visible: visible } + )); + } + + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Add a child component to the primary client region of this component + append(element) { + this.element.append(element instanceof Toolkit.Component ? + element.element : element); + } + + // Determine whether a component or element is a child of this component + contains(child) { + return this.element.contains(child instanceof Toolkit.Component ? + child.element : child); + } + + // Generate a list of focusable descendant elements + getFocusable(element = this.element) { + let cache; + return Array.from(element.querySelectorAll( + "*:is(a[href],area,button,details,input,textarea,select," + + "[tabindex='0']):not([disabled])" + )).filter(e=>{ + for (; e instanceof Element; e = e.parentNode) { + let style = + (cache || (cache = new Map())).get(e) || + cache.set(e, getComputedStyle(e)).get(e) + ; + if (style.display == "none" || style.visibility == "hidden") + return false; + } + return true; + }); + } + + // Specify the inner text of the primary client region of this component + get innerText() { return this.element.textContent; } + set innerText(text) { this.element.innerText = text; } + + // Determine whether an element is focusable + isFocusable(element = this.element) { + return element.matches( + ":is(a[href],area,button,details,input,textarea,select," + + "[tabindex='0'],[tabindex='-1']):not([disabled])" + ); + } + + // Determine whether a pointer event is within the element + isWithin(e, element = this.element) { + let bounds = element.getBoundingClientRect(); + return ( + e.clientX >= bounds.left && e.clientX < bounds.right && + e.clientY >= bounds.top && e.clientY < bounds.bottom + ); + } + + // Common processing for localizing the accessible name + localizeLabel(element = this.element) { + + // There is no label or the label is another element + if (!this.label || element.hasAttribute("aria-labelledby")) { + element.removeAttribute("aria-label"); + return; + } + + // Localize the label + let text = this.label; + text = !text[1] ? text[0] : + this.app.localize(text[0], this.substitutions); + element.setAttribute("aria-label", text); + } + + // Common processing for localizing the accessible role description + localizeRoleDescription(element = this.element) { + + // There is no role description + if (!this.roleDescription) { + element.removeAttribute("aria-roledescription"); + return; + } + + // Localize the role description + let text = this.roleDescription; + text = !text[1] ? text[0] : + this.app.localize(text[0], this.substitutions); + element.setAttribute("aria-roledescription", text); + } + + // Common processing for localizing inner text + localizeText(element = this.element) { + + // There is no title + if (!this.text) { + element.innerText = ""; + return; + } + + // Localize the text + let text = this.text; + text = !text[1] ? text[0] : + this.app.localize(text[0], this.substitutions); + element.innerText = text; + } + + // Common processing for localizing the tooltip text + localizeTitle(element = this.element) { + + // There is no title + if (!this.title) { + element.removeAttribute("title"); + return; + } + + // Localize the title + let text = this.title; + text = !text[1] ? text[0] : + this.app.localize(text[0], this.substitutions); + element.setAttribute("title", text); + } + + // Common handler for configuring whether the component is disabled + setDisabled(disabled, element = this.element) { + element[disabled ? "setAttribute" : "removeAttribute"] + ("disabled", ""); + element.setAttribute("aria-disabled", disabled ? "true" : "false"); + } + + // Specify display text + setString(key, value, localize = true) { + + // There is no method to update the display text + if (!(this.localize instanceof Function)) + return; + + // Working variables + let app = this instanceof Toolkit.App ? this : this.app; + + // Remove the string + if (value === null) { + if (app && this[key] != null && this[key][1]) + app.localize(this, false); + this[key] = null; + } + + // Set or replace the string + else { + if (app && localize && (this[key] == null || !this[key][1])) + app.localize(this, true); + this[key] = [ value, localize ]; + } + + // Update the display text + this.localize(); + } + + // Retrieve the substitutions map + get substitutions() { + if (!this._substitutions) + this._substitutions = new Map(); + return this._substitutions; + } + +}; + +export { register }; diff --git a/web/toolkit/Desktop.js b/web/toolkit/Desktop.js new file mode 100644 index 0000000..16815bb --- /dev/null +++ b/web/toolkit/Desktop.js @@ -0,0 +1,86 @@ +let register = Toolkit => Toolkit.Desktop = + +// Layered window manager +class Desktop extends Toolkit.Component { + + //////////////////////////////// Constants //////////////////////////////// + + // Comparator for ordering child windows + static CHILD_ORDER(a, b) { + return b.element.style.zIndex - a.element.style.zIndex; + } + + + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, options = {}) { + super(app, Object.assign({ + class: "tk desktop" + }, options, { style: Object.assign({ + position: "relative", + zIndex : "0" + }, options.style || {})} )); + + // Configure event listeners + this.addEventListener("resize", e=>this.onResize()); + } + + + + ///////////////////////////// Event Handlers ////////////////////////////// + + // Element resized + onResize() { + + // The element is hidden: its size is indeterminate + if (!this.isVisible()) + return; + + // Don't allow children to be out-of-frame + if (this.children != null) { + for (let child of this.children) { + child.left = child.left; + child.top = child.top; + } + } + + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Add a child component + add(comp) { + super.add(comp); + this.bringToFront(comp); + } + + // Retrieve the foremost visible window + getActiveWindow() { + if (this.children != null) { + for (let child of this.children) { + if (child.isVisible()) + return child; + } + } + return null; + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Reorder children so that a particular child is in front + bringToFront(child) { + this.children.splice(this.children.indexOf(child), 1); + this.children.push(child); + let z = 1 - this.children.length; + for (let child of this.children) + child.element.style.zIndex = z++; + } + +} + +export { register }; diff --git a/web/toolkit/DropDown.js b/web/toolkit/DropDown.js new file mode 100644 index 0000000..e95bdb2 --- /dev/null +++ b/web/toolkit/DropDown.js @@ -0,0 +1,154 @@ +let register = Toolkit => Toolkit.DropDown = + +class DropDown extends Toolkit.Component { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(app, options = {}) { + super(app, Object.assign({ + class: "tk drop-down", + tag : "select" + }, options)); + + // Configure instance fields + this.items = []; + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Add an item + add(text, localize, value) { + + // Record the item data + this.items.push([ text, localize, value ]); + + // Add an