From 3cf006ba13116a3f721c18f8f155c91db57a571d Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Thu, 14 Apr 2022 20:51:09 -0500 Subject: [PATCH] Full front-end rewrite --- app/App.js | 184 --- app/Bundle.java | 10 +- app/Debugger.js | 54 - app/Emulator.js | 161 -- app/_boot.js | 514 +++--- app/app/App.js | 444 ++++++ app/app/CPU.js | 106 ++ app/app/Debugger.js | 187 +++ app/app/Disassembler.js | 892 +++++++++++ app/app/Memory.js | 467 ++++++ app/app/RegisterList.js | 863 +++++++++++ app/app/Util.js | 44 + app/core/Core.js | 196 +++ app/core/CoreWorker.js | 378 +++++ app/core/Sim.js | 151 ++ app/locale/en-US.js | 44 - app/locale/en-US.json | 74 + app/main.js | 34 + app/template.html | 4 +- app/theme/check.svg | 6 + app/theme/close.svg | 6 + app/theme/collapse.svg | 6 + app/theme/dark.css | 51 +- app/theme/expand.svg | 7 + ...xpanded-Medium.woff2 => inconsolata.woff2} | Bin app/theme/kiosk.css | 1161 ++++++++------ app/theme/light.css | 49 +- app/theme/radio.svg | 6 + .../{Roboto-Regular.woff2 => roboto.woff2} | Bin app/theme/scroll.svg | 6 + app/theme/virtual.css | 91 +- app/toolkit/Application.js | 208 --- app/toolkit/Button.js | 517 ++++--- app/toolkit/ButtonGroup.js | 44 - app/toolkit/CheckBox.js | 190 --- app/toolkit/Component.js | 379 +++-- app/toolkit/Label.js | 80 - app/toolkit/ListBox.js | 234 --- app/toolkit/Menu.js | 225 --- app/toolkit/MenuBar.js | 837 ++++++++-- app/toolkit/MenuItem.js | 320 ---- app/toolkit/Panel.js | 354 ----- app/toolkit/RadioButton.js | 59 - app/toolkit/ScrollBar.js | 1149 ++++++++++++++ app/toolkit/Splitter.js | 301 ---- app/toolkit/TextBox.js | 215 +-- app/toolkit/Toolkit.js | 330 +++- app/toolkit/Window.js | 999 +++++++----- app/windows/CPUWindow.js | 667 -------- app/windows/Disassembler.js | 388 ----- app/windows/MemoryWindow.js | 502 ------ app/windows/Register.js | 520 ------- core/bus.c | 32 +- core/cpu.c | 1372 ++++++++--------- core/vb.c | 265 ++-- core/vb.h | 48 +- license.txt | 2 +- makefile | 14 +- wasm/wasm.c | 205 ++- 59 files changed, 9424 insertions(+), 7228 deletions(-) delete mode 100644 app/App.js delete mode 100644 app/Debugger.js delete mode 100644 app/Emulator.js create mode 100644 app/app/App.js create mode 100644 app/app/CPU.js create mode 100644 app/app/Debugger.js create mode 100644 app/app/Disassembler.js create mode 100644 app/app/Memory.js create mode 100644 app/app/RegisterList.js create mode 100644 app/app/Util.js create mode 100644 app/core/Core.js create mode 100644 app/core/CoreWorker.js create mode 100644 app/core/Sim.js delete mode 100644 app/locale/en-US.js create mode 100644 app/locale/en-US.json create mode 100644 app/main.js create mode 100644 app/theme/check.svg create mode 100644 app/theme/close.svg create mode 100644 app/theme/collapse.svg create mode 100644 app/theme/expand.svg rename app/theme/{Inconsolata_SemiExpanded-Medium.woff2 => inconsolata.woff2} (100%) create mode 100644 app/theme/radio.svg rename app/theme/{Roboto-Regular.woff2 => roboto.woff2} (100%) create mode 100644 app/theme/scroll.svg delete mode 100644 app/toolkit/Application.js delete mode 100644 app/toolkit/ButtonGroup.js delete mode 100644 app/toolkit/CheckBox.js delete mode 100644 app/toolkit/Label.js delete mode 100644 app/toolkit/ListBox.js delete mode 100644 app/toolkit/Menu.js delete mode 100644 app/toolkit/MenuItem.js delete mode 100644 app/toolkit/Panel.js delete mode 100644 app/toolkit/RadioButton.js create mode 100644 app/toolkit/ScrollBar.js delete mode 100644 app/toolkit/Splitter.js delete mode 100644 app/windows/CPUWindow.js delete mode 100644 app/windows/Disassembler.js delete mode 100644 app/windows/MemoryWindow.js delete mode 100644 app/windows/Register.js diff --git a/app/App.js b/app/App.js deleted file mode 100644 index 5f7cd5c..0000000 --- a/app/App.js +++ /dev/null @@ -1,184 +0,0 @@ -"use strict"; - -// Top-level state and UI manager -globalThis.App = class App { - - // Produce a new class instance - static async create() { - let ret = new App(); - await ret.init(); - return ret; - } - - // Perform initial tasks - async init() { - - // Initialization tasks - this.initEnv(); - this.initMenus(); - - // WebAssembly core module - this.core = new Worker(Bundle.get("app/Emulator.js").toDataURL()); - await new Promise((resolve,reject)=>{ - this.core.onmessage = e=>{ - this.core.onmessage = e=>this.onmessage(e.data); - resolve(); - }; - this.core.postMessage({ - command: "Init", - wasm : Bundle.get("core.wasm").data.buffer - }); - }); - - // Desktop pane - this.desktop = this.gui.newPanel({ layout: "desktop" }); - this.desktop.setRole("group"); - this.desktop.element.setAttribute("desktop", ""); - this.gui.add(this.desktop); - - // Debugging windows - this.debuggers = [ - new Debugger(this, 0), - new Debugger(this, 1) - ]; - - } - - // Initialize environment settings - initEnv() { - - // Configure themes - Bundle.get("app/theme/kiosk.css").style(); - this.themes = { - dark : Bundle.get("app/theme/dark.css" ).style(false), - light : Bundle.get("app/theme/light.css" ).style(true ), - virtual: Bundle.get("app/theme/virtual.css").style(false) - }; - this.theme = this.themes["light"]; - - // Produce toolkit instance - this.gui = new Toolkit.Application({ - layout: "grid", - rows : "max-content auto" - }); - document.body.appendChild(this.gui.element); - window.addEventListener("resize", ()=>{ - this.gui.setSize(window.innerWidth+"px", window.innerHeight+"px"); - }); - window.dispatchEvent(new Event("resize")); - - // Configure locales - this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString()); - this.gui.setLocale(navigator.language); - } - - // Initialize main menu bar - initMenus() { - - // Menu bar - this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" }); - this.gui.add(this.mainMenu); - this.gui.addPropagationListener(e=>this.mainMenu.restoreFocus()); - - // File menu - let menu = this.mainMenu.newMenu({ text: "{menu.file._}"}); - let item = menu.newMenuItem({ text: "{menu.file.loadROM}"}); - item.addClickListener(()=>this.loadROM()); - - // Debug menu - menu = this.mainMenu.newMenu({ text: "{menu.debug._}" }); - item = menu.newMenuItem({ text: "{memory._}" }); - item.addClickListener( - ()=>this.debuggers[0].memory.setVisible(true, true)); - item = menu.newMenuItem({ text: "{cpu._}" }); - item.addClickListener( - ()=>this.debuggers[0].cpu.setVisible(true, true)); - - // Theme menu - menu = this.mainMenu.newMenu({ text: "{menu.theme._}" }); - item = menu.newMenuItem({ text: "{menu.theme.light}" }); - item.addClickListener(()=>this.setTheme("light")); - item = menu.newMenuItem({ text: "{menu.theme.dark}" }); - item.addClickListener(()=>this.setTheme("dark")); - item = menu.newMenuItem({ text: "{menu.theme.virtual}" }); - item.addClickListener(()=>this.setTheme("virtual")); - } - - - ///////////////////////////// Message Methods ///////////////////////////// - - // Message received - onmessage(msg) { - if ("dbgwnd" in msg) { - this.debuggers[msg.sim].message(msg); - return; - } - switch (msg.command) { - case "SetROM": this.setROM(msg); break; - } - } - - // ROM buffer has been configured - setROM(msg) { - let dbg = this.debuggers[msg.sim]; - dbg.refresh(true); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Prompt the user to select a ROM file - loadROM() { - let file = document.createElement("input"); - file.type = "file"; - file.addEventListener("input", ()=>this.selectROM(file.files[0])); - file.click(); - } - - // Specify a ROM file - async selectROM(file) { - - // No file is specified (perhaps the user canceled) - if (file == null) - return; - - // Check the file's size - if ( - file.size < 1024 || - file.size > 0x1000000 || - (file.size - 1 & file.size) != 0 - ) { - alert(this.gui.translate("{app.romNotVB}")); - return; - } - - // Load the file data into a byte buffer - let filename = file.name; - try { file = await file.arrayBuffer(); } - catch { - alert(this.gui.translate("{app.readFileError}")); - return; - } - - // Send the ROM to the WebAssembly core module - this.core.postMessage({ - command: "SetROM", - reset : true, - rom : file, - sim : 0 - }, file); - } - - // Specify the current color theme - setTheme(key) { - let theme = this.themes[key]; - if (theme == this.theme) - return; - let old = this.theme; - this.theme = theme; - theme.setEnabled(true); - old.setEnabled(false); - } - -}; diff --git a/app/Bundle.java b/app/Bundle.java index abef5dc..efa3aa9 100644 --- a/app/Bundle.java +++ b/app/Bundle.java @@ -98,12 +98,12 @@ public class Bundle { var manifest = new StringBuilder(); manifest.append("\"use strict\";\nlet manifest=["); for (var file : values) { - manifest.append("{" + - "name:\"" + file.filename + "\"," + - "size:" + file.data.length + - "},"); + manifest.append( + "[\"" + file.filename + "\"," + file.data.length + "]"); + if (file != values[values.length - 1]) + manifest.append(","); } - manifest.append("];\nlet bundleName=\"" + bundleName + "\";\n"); + manifest.append("],bundleName=\"" + bundleName + "\";"); // Prepend the manifest to _boot.js var boot = files.get("app/_boot.js"); diff --git a/app/Debugger.js b/app/Debugger.js deleted file mode 100644 index 2b917b7..0000000 --- a/app/Debugger.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; - -// Debugging UI manager -globalThis.Debugger = class Debugger { - - // Object constructor - constructor(app, sim) { - - // Configure instance fields - this.app = app; - this.core = app.core; - this.gui = app.gui; - this.sim = sim; - - // Memory window - this.memory = new MemoryWindow(this, { - title : "{sim}{memory._}", - height : 300, - visible: false, - width : 400 - }); - this.memory.addCloseListener(e=>this.memory.setVisible(false)); - app.desktop.add(this.memory); - - // CPU window - this.cpu = new CPUWindow(this, { - title : "{sim}{cpu._}", - height : 300, - visible: false, - width : 400 - }); - this.cpu.addCloseListener(e=>this.cpu.setVisible(false)); - app.desktop.add(this.cpu); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Message received from emulation thread - message(msg) { - switch (msg.dbgwnd) { - case "CPU" : this.cpu .message(msg); break; - case "Memory": this.memory.message(msg); break; - } - } - - // Reload all output - refresh(seekToPC) { - this.cpu .refresh(seekToPC); - this.memory.refresh(); - } - -}; diff --git a/app/Emulator.js b/app/Emulator.js deleted file mode 100644 index d1c35fb..0000000 --- a/app/Emulator.js +++ /dev/null @@ -1,161 +0,0 @@ -"use strict"; - -// Worker that manages a WebAssembly instance of the C core library -(globalThis.Emulator = class Emulator { - - // Static initializer - static initializer() { - new Emulator(); - } - - // Object constructor - constructor() { - - // Configure instance fields - this.buffers = {}; - - // Configure message port - onmessage = e=>this.onmessage(e.data); - } - - - - ///////////////////////////// Message Methods ///////////////////////////// - - // Message received - onmessage(msg) { - switch (msg.command) { - case "GetRegisters": this.getRegisters(msg); break; - case "Init" : this.init (msg); break; - case "ReadBuffer" : this.readBuffer (msg); break; - case "RunNext" : this.runNext (msg); break; - case "SetRegister" : this.setRegister (msg); break; - case "SetROM" : this.setROM (msg); break; - case "SingleStep" : this.singleStep (msg); break; - case "Write" : this.write (msg); break; - } - } - - // Retrieve the values of all the CPU registers - getRegisters(msg) { - msg.pc = this.core.GetProgramCounter(msg.sim, 0) >>> 0; - msg.pcFrom = this.core.GetProgramCounter(msg.sim, 1) >>> 0; - msg.pcTo = this.core.GetProgramCounter(msg.sim, 2) >>> 0; - msg.adtre = this.core.GetSystemRegister(msg.sim, 25) >>> 0; - msg.chcw = this.core.GetSystemRegister(msg.sim, 24) >>> 0; - msg.ecr = this.core.GetSystemRegister(msg.sim, 4) >>> 0; - msg.eipc = this.core.GetSystemRegister(msg.sim, 0) >>> 0; - msg.eipsw = this.core.GetSystemRegister(msg.sim, 1) >>> 0; - msg.fepc = this.core.GetSystemRegister(msg.sim, 2) >>> 0; - msg.fepsw = this.core.GetSystemRegister(msg.sim, 3) >>> 0; - msg.pir = this.core.GetSystemRegister(msg.sim, 6) >>> 0; - msg.psw = this.core.GetSystemRegister(msg.sim, 5) >>> 0; - msg.tkcw = this.core.GetSystemRegister(msg.sim, 7) >>> 0; - msg.sr29 = this.core.GetSystemRegister(msg.sim, 29) >>> 0; - msg.sr30 = this.core.GetSystemRegister(msg.sim, 30) >>> 0; - msg.sr31 = this.core.GetSystemRegister(msg.sim, 31) >>> 0; - msg.program = new Array(32); - for (let x = 0; x <= 31; x++) - msg.program[x] = this.core.GetProgramRegister(msg.sim, x); - postMessage(msg); - } - - // Initialize the WebAssembly core module - async init(msg) { - - // Load and instantiate the WebAssembly module - this.wasm = await WebAssembly.instantiate(msg.wasm, - { env: { emscripten_notify_memory_growth: ()=>this.onmemory() }}); - this.wasm.instance.exports.Init(); - - // Configure instance fields - this.core = this.wasm.instance.exports; - - postMessage({ command: "Init" }); - } - - // Read multiple data units from the bus - readBuffer(msg) { - let buffer = this.malloc(Uint8Array, msg.size); - this.core.ReadBuffer(msg.sim, buffer.pointer, - msg.address, msg.size, msg.debug ? 1 : 0); - msg.buffer = this.core.memory.buffer.slice( - buffer.pointer, buffer.pointer + msg.size); - this.free(buffer); - msg.pc = this.core.GetProgramCounter(msg.sim) >>> 0; - postMessage(msg, msg.buffer); - } - - // Attempt to advance to the next instruction - runNext(msg) { - this.core.RunNext(msg.sim); - postMessage(msg); - } - - // Specify a new value for a register - setRegister(msg) { - switch (msg.type) { - case "pc" : msg.value = - this.core.SetProgramCounter (msg.sim, msg.value); - break; - case "program": msg.value = - this.core.SetProgramRegister(msg.sim, msg.id, msg.value); - break; - case "system" : msg.value = - this.core.SetSystemRegister (msg.sim, msg.id, msg.value); - } - postMessage(msg); - } - - // Supply a ROM buffer - setROM(msg) { - let rom = new Uint8Array(msg.rom); - let buffer = this.malloc(Uint8Array, rom.length); - for (let x = 0; x < rom.length; x++) - buffer.data[x] = rom[x]; - msg.success = !!this.core.SetROM(msg.sim, buffer.pointer, rom.length); - delete msg.rom; - if (msg.reset) - this.core.Reset(msg.sim); - postMessage(msg); - } - - // Execute the current instruction - singleStep(msg) { - this.core.SingleStep(msg.sim); - postMessage(msg); - } - - // Write a single value into the bus - write(msg) { - this.core.Write(msg.sim, msg.address, msg.type, msg.value, - msg.debug ? 1 : 0); - postMessage(msg); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Delete a previously-allocated memory buffer - free(buffer) { - this.core.Free(buffer.pointer); - delete this.buffers[buffer.pointer]; - } - - // Allocate a typed array in WebAssembly core memory - malloc(type, size) { - let pointer = this.core.Malloc(size); - let data = new type(this.core.memory.buffer, pointer, size); - return this.buffers[pointer] = { data: data, pointer: pointer }; - } - - // WebAssembly memory has grown - onmemory() { - for (let buffer of Object.values(this.buffers)) { - buffer.data = new buffer.data.constructor( - this.core.memory.buffer, buffer.pointer); - } - } - -}).initializer(); diff --git a/app/_boot.js b/app/_boot.js index 0453cf8..5e93351 100644 --- a/app/_boot.js +++ b/app/_boot.js @@ -1,10 +1,8 @@ -// Produce an async function from a source string -if (!globalThis.AsyncFunction) - globalThis.AsyncFunction = - Object.getPrototypeOf(async function(){}).constructor; - -// Read scripts from files on disk to aid with debugging -let debug = location.hash == "#debug"; +/* + 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. +*/ @@ -12,55 +10,145 @@ let debug = location.hash == "#debug"; // Bundle // /////////////////////////////////////////////////////////////////////////////// -// Resource asset manager -globalThis.Bundle = class BundledFile { +// 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); - ///////////////////////////// Static Methods ////////////////////////////// + // 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; + } - // Adds a bundled file from loaded file data - static add(name, data) { - return Bundle.files[name] = new Bundle(name, data); - } - - // Retrieve the file given its filename - static get(name) { - return Bundle.files[name]; - } - - // Run a file as a JavaScript source file - static async run(name) { - await Bundle.files[name].run(); - } - - // Resolve a URL for a source file - static source(name) { - return debug ? name : Bundle.files[name].toDataURL(); } - ///////////////////////// Initialization Methods ////////////////////////// + ///////////////////////////// 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 { - // Object constructor constructor(name, data) { // Configure instance fields this.data = data; this.name = name; - // Detect the MIME type + // 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(".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" : + 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" ; @@ -70,255 +158,151 @@ globalThis.Bundle = class BundledFile { ///////////////////////////// Public Methods ////////////////////////////// - // Execute the file as a JavaScript source file - async run() { - - // Not running in debug mode - if (!debug) { - await new AsyncFunction(this.toString())(); - return; + // Install a font from a bundled resource + async installFont(name) { + if (name === undefined) { + name = "/" + this.name; + name = name.substring(name.lastIndexOf("/") + 1); } - - // Running in debug mode - await new Promise((resolve,reject)=>{ - let script = document.createElement("script"); - document.head.appendChild(script); - script.addEventListener("load", ()=>resolve()); - script.src = this.name; - }); - + let ret = new FontFace(name, "url('"+ + (Bundle.debug ? this.name : this.toDataURL()) + "'"); + await ret.load(); + document.fonts.add(ret); + return ret; } - // Register the file as a CSS stylesheet - style(enabled) { - let link = document.createElement("link"); - link.href = debug ? this.name : this.toDataURL(); - link.rel = "stylesheet"; - link.type = "text/css"; - link.setEnabled = enabled=>{ + // 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) - link.removeAttribute("disabled"); - else link.setAttribute("disabled", null); + ret.removeAttribute("disabled"); + else ret.setAttribute("disabled", ""); }; - link.setEnabled(enabled === undefined || !!enabled); - document.head.appendChild(link); - return link; - } - - // Produce a blob from the file data - toBlob() { - return new Blob(this.data, { type: this.mime }); - } - - // Produce a blob URL for the file data - toBlobURL() { - return URL.createObjectURL(this.toBlob()); + ret.setEnabled(!!enabled); + document.head.appendChild(ret); + return ret; } // Encode the file data as a data URL toDataURL() { - return "data:" + this.mime + ";base64," + btoa(this.toString()); + 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 new TextDecoder().decode(this.data); + return Bundle.decoder.decode(this.data); } -}; -Bundle.files = []; + + + ///////////////////////////// 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); + } + +} /////////////////////////////////////////////////////////////////////////////// -// ZIP Bundler // +// Boot Program // /////////////////////////////////////////////////////////////////////////////// -// Data buffer utility processor -class Bin { - - // Object constructor - constructor() { - this.data = []; - this.offset = 0; - } - - ///////////////////////////// Public Methods ////////////////////////////// - - // Convert the data contents to a byte array - toByteArray() { - return Uint8Array.from(this.data); - } - - // Encode a byte array - writeBytes(data) { - this.data = this.data.concat(Array.from(data)); - } - - // Encode a sized integer - writeInt(length, value) { - for (value &= 0xFFFFFFFF; length > 0; length--, value >>>= 8) - this.data.push(value & 0xFF); - } - - // Encode a string as UTF-8 with prepended length - writeString(value) { - this.writeBytes(new TextEncoder().encode(value)); - } - -} - -// Generate the CRC32 lookup table -let 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))); - crcLookup[x] = l; -} - -// Calculate the CRC32 checksum for a byte array -function crc32(data) { - let c = 0xFFFFFFFF; - for (let x = 0; x < data.length; x++) - c = ((c >>> 8) ^ crcLookup[(c ^ data[x]) & 0xFF]); - return ~c & 0xFFFFFFFF; -} - -// Produce a .ZIP header from a bundled file -function toZipHeader(file, crc32, offset) { - let central = offset || offset === 0; - let ret = new Bin(); - if (central) { - ret.writeInt (4, 0x02014B50); // Signature - ret.writeInt (2, 20); // Version created by - } else - ret.writeInt (4, 0x04034B50); // Signature - ret.writeInt (2, 20); // Version required - ret.writeInt (2, 0); // Bit flags - ret.writeInt (2, 0); // Compression method - ret.writeInt (2, 0); // Modified time - ret.writeInt (2, 0); // Modified date - ret.writeInt (4, crc32); // Checksum - ret.writeInt (4, file.data.length); // Compressed size - ret.writeInt (4, file.data.length); // Uncompressed size - ret.writeInt (2, file.name.length); // Filename length - ret.writeInt (2, 0); // Extra field length - if (central) { - ret.writeInt (2, 0); // File comment length - ret.writeInt (2, 0); // Disk number start - ret.writeInt (2, 0); // Internal attributes - ret.writeInt (4, 0); // External attributes - ret.writeInt (4, offset); // Relative offset - } - ret.writeString (file.name, true); // Filename - if (!central) - ret.writeBytes(file.data); // File data - return ret.toByteArray(); -} - -// Package all bundled files into a .zip file for download -Bundle.save = function() { - let centrals = new Array(manifest.length); - let locals = new Array(manifest.length); - let offset = 0; - let size = 0; - - // Encode file and directory entries - let keys = Object.keys(Bundle.files); - for (let x = 0; x < keys.length; x++) { - let file = Bundle.get(keys[x]); - let sum = crc32(file.data); - locals [x] = toZipHeader(file, sum); - centrals[x] = toZipHeader(file, sum, offset); - offset += locals [x].length; - size += centrals[x].length; - } - - // Encode end of central directory - let end = new Bin(); - end.writeInt(4, 0x06054B50); // Signature - end.writeInt(2, 0); // Disk number - end.writeInt(2, 0); // Central dir start disk - end.writeInt(2, centrals.length); // # central dir this disk - end.writeInt(2, centrals.length); // # central dir total - end.writeInt(4, size); // Size of central dir - end.writeInt(4, offset); // Offset of central dir - end.writeInt(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([end.toByteArray()]), - { type: "application/zip" } - )); - a.click(); -} - - - - -/////////////////////////////////////////////////////////////////////////////// -// Boot Loader // -/////////////////////////////////////////////////////////////////////////////// - -/* - The Bundle.java utility prepends a manifest object to _boot.js that is an - array of file infos, each of which has a name and size property. -*/ +// De-register the boot function +delete globalThis.a; // Remove the bundle image element from the document +Bundle.src = arguments[0].src; arguments[0].remove(); -// Process all files from the bundle blob -let blob = arguments[1]; -let offset = arguments[2] - manifest[0].size * 4; -for (let file of manifest) { - let data = new Uint8Array(file.size); - for (let x = 0; x < file.size; x++, offset += 4) - data[x] = blob[offset]; - if (file == manifest[0]) - offset += 4; - Bundle.add(file.name, data); +// 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 } -// Program startup -let run = async function() { - - // Fonts - for (let file of Object.values(Bundle.files)) { - if (!file.name.endsWith(".woff2")) - continue; - let family = "/" + file.name; - family = family.substring(family.lastIndexOf("/")+1, family.length-6); - let font = new FontFace(family, file.data); - await font.load(); - document.fonts.add(font); - } - - // Scripts - await Bundle.run("app/App.js"); - await Bundle.run("app/Debugger.js"); - await Bundle.run("app/toolkit/Toolkit.js"); - await Bundle.run("app/toolkit/Component.js"); - await Bundle.run("app/toolkit/Panel.js"); - await Bundle.run("app/toolkit/Application.js"); - await Bundle.run("app/toolkit/Button.js"); - await Bundle.run("app/toolkit/ButtonGroup.js"); - await Bundle.run("app/toolkit/CheckBox.js"); - await Bundle.run("app/toolkit/Label.js"); - await Bundle.run("app/toolkit/MenuBar.js"); - await Bundle.run("app/toolkit/MenuItem.js"); - await Bundle.run("app/toolkit/Menu.js"); - await Bundle.run("app/toolkit/RadioButton.js"); - await Bundle.run("app/toolkit/Splitter.js"); - await Bundle.run("app/toolkit/TextBox.js"); - await Bundle.run("app/toolkit/Window.js"); - await Bundle.run("app/windows/CPUWindow.js"); - await Bundle.run("app/windows/Disassembler.js"); - await Bundle.run("app/windows/Register.js"); - await Bundle.run("app/windows/MemoryWindow.js"); - await App.create(); -}; -run(); +// 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 new file mode 100644 index 0000000..8d2f8b0 --- /dev/null +++ b/app/app/App.js @@ -0,0 +1,444 @@ +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("Disassembler keyboard commands:"); + console.log(" Ctrl+G: Goto (also works in Memory window)"); + 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 new file mode 100644 index 0000000..f3ec1dd --- /dev/null +++ b/app/app/CPU.js @@ -0,0 +1,106 @@ +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 new file mode 100644 index 0000000..48490f0 --- /dev/null +++ b/app/app/Debugger.js @@ -0,0 +1,187 @@ +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 new file mode 100644 index 0000000..140b9b5 --- /dev/null +++ b/app/app/Disassembler.js @@ -0,0 +1,892 @@ +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.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 = 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) { + visible = visible ? "block" : "none"; + this.lblAddress .style.display = visible; + this.lblBytes .style.display = visible; + this.lblMnemonic.style.display = visible; + this.lblOperands.style.display = visible; + } + + + + ///////////////////////////// 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.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); + + // Processing by key + 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; + + // Goto + case "g": case "G": + if (!e.ctrlKey) + return; + this.promptGoto(); + 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" + }) + ); + } + + // 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; + } + + // 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.viewport.getBoundingClientRect().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 new file mode 100644 index 0000000..be6603c --- /dev/null +++ b/app/app/Memory.js @@ -0,0 +1,467 @@ +import { Util } from /**/"./Util.js"; + + + +// 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 address = Util.u32(this.parent.address + this.index * 16); + let data = this.parent.data; + let dataAddress = this.parent.dataAddress; + let hexCaps = this.parent.dasm.hexCaps; + let offset = Util.s32(address - dataAddress); + + // 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 = "--"; + + // 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) == this.parent.selection) { + lbl.classList.add("selected"); + if (this.parent.digit !== null) + text = this.parent.digit.toString(16); + } + + // The byte is not the current selection + else lbl.classList.remove("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.ScrollPane { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(debug) { + super(debug.app, { + className : "tk tk-scrollpane tk-memory", + horizontal: Toolkit.ScrollPane.AS_NEEDED, + focusable : true, + tabStop : true, + tagName : "div", + vertical : Toolkit.ScrollPane.NEVER + }); + + // Configure instance fields + this.address = 0x05000000; + this.app = debug.app; + this.dasm = debug.disassembler; + this.data = []; + this.dataAddress = this.address; + this.debug = debug; + this.digit = null; + this.isSubscribed = false; + this.lines = []; + this.selection = this.address; + this.sim = debug.sim; + + // Configure view + let view = document.createElement("div"); + view.className = "tk tk-view"; + Object.assign(view.style, { + display : "grid", + gridTemplateColumns: "repeat(17, max-content)" + }); + 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); + + // Configure event handlers + Toolkit.addResizeListener(this.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) { + + // Begin an edit + if (this.digit === null) { + this.digit = digit; + this.setSelection(this.selection, true); + } + + // Complete an edit + else { + this.digit = this.digit << 4 | digit; + this.setSelection(this.selection + 1); + } + + } + + // Key press + onKeyDown(e) { + let key = e.key; + + // A hex digit was entered + if (key.toUpperCase() in DIGITS) { + this.onDigit(DIGITS[key.toUpperCase()]); + key = "digit"; + } + + // Processing by key + switch (key) { + + // Arrow key navigation + case "ArrowDown" : this.setSelection(this.selection + 16); break; + case "ArrowLeft" : this.setSelection(this.selection - 1); break; + case "ArrowRight": this.setSelection(this.selection + 1); break; + case "ArrowUp" : this.setSelection(this.selection - 16); break; + + // Commit current edit + case "Enter": + case " ": + if (this.digit !== null) + this.setSelection(this.selection); + break; + + // Goto + case "g": case "G": + if (!e.ctrlKey) + return; + this.promptGoto(); + break; + + // Page key navigation + case "PageDown": + this.setSelection(this.selection + this.tall(false) * 16); + break; + case "PageUp": + this.setSelection(this.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.address = Util.u32(this.address + offset); + this.fetch(this.address, true); + } + + // Pointer down + onPointerDown(e) { + + // Common handling + this.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(1, Math.ceil(this.metrics.getBounds().height)); + let y = Math.floor((e.y - this.getBounds().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.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.horizontal.setIncrement(this.metrics.getBounds().width); + + // Update the display + if (fetch) + this.fetch(this.address, true); + else this.refresh(); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Update with memory state from the core + refresh(data) { + + // Update with data from the core thread + if (data) { + this.data = data.bytes; + this.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.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.address = 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" + }) + ); + } + + // 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(Util.u32( + (address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16)); + } + + // Move the selection and refresh the display + this.setSelection(Util.u32(address)); + } + + // Specify which byte is selected + setSelection(address, noCommit) { + 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 + this.selection = address = Util.u32(address); + + // Working variables + let row = Util.s32(address - this.address & 0xFFFFFFF0) / 16; + + // The new address is above the top line of output + if (row < 0) { + this.fetch(Util.u32(this.address + row * 16), true); + return; + } + + // The new address is below the bottom line of output + let tall = this.tall(false); + if (row >= tall) { + this.fetch(Util.u32(address - tall * 16 + 16 & 0xFFFFFFF0), true); + return; + } + + // Update the display + if (fetch) + this.fetch(this.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.viewport.getBoundingClientRect().height / lineHeight)); + } + + // Write a value to the core thread + write(value) { + this.data[Util.s32(this.selection - this.dataAddress)] = value; + this.sim.write( + this.selection, + Uint8Array.from([ value ]), { + refresh: true + }); + } + +} + + + +export { Memory }; diff --git a/app/app/RegisterList.js b/app/app/RegisterList.js new file mode 100644 index 0000000..2dfee16 --- /dev/null +++ b/app/app/RegisterList.js @@ -0,0 +1,863 @@ +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.index = index; + this.isExpanded = null; + this.list = list; + this.orMask = orMask; + this.sim = list.sim; + this.system = list.system; + this.type = HEX; + this.value = 0x00000000; + + // Configure main controls + this.contents = new Toolkit.Component(this.app, { + className: "tk tk-contents", + tagName : "div", + style : { + display : "grid", + gridTemplateColumns: "max-content auto max-content" + } + }); + + // 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" + }); + this.list.view.append(this.btnExpand); + + // 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"; + this.list.view.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()); + this.list.view.append(this.txtValue); + + // 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 = new Toolkit.Component(this.app, { + className: "tk tk-expansion", + id : Toolkit.id(), + tagName : "div", + style : { + display : "none", + gridColumnEnd : "span 3", + gridTemplateColumns: + this.system ? "repeat(2, max-content)" : "max-content" + } + }); + + // 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.setType(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.type) { + + // 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); + } + + // 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.element.style.display = + expanded ? "inline-grid" : "none"; + } + + + + ///////////////////////////// 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; + + // Value text box + if (this.type == HEX) { + this.txtValue.element.classList.add("tk-mono"); + this.txtValue.setMaxLength(8); + } else { + this.txtValue.element.classList.remove("tk-mono"); + this.txtValue.setMaxLength(null); + } + switch (this.type) { + + // 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); + } + + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Specify the formatting type of the register value + setType(type) { + if (type == this.type) + return; + this.type = type; + 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.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 : "div", + style : { + display : "grid", + gridTemplateColumns: "max-content auto max-content" + } + })); + + // 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[0] = new Register(this, 0, 0x00000000, 0x00000000); + for (let x = 1; x < 32; x++) + this[x] = new Register(this, x, 0xFFFFFFFF, 0x00000000); + } + + // Initialie a list of system registers + initSystem() { + this[PC ] = new Register(this, PC , 0xFFFFFFFE, 0x00000000); + this[PSW ] = new Register(this, PSW , 0x000FF3FF, 0x00000000); + this[ADTRE] = new Register(this, ADTRE, 0xFFFFFFFE, 0x00000000); + this[CHCW ] = new Register(this, CHCW , 0x00000002, 0x00000000); + this[ECR ] = new Register(this, ECR , 0xFFFFFFFF, 0x00000000); + this[EIPC ] = new Register(this, EIPC , 0xFFFFFFFE, 0x00000000); + this[EIPSW] = new Register(this, EIPSW, 0x000FF3FF, 0x00000000); + this[FEPC ] = new Register(this, FEPC , 0xFFFFFFFE, 0x00000000); + this[FEPSW] = new Register(this, FEPSW, 0x000FF3FF, 0x00000000); + this[PIR ] = new Register(this, PIR , 0x00000000, 0x00005346); + this[TKCW ] = new Register(this, TKCW , 0x00000000, 0x000000E0); + this[29 ] = new Register(this, 29 , 0xFFFFFFFF, 0x00000000); + this[30 ] = new Register(this, 30 , 0x00000000, 0x00000004); + this[31 ] = 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 monospaced text boxes + for (let box of this.element + .querySelectorAll(".tk-textbox[digits]")) { + Object.assign(box.style, { + height: height + "px", + width : (parseInt(box.getAttribute("digits")) * width) + "px" + }); + } + + // 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) { + this[ADTRE].refresh(registers.adtre); + this[CHCW ].refresh(registers.chcw ); + this[ECR ].refresh(registers.ecr ); + this[EIPC ].refresh(registers.eipc ); + this[EIPSW].refresh(registers.eipsw); + this[FEPC ].refresh(registers.fepc ); + this[FEPSW].refresh(registers.fepsw); + this[PC ].refresh(registers.pc ); + this[PIR ].refresh(registers.pir ); + this[PSW ].refresh(registers.psw ); + this[TKCW ].refresh(registers.tkcw ); + this[29 ].refresh(registers[29] ); + this[30 ].refresh(registers[30] ); + this[31 ].refresh(registers[31] ); + } + + // 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() { + if (this.system) { + for (let key of Object.keys(SYSREGS)) + this[key].dasmChanged(); + } else { + for (let key = 0; key < 32; key++) + this[key].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.getBounds().bottom - + this.view.getBounds().top; + } + + return ret; + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // 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 new file mode 100644 index 0000000..243ef03 --- /dev/null +++ b/app/app/Util.js @@ -0,0 +1,44 @@ +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 new file mode 100644 index 0000000..b56a341 --- /dev/null +++ b/app/core/Core.js @@ -0,0 +1,196 @@ +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 new file mode 100644 index 0000000..b8989e4 --- /dev/null +++ b/app/core/CoreWorker.js @@ -0,0 +1,378 @@ +"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 + //size : Math.min(u32(pc - address), 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 new file mode 100644 index 0000000..f7d6928 --- /dev/null +++ b/app/core/Sim.js @@ -0,0 +1,151 @@ +/////////////////////////////////////////////////////////////////////////////// +// 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.js b/app/locale/en-US.js deleted file mode 100644 index 7cac528..0000000 --- a/app/locale/en-US.js +++ /dev/null @@ -1,44 +0,0 @@ -{ - key : "en-US", - name: "English (United States)", - app : { - close : "Close", - console : "Console", - goto_ : "Enter the address to seek to:", - romLoaded : "Successfully loaded file \"{filename}\" ({size})", - romNotVB : "The selected file is not a Virtual Boy ROM.", - readFileError: "Unable to read the selected file." - }, - cpu: { - _ : "CPU", - disassembler: "Disassembler", - float_ : "Float", - hex : "Hex", - pcFrom : "From", - pcTo : "To", - mainSplit : "Disassembler and registers splitter", - regsSplit : "System registers and program registers splitter", - signed : "Signed", - unsigned : "Unsigned" - }, - memory: { - _ : "Memory", - hexEditor: "Hex viewer" - }, - menu: { - _ : "Main application menu", - debug: { - _: "Debug", - }, - file : { - _ : "File", - loadROM: "Load ROM..." - }, - theme: { - _ : "Theme", - dark : "Dark", - light : "Light", - virtual: "Virtual" - } - } -} diff --git a/app/locale/en-US.json b/app/locale/en-US.json new file mode 100644 index 0000000..ab06e98 --- /dev/null +++ b/app/locale/en-US.json @@ -0,0 +1,74 @@ +{ + "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 new file mode 100644 index 0000000..55ea892 --- /dev/null +++ b/app/main.js @@ -0,0 +1,34 @@ +// 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 index 285484d..93fd934 100644 --- a/app/template.html +++ b/app/template.html @@ -3,8 +3,10 @@ Virtual Boy Emulator + + - + diff --git a/app/theme/check.svg b/app/theme/check.svg new file mode 100644 index 0000000..ac12455 --- /dev/null +++ b/app/theme/check.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/theme/close.svg b/app/theme/close.svg new file mode 100644 index 0000000..d70dcf1 --- /dev/null +++ b/app/theme/close.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/theme/collapse.svg b/app/theme/collapse.svg new file mode 100644 index 0000000..7d7ab60 --- /dev/null +++ b/app/theme/collapse.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/theme/dark.css b/app/theme/dark.css index cc6ed2f..fd61751 100644 --- a/app/theme/dark.css +++ b/app/theme/dark.css @@ -1,27 +1,26 @@ :root { - --close : #ee9999; - --close-blur : #998080; - --close-blur-border: #999999; - --close-blur-text : #ffffff; - --close-border : #999999; - --close-text : #ffffff; - --control : #333333; - --control-disabled : #999999; - --control-focus : #555555; - --control-shadow : #999999; - --control-text : #cccccc; - --desktop : #111111; - --selected : #008542; - --selectedBlur : #57665d; - --selectedText : #ffffff; - --selectedTextBlur : #ffffff; - --splitter-focus : #0099ff99; - --title : #007ACC; - --title-blur : #555555; - --title-blur-text : #cccccc; - --title-text : #ffffff; - --window : #222222; - --window-border : #cccccc; - --window-disabled : #888888; - --window-text : #cccccc; -} \ No newline at end of file + --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/expand.svg b/app/theme/expand.svg new file mode 100644 index 0000000..f8cdfe5 --- /dev/null +++ b/app/theme/expand.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/theme/Inconsolata_SemiExpanded-Medium.woff2 b/app/theme/inconsolata.woff2 similarity index 100% rename from app/theme/Inconsolata_SemiExpanded-Medium.woff2 rename to app/theme/inconsolata.woff2 diff --git a/app/theme/kiosk.css b/app/theme/kiosk.css index 1b49e3c..1560cad 100644 --- a/app/theme/kiosk.css +++ b/app/theme/kiosk.css @@ -1,541 +1,736 @@ -/* Common styles for all themes */ - :root { - --font-dialog: "Roboto-Regular", sans-serif; - --font-hex : "Inconsolata_SemiExpanded-Medium", monospace; - --font-size : 12px; + --tk-font-dialog: "Roboto", sans-serif; + --tk-font-mono : "Inconsolata SemiExpanded Medium", monospace; + --tk-font-size : 12px; } -:root { - color : var(--control-text); - font-family: var(--font-dialog); - font-size : var(--font-size); +.tk { + font-family: var(--tk-font-dialog); + font-size : var(--tk-font-size); + line-height: 1em; + margin : 0; + outline : none; + padding : 0; } -body { - background: var(--window); - overflow : hidden; +.tk-body { + overflow: hidden; } -* { - background: transparent; - border : none; - margin : 0; - padding : 0; +.tk-app { + /* Height managed through resize listener */ + width: 100%; } -*:focus { - outline: none; -} - -[desktop] { - background: var(--desktop); +.tk-mono { + font-family: var(--tk-font-mono); } /********************************** Button ***********************************/ -[role="button"] { - box-shadow: - 0 0 0 1px var(--control-shadow), - 1px 1px 0 1px var(--control-shadow) - ; - margin : 1px; +.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); +} + + + +/********************************* 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; } -[role="button"]:focus { - background: var(--control-focus); +.tk-menu-item > * > .tk-icon { + box-sizing: border-box; + display : none; + height : 1em; + width : 1em; } -[role="button"][active] { - box-shadow: 0 0 0 1px var(--control-shadow); - margin : 2px 0 0 2px; +.tk-menu-item > * > .tk-text { + flex-grow: 1; } - -/********************************* Checkbox **********************************/ - -[role="checkbox"] { - column-gap: 2px; +.tk-menu-item[aria-disabled="true"] > * > .tk-text { + color: var(--tk-control-shadow); } -[role="checkbox"] [name="check"] { - background : var(--window); - border : 1px solid var(--control-shadow); - color : var(--window-text); - font-size : calc(var(--font-size) - 1px); - line-height: calc(var(--font-size) - 2px); - height : calc(var(--font-size) - 2px); - overflow : hidden; - position : relative; - width : calc(var(--font-size) - 2px); +.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); } -[role="checkbox"] [name="check"]:after { - bottom : 0; - left : 0; - line-height: 100%; - position : absolute; - right : 0; - text-align : center; - top : 0; +.tk-menu.icons > .tk-menu-item > * > .tk-icon { + display: block; } -[role="checkbox"][active] [name="check"]:after, -[role="checkbox"][aria-checked="true"] [name="check"]:after { - content: "\2713"; +.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon { + border: 1px solid var(--tk-control-border); } -[role="checkbox"][active] [name="check"]:after { - color: var(--window); +.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; } -[role="checkbox"]:focus [name="check"] { - background: var(--control-focus); +.tk-menu-item[role="menuitemcheckbox"][aria-checked="true"] + > * > .tk-icon:before { + background: var(--tk-control-text); } -[role="checkbox"][aria-disabled="true"] [name="check"] { - border-color: var(--window-disabled); - color : var(--window-disabled); +.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"] + > * > .tk-icon { + border: 1px solid var(--tk-control-shadow); } -[role="checkbox"] [name="label"] { - color: var(--control-text); +.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"] + > * > .tk-icon:before { + background: var(--tk-control-shadow); } -[role="checkbox"][aria-disabled="true"] [name="label"] { - color: var(--control-disabled); +.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; } -/********************************* Checkbox **********************************/ +/*********************************** Radio ***********************************/ -[role="radio"] { - column-gap: 2px; -} - -[role="radio"] [name="check"] { - background : var(--window); - border : 1px solid var(--control-shadow); - border-radius: 50%; - color : var(--window-text); - height : calc(var(--font-size) - 2px); - overflow : hidden; - position : relative; - width : calc(var(--font-size) - 2px); -} - -[role="radio"] [name="check"]:after { - background : currentColor; - border-radius: 50%; - bottom : 0; - left : 0; - position : absolute; - margin : 30%; - right : 0; - top : 0; -} - -[role="radio"][active] [name="check"]:after, -[role="radio"][aria-checked="true"] [name="check"]:after { - content: ""; -} - -[role="radio"][active] [name="check"]:after { - color: var(--window); -} - -[role="radio"]:focus [name="check"] { - background: var(--control-focus); -} - -[role="radio"][aria-disabled="true"] [name="check"] { - border-color: var(--window-disabled); - color : var(--window-disabled); -} - -[role="radio"] [name="label"] { - color: var(--control-text); -} - -[role="radio"][aria-disabled="true"] [name="label"] { - color: var(--control-disabled); -} - - - -/********************************** ListBox **********************************/ - -select { - background : var(--window); - border : 1px solid var(--control-shadow); - color : var(--window-text); - font-family: var(--font-dialog); - font-size : var(--font-size); - line-height: var(--font-size); - padding : 1px; -} - - - -/********************************** MenuBar **********************************/ - -[role="menubar"] { - background : var(--control); - border-bottom: 1px solid var(--control-text); - padding : 2px 3px 3px 2px; -} - -[role="menubar"] [role="menuitem"] { - line-height: 1em; - margin : 1px; - padding : 3px; -} - -[role="menubar"] [role="menuitem"]:focus { - background: var(--control-focus); -} - -[role="menubar"] [role="menuitem"]:not([active],[aria-expanded="true"]):focus, -[role="menubar"] [role="menuitem"]:not([active],[aria-expanded="true"]):hover { - box-shadow: - 0 0 0 1px var(--control-shadow), - 1px 1px 0 1px var(--control-shadow) - ; -} - -[role="menubar"] [role="menuitem"][active], -[role="menubar"] [role="menuitem"][aria-expanded="true"] { - box-shadow: 0 0 0 1px var(--control-shadow); - margin : 2px 0 0 2px; -} - -[role="menubar"] [role="menu"] { - background: var(--control); - box-shadow: - 0 0 0 1px var(--control-text), - 1px 1px 0 1px var(--control-text) - ; - min-height: 16px; - min-width : 16px; - padding : 2px 3px 3px 2px; -} - - - -/********************************* Splitter **********************************/ - -[role="separator"][tabindex] { - z-index: 1; -} - -[role="separator"][tabindex]:focus { - background: var(--splitter-focus); -} - - - -/********************************** TextBox **********************************/ - -input[type="text"] { - background : var(--window); - border : 1px solid var(--control-shadow); - color : var(--window-text); - font-family: var(--font-dialog); - font-size : var(--font-size); - line-height: var(--font-size); - padding : 1px; -} - - - - -/********************************** Window ***********************************/ - -[role="dialog"] { - padding: 3px; -} - -[role="dialog"] [name="body"] { - box-shadow: - 0 0 0 2px var(--window-border), - 1px 1px 0 2px var(--window-border) - ; -} - -[role="dialog"] [name="title-bar"] { +.tk-radio { align-items: center; - background : var(--title); - column-gap : 1px; - padding : 0 0 1px 0; - margin : 0 0 2px 0; - box-shadow : - -0.5px -0.5px 0 0.5px var(--title ), - 0.5px -0.5px 0 0.5px var(--title ), - 0 0 0 1px var(--control-shadow) - ; + column-gap : 2px; } -[role="dialog"][focus="false"] [name="title-bar"] { - background: var(--title-blur); - box-shadow: - -0.5px -0.5px 0 0.5px var(--title-blur ), - 0.5px -0.5px 0 0.5px var(--title-blur ), - 0 0 0 1px var(--control-shadow) - ; +.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; } -[role="dialog"] [name="title-bar"] [name="icon"] { - height: 15px; - width : 15px; +.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; } -[role="dialog"] [name="title-bar"] [name="title"] { - color : var(--title-text); - font-size : var(--font-size); - font-weight: bold; +.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); +} + + + +/******************************* Disassembler ********************************/ + +.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; +} + +.tk-reglist .tk-expansion { + align-items : center; + margin-bottom: 2px; + padding : 2px 0 0 1.5em; +} + +.tk-reglist .tk-expansion { + column-gap: 0.8em; +} + +.tk-reglist .tk-expansion .tk-number .tk-label { + align-items : center; + display : flex; + justify-content: center; + min-width : 12px; +} + +.tk-reglist .tk-expansion .tk-textbox { + width: 1.5em; +} + +.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 { + 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; + margin-left: calc(0.6em - 1px); + padding : 0 1px; text-align : center; } -[role="dialog"][focus="false"] [name="title-bar"] [name="title"] { - color: var(--title-blur-text); +.tk-memory .tk-byte.tk-0, +.tk-memory .tk-byte.tk-8 { + margin-left: calc(1.2em - 1px); } -[role="dialog"] [name="title-bar"] [name="close"] { - background : var(--close); - border : 1px solid var(--close-border); - box-sizing : border-box; - box-shadow : none; - color : var(--close-text); - font-size : 13px; - font-weight: bold; - height : 15px; - line-height: 13px; - margin : 0; - padding : 0; - position : relative; - text-align : center; - width : 15px; +.tk-memory .tk-byte.selected { + background: var(--tk-selected-blur); + color : var(--tk-selected-blur-text); } -[role="dialog"][focus="false"] [name="title-bar"] [name="close"] { - background : var(--close-blur); - border-color: var(--close-blur-border); - color : var(--close-blur-text); -} - -[role="dialog"] [name="title-bar"] [name="close"]:after { - content : "\00d7"; - position: absolute; - inset : 0 0 0 0; -} - -[role="dialog"] [name="title-bar"] [name="close"][active]:after { - inset: 1px -1px -1px 1px; -} - -[role="dialog"] [name="client"] { - background: var(--control); - box-shadow: 0 0 0 1px var(--control); -} - - - -/******************************* Memory Window *******************************/ - -[role="dialog"][window="memory"] [name="wrap-hex"] { - background : var(--window); - box-shadow : 0 0 0 1px var(--control-shadow); - font-family: var(--font-hex); - margin : 1px; - padding : 1px; -} - -[role="dialog"][window="memory"] [name="hex"] [role="row"] { - column-gap: calc(var(--font-size) / 2); -} - -[role="dialog"][window="memory"] [name="hex"] [role="row"] > *:nth-child( 2), -[role="dialog"][window="memory"] [name="hex"] [role="row"] > *:nth-child(10) { - margin-left: calc(var(--font-size) / 2); -} - -[role="dialog"][window="memory"] [name="hex"] [name="byte"][selected] { - background: var(--selected); - box-shadow: - -0.5px 0.5px 0 0.5px var(--selected), - 0.5px 0.5px 0 0.5px var(--selected) - ; - color : var(--selectedText); -} - -[role="dialog"][window="memory"][focus="false"] -[name="hex"] [name="byte"][selected] { - background: var(--selectedBlur); - box-shadow: - -0.5px 0.5px 0 0.5px var(--selectedBlur), - 0.5px 0.5px 0 0.5px var(--selectedBlur) - ; - color : var(--selectedTextBlur); -} - - - - -/******************************** CPU Window *********************************/ - -[role="dialog"][window="cpu"] [name="wrap-disassembler"], -[role="dialog"][window="cpu"] [name="wrap-system-registers"], -[role="dialog"][window="cpu"] [name="wrap-program-registers"] { - background: var(--window); - color : var(--window-text); - box-shadow: 0 0 0 1px var(--control-shadow); -} - -[role="dialog"][window="cpu"] [name="wrap-main"] { - box-shadow : 0 0 0 1px var(--control-shadow); - margin-left: 1px; -} - -[role="dialog"][window="cpu"] [name="wrap-disassembler"] { - margin: 1px 0; -} - -[role="dialog"][window="cpu"] [name="split-main"] { - margin-right: -1px; -} - -[role="dialog"][window="cpu"] [name="wrap-registers"] { - box-shadow: - 0 -1px 0 0 var(--control-shadow), - 0 1px 0 0 var(--control-shadow) - ; - margin: 1px 0; - width : 140px; -} - -[role="dialog"][window="cpu"] [name="wrap-system-registers"] { - height: 143px; - margin: 0 1px; -} - -[role="dialog"][window="cpu"] [name="wrap-program-registers"] { - margin: 0 1px; -} - -[role="dialog"][window="cpu"] [name="disassembler"], -[role="dialog"][window="cpu"] [name="system-registers"], -[role="dialog"][window="cpu"] [name="program-registers"] { - padding: 1px; -} - -[role="dialog"][window="cpu"] [name="expand"] { - column-gap: 0; -} - -[role="dialog"][window="cpu"] [name="expand"] [name="check"] { - border: none; -} - -[role="dialog"][window="cpu"] [name="expand"][aria-checked="false"] - [name="check"]:after { - content: "+"; -} - -[role="dialog"][window="cpu"] [name="expand"][aria-checked="true"] - [name="check"]:after { - content: "-"; -} - -[role="dialog"][window="cpu"] [name="wrap-registers"] input[type="text"] { - border : none; - font-family : var(--font-hex); - height : var(--font-size); - margin-right: 1px; - padding : 0; - text-align : right; - width : 58px; -} - -[role="dialog"][window="cpu"] [name="wrap-registers"] - [format="float"] [name="value"], -[role="dialog"][window="cpu"] [name="wrap-registers"] - [format="signed"] [name="value"], -[role="dialog"][window="cpu"] [name="wrap-registers"] [format="unsigned"] - [name="value"] { - font-family: var(--dialog-font); - width : 80px; -} - -[role="dialog"][window="cpu"] [name="wrap-registers"] [name="expansion"] { - margin-left: calc(var(--font-size) * 1.5); -} - -[role="dialog"][window="cpu"] [register="psw" ], -[role="dialog"][window="cpu"] [register="tkcw"] { - column-gap: var(--font-size); -} - -[role="dialog"][window="cpu"] [register="ecr" ], -[role="dialog"][window="cpu"] [register="pir" ], -[role="dialog"][window="cpu"] [register="psw" ] [name="I" ], -[role="dialog"][window="cpu"] [register="tkcw"] [name="RD"] { - column-gap: 4px; -} - -[role="dialog"][window="cpu"] [register="psw" ] [name="I" ] input[type="text"], -[role="dialog"][window="cpu"] [register="tkcw"] [name="RD"] input[type="text"]{ - font-family: var(--font-dialog); - text-align : left; - width : 20px; -} - -[role="dialog"][window="cpu"] [register="ecr"] input[type="text"], -[role="dialog"][window="cpu"] [register="pir"] input[type="text"] { - width: 32px; -} - -[role="dialog"][window="cpu"] [name="expand"][aria-disabled="true"] - [name="check"]:after { - content: ""; -} - -[role="dialog"][window="cpu"] [aria-disabled="true"] * { - color: var(--window-text); -} - -[role="dialog"][window="cpu"] [aria-disabled="true"] [name="check"] { - border-color: var(--control-shadow); - color : var(--window-text); -} - -[role="dialog"][window="cpu"] [name="expansion"] - input[type="text"][aria-disabled="true"], -[role="dialog"][window="cpu"] [name="expansion"] - [aria-disabled="true"] [name="check"]{ - cursor: not-allowed !important; -} - -[role="dialog"][window="cpu"] [name="disassembler"] { - font-family: var(--font-hex); -} - -[role="dialog"][window="cpu"] [name="disassembler"] [name="row"] { - column-gap: calc(var(--font-size) * 1.5); -} - -[role="dialog"][window="cpu"] [name="disassembler"] [name="row"][pc] { - background: var(--selected); - box-shadow: 0 1px 0 var(--selected); - color : var(--selectedText); -} - -[role="dialog"][window="cpu"][focus="false"] -[name="disassembler"] [name="row"][pc] { - background: var(--selectedBlur); - box-shadow: 0 1px 0 var(--selectedBlur); - color : var(--selectedTextBlur); +.tk-memory:focus-within .tk-byte.selected { + background: var(--tk-selected); + color : var(--tk-selected-text); } diff --git a/app/theme/light.css b/app/theme/light.css index 2fe9588..3515cb2 100644 --- a/app/theme/light.css +++ b/app/theme/light.css @@ -1,27 +1,26 @@ :root { - --close : #ee9999; - --close-blur : #d4c4c4; - --close-blur-border: #999999; - --close-blur-text : #ffffff; - --close-border : #999999; - --close-text : #ffffff; - --control : #eeeeee; - --control-disabled : #888888; - --control-focus : #cccccc; - --control-shadow : #999999; - --control-text : #000000; - --desktop : #cccccc; - --selected : #008542; - --selectedBlur : #57665d; - --selectedText : #ffffff; - --selectedTextBlur : #ffffff; - --splitter-focus : #0099ff99; - --title : #80ccff; - --title-blur : #cccccc; - --title-blur-text : #444444; - --title-text : #000000; - --window : #ffffff; - --window-border : #000000; - --window-disabled : #aaaaaa; - --window-text : #000000; + --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/radio.svg b/app/theme/radio.svg new file mode 100644 index 0000000..3d6392a --- /dev/null +++ b/app/theme/radio.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/theme/Roboto-Regular.woff2 b/app/theme/roboto.woff2 similarity index 100% rename from app/theme/Roboto-Regular.woff2 rename to app/theme/roboto.woff2 diff --git a/app/theme/scroll.svg b/app/theme/scroll.svg new file mode 100644 index 0000000..dc7e5b5 --- /dev/null +++ b/app/theme/scroll.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/theme/virtual.css b/app/theme/virtual.css index f948854..98f675f 100644 --- a/app/theme/virtual.css +++ b/app/theme/virtual.css @@ -1,31 +1,70 @@ :root { - --close : #aa0000; - --close-blur : #550000; - --close-blur-border: #aa0000; - --close-blur-text : #aa0000; - --close-border : #ff0000; - --close-text : #ff0000; - --control : #000000; - --control-disabled : #aa0000; - --control-focus : #550000; - --control-shadow : #aa0000; - --control-text : #ff0000; - --desktop : #000000; - --selected : #ff0000; - --selectedBlur : #aa0000; - --selectedText : #550000; - --selectedTextBlur : #000000; - --splitter-focus : #ff000099; - --title : #550000; - --title-blur : #000000; - --title-blur-text : #aa0000; - --title-text : #ff0000; - --window : #000000; - --window-border : #ff0000; - --window-disabled : #aa0000; - --window-text : #ff0000; + --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; } -[filter] { +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/Application.js b/app/toolkit/Application.js deleted file mode 100644 index 0d219c9..0000000 --- a/app/toolkit/Application.js +++ /dev/null @@ -1,208 +0,0 @@ -"use strict"; - -// Root element and localization manager for a Toolkit application -Toolkit.Application = class Application extends Toolkit.Panel { - - // Object constructor - constructor(options) { - super(null, options); - - // Configure instance fields - this.application = this; - this.components = []; - this.locale = null; - this.locales = { first: null }; - this.propagationListeners = []; - - // Configure element - this.element.setAttribute("application", ""); - this.element.addEventListener("mousedown" , e=>this.onpropagation(e)); - this.element.addEventListener("pointerdown", e=>this.onpropagation(e)); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a component for localization management - addComponent(component) { - if (this.components.indexOf(component) != -1) - return; - this.components.push(component); - component.localize(); - } - - // Register a locale with the application - addLocale(source) { - let loc = null; - - // Process the locale object from the source - try { loc = new Function("return (" + source + ");")(); } - catch(e) { console.log(e); } - - // Error checking - if ( - !loc || typeof loc != "object" || - !("key" in loc) || !("name" in loc) - ) return null; - - // Register the locale - if (this.locales.first == null) - this.locales.first = loc; - this.locales[loc.key] = loc; - return loc.key; - } - - // Add a callback for propagation events - addPropagationListener(listener) { - if (this.propagationListeners.indexOf(listener) == -1) - this.propagationListeners.push(listener); - } - - // Produce a list of all registered locale keys - listLocales() { - return Object.values(this.locales); - } - - // Remove a compnent from being localized - removeComponent(component) { - let index = this.components.indexOf(component); - if (index == -1) - return false; - this.components.splice(index, 1); - return true; - } - - // Specify which localized strings to use for application controls - setLocale(lang) { - - // Error checking - if (this.locales.first == null) - return null; - - // Working variables - lang = lang.toLowerCase(); - let parts = lang.split("-"); - let best = null; - - // Check all locales - for (let loc of Object.values(this.locales)) { - let key = loc.key.toLowerCase(); - - // The language is an exact match - if (key == lang) { - best = loc; - break; - } - - // The language matches, but the region may not - if (best == null && key.split("-")[0] == parts[0]) - best = loc; - } - - // The language did not match: use the first locale that was registered - if (best == null) - best = this.locales.first; - - // Select the locale - this.locale = best; - return best.key; - } - - // Localize text for a component - translate(text, properties) { - properties = !properties ? {} : - properties instanceof Toolkit.Component ? properties.properties : - properties; - - // Process all characters from the input - let sub = { text: "", parent: null }; - for (let x = 0; x < text.length; x++) { - let c = text[x]; - let last = x == text.length - 1; - - // Left curly brace - if (c == '{') { - - // Literal left curly brace - if (!last && text[x + 1] == '{') { - sub.text += c; - x++; - continue; - } - - // Open a substring - sub = { text: "", parent: sub }; - continue; - } - - // Right curly brace - if (c == '}') { - - // Literal right curly brace - if (!last && text[x + 1] == '}') { - sub.text += c; - x++; - continue; - } - - // Close a sub (if there are any to close) - if (sub.parent != null) { - - // Text comes from component property - if (sub.text in properties) { - sub.parent.text += properties[sub.text]; - sub = sub.parent; - continue; - } - - // Text comes from locale - let value = this.fromLocale(sub.text, true); - if (value !== null) { - text = value + text.substring(x + 1); - x = -1; - sub = sub.parent; - continue; - } - - // Take the text as-is - sub.parent.text += "{" + sub.text + "}"; - sub = sub.parent; - continue; - } - - } - - // Append the character to the sub's text - sub.text += c; - } - - // Close any remaining subs (should never happen) - for (; sub.parent != null; sub = sub.parent) - sub.parent.text += sub.text; - return sub.text; - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Retrieve the text for a key in the locale - fromLocale(key) { - let locale = this.locale || {}; - for (let part of key.split(".")) { - if (!(part in locale)) - return null; - locale = locale[part]; - } - return typeof locale == "string" ? locale : null; - } - - // A pointer or mouse down even has propagated - onpropagation(e) { - e.stopPropagation(); - for (let listener of this.propagationListeners) - listener(e, this); - } - -}; diff --git a/app/toolkit/Button.js b/app/toolkit/Button.js index 4f9ad8e..6f0f6f6 100644 --- a/app/toolkit/Button.js +++ b/app/toolkit/Button.js @@ -1,230 +1,387 @@ -"use strict"; +import { Component } from /**/"./Component.js"; +let Toolkit; -// Push button -Toolkit.Button = class Button extends Toolkit.Component { - // Object constructor - constructor(application, options) { - super(application, "div", options); - options = options || {}; + +/////////////////////////////////////////////////////////////////////////////// +// 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 - this.clickListeners = []; - this.enabled = "enabled" in options ? !!options.enabled : true; - this.focusable = "focusable" in options?!!options.focusable:true; - this.name = options.name || ""; - this.text = options.text || ""; - this.toolTip = options.toolTip || ""; + 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 element - this.element.setAttribute("role", "button"); - this.element.setAttribute("tabindex", "0"); - this.element.style.cursor = "default"; - this.element.style.userSelect = "none"; - this.element.addEventListener("keydown" , e=>this.onkeydown (e)); - this.element.addEventListener("pointerdown", e=>this.onpointerdown(e)); - this.element.addEventListener("pointermove", e=>this.onpointermove(e)); - this.element.addEventListener("pointerup" , e=>this.onpointerup (e)); + // Configure contents + this.contents = document.createElement("div"); + this.append(this.contents); - // Configure properties - this.setEnabled (this.enabled ); - this.setFocusable(this.focusable); - this.setName (this.name ); - this.setText (this.text ); - this.setToolTip (this.toolTip ); - this.application.addComponent(this); + // 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 ////////////////////////////// - // Add a callback for click events - addClickListener(listener) { - if (this.clickListeners.indexOf(listener) == -1) - this.clickListeners.push(listener); + // Programmatically activate the button + click() { + if (this instanceof Toolkit.CheckBox) + this.setSelected(this instanceof Toolkit.Radio||!this.isSelected); + this.event("action"); } - // The button was activated - click(e) { - if (!this.enabled) - return; - for (let listener of this.clickListeners) - listener(e); - } - - // Request focus on the appropriate element - focus() { - this.element.focus(); - } - - // Retrieve the component's accessible name - getName() { - return this.name; - } - - // Retrieve the component's display text - getText() { - return this.text; - } - - // Retrieve the component's tool tip text - getToolTip() { - return this.toolTip; - } - - // Determine whether the component is enabled - isEnabled() { - return this.enabled; - } - - // Determine whether the component is focusable - isFocusable() { - return this.focusable; - } - - // Specify whether the component is enabled + // Specify whether the button can be activated setEnabled(enabled) { - this.enabled = enabled = !!enabled; - this.element.setAttribute("aria-disabled", !enabled); + this.isEnabled = enabled = !!enabled; + this.setAttribute("aria-disabled", enabled ? null : "true"); } - // Specify whether the component can receive focus - setFocusable(focusable) { - this.focusable = focusable = !!focusable; - if (focusable) - this.element.setAttribute("tabindex", "0"); - else this.element.removeAttribute("tabindex"); + // 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 component's accessible name - setName(name) { - this.name = name || ""; - this.localize(); - } - - // Specify the component's display text + // Specify the widget's display text setText(text) { - this.text = text || ""; - this.localize(); + this.text = (text || "").toString().trim(); + this.translate(); } - // Specify the component's tool tip text - setToolTip(toolTip) { - this.toolTip = toolTip || ""; - this.localize(); + // 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 display text with localized strings - localize() { - let name = this.name || this.text; - let text = this.text; - let toolTip = this.toolTip; - if (this.application) { - name = this.application.translate(name, this); - text = this.application.translate(text, this); - if (toolTip) - toolTip = this.application.translate(toolTip, this); - } - this.element.setAttribute("aria-label", name); - this.element.innerText = text; - if (toolTip) - this.element.setAttribute("title", toolTip); - else this.element.removeAttribute("title"); + // 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 = []; } - ///////////////////////////// Private Methods ///////////////////////////// + ///////////////////////////// Public Methods ////////////////////////////// - // Key down event handler - onkeydown(e) { + // Add an item + add(item) { // Error checking - if (!this.enabled) - return; + if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1) + return item; - // Processing by key - switch (e.key) { - case " ": - case "Enter": - this.click(e); - break; - default: return; - } + // Configure component + this.setAttribute("role", + item instanceof Toolkit.Radio ? "radiogroup" : "group"); - // Configure event - e.preventDefault(); - e.stopPropagation(); + // 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; } - // Pointer down event handler - onpointerdown(e) { + // Remove all items + clear() { + this.items.splice(); + this.setAttribute("aria-owns", ""); + } - // Configure event - e.stopPropagation(); + // Un-check all items in the group + deselect() { + for (let item of this.items) + if (item.isSelected && "setSelected" in item) + item.setSelected(false); + } - // Configure focus - if (this.enabled) - this.focus(); - else return; + // Remove an item + remove(item) { // Error checking - if (e.button != 0 || this.element.hasPointerCapture(e.captureId)) + let index = this.items.indexOf(item); + if (index == -1) return; - // Configure element - this.element.setPointerCapture(e.pointerId); - this.element.setAttribute("active", ""); + // 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; } - // Pointer move event handler - onpointermove(e) { +} - // Configure event - e.preventDefault(); - e.stopPropagation(); - // Error checking - if (!this.element.hasPointerCapture(e)) - return; - // Working variables - let bounds = this.getBounds(); - let active = - e.x >= bounds.x && e.x < bounds.x + bounds.width && - e.y >= bounds.y && e.y < bounds.y + bounds.height - ; - - // Configure element - if (active) - this.element.setAttribute("active", ""); - else this.element.removeAttribute("active"); - } - - // Pointer up event handler - onpointerup(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Error checking - if (!this.element.hasPointerCapture(e.pointerId)) - return; - - // Configure element - this.element.releasePointerCapture(e.pointerId); - - // Activate the component if it is active - if (!this.element.hasAttribute("active")) - return; - this.element.removeAttribute("active"); - this.click(e); - } - -}; +export { Button, CheckBox, Group, Radio }; diff --git a/app/toolkit/ButtonGroup.js b/app/toolkit/ButtonGroup.js deleted file mode 100644 index 6fe3693..0000000 --- a/app/toolkit/ButtonGroup.js +++ /dev/null @@ -1,44 +0,0 @@ -"use strict"; - -// Grouping manager for mutually-exclusive controls -Toolkit.ButtonGroup = class ButtonGroup { - - // Object constructor - constructor() { - this.components = []; - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a component to the group - add(component) { - if (this.components.indexOf(component) != -1) - return component; - this.components.push(component); - if ("setGroup" in component) - component.setGroup(this); - return component; - } - - // Select only one button in the group - setChecked(component) { - for (let comp of this.components) { - if ("setChecked" in comp) - comp.setChecked(comp == component, this); - } - } - - // Remove a component from the group - remove(component) { - let index = this.components.indexOf(component); - if (index == -1) - return false; - this.components.splice(index, 1); - if ("setGroup" in component) - component.setGroup(null); - return true; - } - -}; diff --git a/app/toolkit/CheckBox.js b/app/toolkit/CheckBox.js deleted file mode 100644 index ed70f27..0000000 --- a/app/toolkit/CheckBox.js +++ /dev/null @@ -1,190 +0,0 @@ -"use strict"; - -// On/off toggle checkbox -Toolkit.CheckBox = class CheckBox extends Toolkit.Panel { - - // Object constructor - constructor(application, options) { - super(application, options); - options = options || {}; - - // Configure instance fields - this.changeListeners = []; - this.checked = false; - this.enabled = "enabled" in options ? !!options.enabled : true; - this.text = options.text || ""; - - // Configure element - this.setLayout("grid", { - columns : "max-content max-content" - }); - this.setDisplay("inline-grid"); - this.setHollow(false); - this.setOverflow("visible", "visible"); - this.element.setAttribute("tabindex", "0"); - this.element.setAttribute("role", "checkbox"); - this.element.setAttribute("aria-checked", "false"); - this.element.style.alignItems = "center"; - this.element.addEventListener("keydown" , e=>this.onkeydown (e)); - this.element.addEventListener("pointerdown", e=>this.onpointerdown(e)); - this.element.addEventListener("pointermove", e=>this.onpointermove(e)); - this.element.addEventListener("pointerup" , e=>this.onpointerup (e)); - - // Configure check box - this.check = this.add(this.newLabel()); - this.check.element.setAttribute("name", "check"); - this.check.element.setAttribute("aria-hidden", "true"); - - // Configure label - this.label = this.add(this.newLabel({ localized: true })); - this.label.element.setAttribute("name", "label"); - this.element.setAttribute("aria-labelledby", this.label.id); - - // Configure properties - this.setChecked(options.checked); - this.setEnabled(this.enabled); - this.setText (this.text ); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a callback for change events - addChangeListener(listener) { - if (this.changeListeners.indexOf(listener) == -1) - this.changeListeners.push(listener); - } - - // Request focus on the appropriate element - focus() { - this.element.focus(); - } - - // Determine whether the component is checked - isChecked() { - return this.checked; - } - - // Determine whether the component is enabled - isEnabled() { - return this.enabled; - } - - // Specify whether the component is checked - setChecked(checked, e) { - checked = !!checked; - if (checked == this.checked) - return; - this.checked = checked; - this.element.setAttribute("aria-checked", checked); - if (e === undefined) - return; - for (let listener of this.changeListeners) - listener(e); - } - - // Specify whether the component is enabled - setEnabled(enabled) { - this.enabled = enabled = !!enabled; - this.element.setAttribute("aria-disabled", !enabled); - if (enabled) - this.element.setAttribute("tabindex", "0"); - else this.element.removeAttribute("tabindex"); - } - - // Specify the component's display text - setText(text) { - this.text = text = text || ""; - this.label.setText(text); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Key down event handler - onkeydown(e) { - - // Error checking - if (!this.enabled) - return; - - // Ignore the key - if (e.key != " ") - return; - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Toggle the checked state - this.setChecked(!this.checked, e); - } - - // Pointer down event handler - onpointerdown(e) { - - // Configure event - //e.preventDefault(); - e.stopPropagation(); - - // Configure focus - if (this.enabled) - this.focus(); - else return; - - // Error checking - if (e.button != 0 || this.element.hasPointerCapture(e.captureId)) - return; - - // Configure element - this.element.setPointerCapture(e.pointerId); - this.element.setAttribute("active", ""); - } - - // Pointer move event handler - onpointermove(e) { - - // Error checking - if (!this.element.hasPointerCapture(e)) - return; - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Working variables - let bounds = this.getBounds(); - let active = - e.x >= bounds.x && e.x < bounds.x + bounds.width && - e.y >= bounds.y && e.y < bounds.y + bounds.height - ; - - // Configure element - if (active) - this.element.setAttribute("active", ""); - else this.element.removeAttribute("active"); - } - - // Pointer up event handler - onpointerup(e) { - - // Configure event - e.stopPropagation(); - - // Error checking - if (!this.element.hasPointerCapture(e.pointerId)) - return; - - // Configure element - this.element.releasePointerCapture(e.pointerId); - - // Activate the component if it is active - if (!this.element.hasAttribute("active")) - return; - this.element.removeAttribute("active"); - this.setChecked(!this.checked, e); - } - -}; diff --git a/app/toolkit/Component.js b/app/toolkit/Component.js index 2224ad8..9d00b30 100644 --- a/app/toolkit/Component.js +++ b/app/toolkit/Component.js @@ -1,179 +1,312 @@ -"use strict"; +let Toolkit; -// Base features for all components -Toolkit.Component = class Component { - // Object constructor - constructor(application, tagname, options) { - options = options || {}; + +/////////////////////////////////////////////////////////////////////////////// +// Component // +/////////////////////////////////////////////////////////////////////////////// + +// Abstract class representing a distinct UI element +class Component { + + ///////////////////////// Initialization Methods ////////////////////////// + + constructor(gui, options, defaults) { // Configure instance fields - this.application = application; - this.containers = [ this ]; - this.display = null; - this.element = document.createElement(tagname); - this.id = this.element.id = Toolkit.id(); - this.parent = null; - this.properties = {}; - this.resizeListeners = []; - this.resizeObserver = null; - this.visible = "visible" in options ? !!options.visible : true; + 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.element.component = this; - this.setSize( - "width" in options ? options.width : null, - "height" in options ? options.height : null - ); - this.setVisible(this.visible); + 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 callback for resize events - addResizeListener(listener) { - if (this.resizeListeners.indexOf(listener) != -1) - return; - if (this.resizeObserver == null) { - this.resizeObserver = new ResizeObserver(()=>this.onresized()); - this.resizeObserver.observe(this.element); - } - this.resizeListeners.push(listener); + // 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; } - // Retrieve the bounding box of the element + // 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(); } - // Retrieve the display CSS property of the visible element - getDisplay() { - return this.display; + // Determine whether this component currently has focus + hasFocus() { + return document.activeElement == this.element; } // Determine whether the component is visible isVisible() { - for (let comp = this; comp != null; comp = comp.parent) - if (!comp.visible) + + // 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; } - // Specify the location and size of the component - setBounds(left, top, width, height, minimum) { - this.setLeft (left ); - this.setTop (top ); - this.setWidth (width , minimum); - this.setHeight(height, minimum); + // 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 the display CSS property of the visible element - setDisplay(display) { - this.display = display || null; - this.setVisible(this.visible); + // 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; } - // Specify the height of the component - setHeight(height, minimum) { - if (height === null) { - this.element.style.removeProperty("min-height"); - this.element.style.removeProperty("height"); - } else { - height = typeof height == "number" ? - Math.max(0, height) + "px" : height; - this.element.style.height = height; - if (minimum) - this.element.style.minHeight = height; - else this.element.style.removeProperty("min-height"); - } + // Remove an event listener + removeEventListener(type, listener, useCapture) { + this.element.removeEventListener(type, listener, useCapture); } - // Specify the horizontal position of the component - setLeft(left) { - if (left === null) - this.element.style.removeProperty("left"); - else this.element.style.left = - typeof left == "number" ? left + "px" : left ; + // 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 the absolute position of the component - setLocation(left, top) { - this.setLeft(left); - this.setTop (top ); + // 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 property value - setProperty(key, value) { - this.properties[key] = value; - this.localize(); + // Specify a localization key for the accessible name label + setLabel(key) { + this.label = key; + this.translate(); } - // Specify both the width and the height of the component - setSize(width, height, minimum) { - this.setHeight(height, minimum); - this.setWidth (width , minimum); + // Specify the DOM Id for this element + setId(id) { + this.id = id = id || null; + this.setAttribute("id", id); } - // Specify the vertical position of the component - setTop(top) { - if (top === null) - this.element.style.removeProperty("top"); - else this.element.style.top = - typeof top == "number" ? top + "px" : top ; + // 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) { - this.visible = visible = !!visible; - if (visible) { - if (this.display == null) - this.element.style.removeProperty("display"); - else this.element.style.display = this.display; - } else this.element.style.display = "none"; - } - - // Specify the width of the component - setWidth(width, minimum) { - if (width === null) { - this.element.style.removeProperty("min-width"); - this.element.style.removeProperty("width"); - } else { - width = typeof width == "number" ? - Math.max(0, width) + "px" : width; - this.element.style.width = width; - if (minimum) - this.element.style.minWidth = width; - else this.element.style.removeProperty("min-width"); - } + let prop = this.visibility ? "visibility" : "display"; + if (!!visible) + this.element.style.removeProperty(prop); + else this.element.style[prop] = this.visibility ? "hidden" : "none"; } ///////////////////////////// Package Methods ///////////////////////////// - // Determine whether this component contains another - contains(comp) { - if (comp == null) - return false; - if (comp instanceof Toolkit.Component) - comp = comp.element; - for (let cont of this.containers) - if ((cont instanceof Toolkit.Component ? cont.element : cont) - .contains(comp)) return true; - return false; + // Dispatch an event + event(type, fields) { + this.element.dispatchEvent(Toolkit.event(type, this, fields)); } + // Update the global Toolkit object + static setToolkit(toolkit) { + Toolkit = toolkit; + } - - ///////////////////////////// Private Methods ///////////////////////////// - - // Resize event handler - onresized() { - let bounds = this.getBounds(); - for (let listener of this.resizeListeners) - listener(bounds); + // 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/Label.js b/app/toolkit/Label.js deleted file mode 100644 index e670cb6..0000000 --- a/app/toolkit/Label.js +++ /dev/null @@ -1,80 +0,0 @@ -"use strict"; - -// Display text component -Toolkit.Label = class Label extends Toolkit.Component { - - // Object constructor - constructor(application, options) { - super(application, options&&options.label ? "label" : "div", options); - options = options || {}; - - // Configure instance fields - this.focusable = "focusable" in options ? !!options.focusable : false; - this.localized = "localized" in options ? !!options.localized : false; - this.text = options.text || ""; - - // Configure element - this.element.style.cursor = "default"; - this.element.style.userSelect = "none"; - - // Configure properties - this.setFocusable(this.focusable); - this.setText (this.text); - if (this.localized) - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Request focus on the appropriate element - focus() { - if (this.focusable) - this.element.focus(); - } - - // Retrieve the label's display text - getText() { - return this.text; - } - - // Determine whether the component is focusable - isFocusable() { - return this.focusable; - } - - // Specify the label's display text - setText(text) { - this.text = text || ""; - this.localize(); - } - - // Specify whether the component is focusable - setFocusable(focusable) { - this.focusable = focusable = !!focusable; - if (focusable) { - this.element.setAttribute("tabindex", "0"); - this.localized && this.application && - this.application.addComponent(this); - } else { - this.element.removeAttribute("aria-label"); - this.element.removeAttribute("tabindex"); - this.localized && this.application && - this.application.removeComponent(this); - } - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update display text with localized strings - localize() { - let text = this.text; - if (this.localized && this.application) - text = this.application.translate(text, this); - this.element.innerText = text; - } - -}; diff --git a/app/toolkit/ListBox.js b/app/toolkit/ListBox.js deleted file mode 100644 index 6f205b9..0000000 --- a/app/toolkit/ListBox.js +++ /dev/null @@ -1,234 +0,0 @@ -"use strict"; - -// Multi-item list picker -Toolkit.ListBox = class ListBox extends Toolkit.Component { - - // Object constructor - constructor(application, options) { - super(application, "select", options); - options = options || {}; - - // Configure instance fields - this.changeListeners = []; - this.dropDown = true; - this.enabled = "enabled" in options ? !!options.enabled : true; - this.items = []; - this.name = options.name || ""; - - // Configure element - this.element.addEventListener("input", e=>this.onchange(e)); - - // Configure properties - this.setDropDown(this.dropDown); - this.setEnabled (this.enabled ); - this.setName (this.name ); - if ("items" in options) - this.add(options.items); - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add one or more items to the list box - add(items, offset, count) { - - // Configure arguments - if (!Array.isArray(items)) - items = [ items ]; - offset = offset || 0; - if (offset < 0 || offset >= items.length) - return; - count = count || items.length - offset; - if (count < 1) - return; - count = Math.min(count, items.length - offset) - - // Add the items to the list - for (let x = 0; x < count; x++) { - let item = items[offset + x]; - let option = new Toolkit.ListBox.Option(this.application, item); - this.items.push(option); - this.element.appendChild(option.element); - } - - } - - // Add a callback for chabge events - addChangeListener(listener) { - if (this.changeListeners.indexOf(listener) == -1) - this.changeListeners.push(listener); - } - - // Remove all items from the list - clear() { - for (let item of this.items) - this.remove(item); - this.items.splice(0, this.items.length); - } - - // Request focus on the appropriate element - focus() { - this.element.focus(); - } - - // Retrieve the component's accessible name - getName() { - return this.name; - } - - // Retrieve the index of the currently selected item - getSelectedIndex() { - return this.element.selectedIndex; - } - - // Retrieve the currently selected item - getSelectedItem() { - let index = this.element.selectedIndex; - return index == -1 ? null : this.items[index]; - } - - // Retrieve the value of the currently selected item - getValue() { - let item = this.getSelectedItem(); - return item == null ? null : item.getValue(); - } - - // Determine whether the component is a drop-down list - isDropDown() { - return this.dropDown; - } - - // Determine whether the component is enabled - isEnabled() { - return this.enabled; - } - - // Remove an item from the list - remove(item, delocalize) { - let index = this.items.indexOf(item); - - // Error checking - if (index == -1) - return; - - // Remove the element - item.element.remove(); - this.items.splice(index, 1); - - // De-localize the element - if (delocalize === undefined || delocalize) - item.application.removeComponent(item); - } - - // Specify whether the component is a drop-down list - setDropDown(dropDown) { - // Not yet implemented - } - - // Specify whether the component is enabled - setEnabled(enabled) { - this.enabled = enabled = !!enabled; - this.element.setAttribute("aria-disabled", !enabled); - } - - // Specify the component's accessible name - setName(name) { - this.name = name || ""; - this.localize(); - } - - // Specify the index of the selected item - setSelectedIndex(index) { - if (typeof index != "number") - return element.selectedIndex; - index = Math.max(Math.min(Math.trunc(index), this.items.length-1), -1); - this.element.selectedIndex = index; - return index; - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update display text with localized strings - localize() { - let name = this.name; - if (this.application) - name = this.application.translate(name, this); - this.element.setAttribute("aria-label", name); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Selection changed event handler - onchange(e) { - if (!this.enabled) - return; - for (let listener of this.changeListeners) - listener(e); - } - -}; - -// List box item -Toolkit.ListBox.Option = class Option extends Toolkit.Component { - - // Object constructor - constructor(application, options) { - super(application, "option", options); - options = options || {}; - - // Configure instance fields - this.localized = "localized" in options ? !!options.localized : true; - this.text = options.text || ""; - this.value = options.value || null; - - // Configure properties - this.setText (this.text ); - this.setValue(this.value); - if (this.localized) - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Retrieve the component's display text - getText() { - return this.text; - } - - // Retrieve the component's value - getValue() { - return this.value; - } - - // Specify the component's display text - setText(text) { - this.text = text || ""; - this.localize(); - } - - // Specify the component's value - setValue(value) { - this.value = value; - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update display text with localized strings - localize() { - let text = this.text; - if (this.localized && this.application) - text = this.application.translate(text, this); - this.element.innerText = text; - } - -}; diff --git a/app/toolkit/Menu.js b/app/toolkit/Menu.js deleted file mode 100644 index ad39ded..0000000 --- a/app/toolkit/Menu.js +++ /dev/null @@ -1,225 +0,0 @@ -"use strict"; - -// Selection within a MenuBar -Toolkit.Menu = class Menu extends Toolkit.MenuItem { - - // Object constructor - constructor(application, options) { - super(application, options); - - // Configure menu element - this.menu = this.add(this.application.newPanel({ - layout : "flex", - alignCross: "stretch", - direction : "column", - visible : false - })); - this.menu.element.style.position = "absolute"; - this.menu.element.setAttribute("role", "menu"); - this.menu.element.setAttribute("aria-labelledby", this.id); - this.menu.element.addEventListener("pointerdown",e=>this.stopEvent(e)); - this.menu.element.addEventListener("pointermove",e=>this.stopEvent(e)); - this.menu.element.addEventListener("pointerup" ,e=>this.stopEvent(e)); - this.containers.push(this.menu); - this.children = this.menu.children; - - // Configure element - this.element.setAttribute("aria-expanded", "false"); - this.element.setAttribute("aria-haspopup", "menu"); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Create a MenuItem and associate it with the application and component - newMenu(options, index) { - let menu = this.menu.add(new Toolkit.Menu( - this.application, options), index); - menu.child(); - menu.element.insertAdjacentElement("afterend", menu.menu.element); - return menu; - } - - // Create a MenuItem and associate it with the application and component - newMenuItem(options, index) { - let item = this.menu.add(new Toolkit.MenuItem( - this.application, options), index); - item.child(); - return item; - } - - // Specify whether the menu is enabled - setEnabled(enabled) { - super.setEnabled(enabled); - if (!this.enabled && this.parent.expanded == this) - this.setExpanded(false); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // The menu item was activated - activate(deeper) { - if (!this.enabled) - return; - this.setExpanded(true); - if (deeper && this.children.length > 0) - this.children[0].focus(); - } - - // Show or hide the pop-up menu - setExpanded(expanded) { - - // Setting expanded to false - if (!expanded) { - - // Hide the pop-up menu - this.element.setAttribute("aria-expanded", "false"); - this.menu.setVisible(false); - this.parent.expanded = null; - - // Close any expanded submenus - if (this.expanded != null) - this.expanded.setExpanded(false); - - return; - } - - // Hide the existing submenu of the parent - if (this.parent.expanded != null && this.parent.expanded != this) - this.parent.expanded.setExpanded(false); - this.parent.expanded = this; - - // Configure element - this.element.setAttribute("aria-expanded", "true"); - - // Configure pop-up menu - let barBounds = this.menuBar.element.getBoundingClientRect(); - let bounds = this.element.getBoundingClientRect(); - this.menu.setVisible(true); - this.menu.setLocation( - (bounds.x - barBounds.x) + "px", - (bounds.y + bounds.height - barBounds.y) + "px" - ); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Key press event handler - onkeydown(e) { - let index; - - // Processing by key - switch (e.key) { - - // Delegate to the MenuItem handler for these keys - case " " : - case "ArrowLeft": - case "End" : - case "Enter" : - case "Escape" : - case "Home" : - return super.onkeydown(e); - - // Conditional - case "ArrowDown": - - // Open the menu and select the first item (if any) - if (this.parent == this.menuBar) - this.activate(true); - - // Delegate to the MenuItem handler - else return super.onkeydown(e); - - break; - - // Conditional - case "ArrowRight": - - // Open the menu and select the first item (if any) - if (this.parent != this.menuBar) - this.activate(true); - - // Delegate to the MenuItem handler - else return super.onkeydown(e); - - break; - - // Conditional - case "ArrowUp": - - // Open the menu and select the last item (if any) - if (this.parent == this.menuBar) { - this.activate(false); - index = this.previousChild(0); - if (index != -1) - this.children[index].focus(); - } - - // Delegate to the MenuItem handler - else return super.onkeydown(e); - - break; - - default: return; - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Pointer down event handler - onpointerdown(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Error checking - if (!this.enabled || e.button != 0) - return; - - // Activate the menu - this.focus(); - this.activate(false); - } - - // Pointer move event handler - onpointermove(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Error checking - if ( - this.parent != this.menuBar || - this.parent.expanded == null || - this.parent.expanded == this - ) return; - - // Activate the menu - this.parent.expanded.setExpanded(false); - this.parent.expanded = this; - this.focus(); - this.setExpanded(true); - } - - // Pointer up event handler (prevent superclass behavior) - onpointerup(e) { - e.preventDefault(); - e.stopPropagation(); - } - - // Prevent an event from bubbling - stopEvent(e) { - e.preventDefault(); - e.stopPropagation(); - } - -}; diff --git a/app/toolkit/MenuBar.js b/app/toolkit/MenuBar.js index 8ebe6a9..2d9b7fa 100644 --- a/app/toolkit/MenuBar.js +++ b/app/toolkit/MenuBar.js @@ -1,119 +1,748 @@ -"use strict"; +import { Component } from /**/"./Component.js"; +let Toolkit; -// Main application menu bar -Toolkit.MenuBar = class MenuBar extends Toolkit.Panel { - // Object constructor - constructor(application, options) { - super(application, options); - // Configure instance fields - this.expanded = null; - this.lastFocus = null; - this.menuBar = this; - this.name = options.name || ""; +/////////////////////////////////////////////////////////////////////////////// +// Menu // +/////////////////////////////////////////////////////////////////////////////// - // Configure element - this.element.style.position = "relative"; - this.element.style.zIndex = "2"; - this.element.setAttribute("role", "menubar"); - this.setLayout("flex", { - direction: "row", - wrap : "false" +// 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", + } }); - this.setOverflow("visible", "visible"); - this.element.addEventListener( - "blur" , e=>this.onblur (e), { capture: true }); - this.element.addEventListener( - "focus", e=>this.onfocus(e), { capture: true }); - // Configure properties - this.setName(this.name); - application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a component as a child of this container - add(component, index) { - super.add(component, index); - component.child(); - return component; - } - - // Create a Menu and associate it with the application and component - newMenu(options, index) { - - // Create and add a new menu - let menu = this.add(new Toolkit.Menu(this.application,options), index); - menu.element.insertAdjacentElement("afterend", menu.menu.element); - - // Ensure only the first menu is focusable - for (let x = 0; x < this.children.length; x++) - this.children[x].element - .setAttribute("tabindex", x == 0 ? "0" : "-1"); - - return menu; - } - - // Return focus to where it was before the menu was activated - restoreFocus() { - if (!this.contains(document.activeElement)) - return; - let elm = this.lastFocus; - if (elm == null) - elm = document.body; - elm.focus(); - if (this.contains(document.activeElement)) - document.activeElement.blur(); - } - - // Specify the menu's accessible name - setName(name) { - this.name = name || ""; - this.localize(); + // Trap pointer events + this.addEventListener("pointerdown", e=>{ + e.stopPropagation(); + e.preventDefault(); + }); } ///////////////////////////// Package Methods ///////////////////////////// - // Blur event capture - onblur(e) { - if (this.contains(e.relatedTarget)) - return; - if (this.children.length > 0) - this.children[0].element.setAttribute("tabindex", "0"); - if (this.expanded != null) - this.expanded.setExpanded(false); - } - - // Focus event capture - onfocus(e) { - - // Configure tabstop on the first menu - if (this.children.length > 0) - this.children[0].element.setAttribute("tabindex", "-1"); - - // Retain a reference to the previously focused element - if (this.contains(e.relatedTarget)) - return; - let from = e.relatedTarget; - if (from == null) - from = document.body; - if ("component" in from) - from = from.component; - this.lastFocus = from; - } - - // Update display text with localized strings - localize() { - let text = this.name; - if (this.application) - text = this.application.translate(text, this); - this.element.setAttribute("aria-label", text); + // 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/MenuItem.js b/app/toolkit/MenuItem.js deleted file mode 100644 index 33b42c3..0000000 --- a/app/toolkit/MenuItem.js +++ /dev/null @@ -1,320 +0,0 @@ -"use strict"; - -// Selection within a Menu -Toolkit.MenuItem = class MenuItem extends Toolkit.Panel { - - // Object constructor - constructor(application, options) { - super(application, options); - - // Configure instance fields - this.clickListeners = []; - this.enabled = "enabled" in options ? !!options.enabled : true; - this.icon = options.icon || null; - this.text = options.text || ""; - this.shortcut = options.shortcut || null; - - // Configure base element - this.setLayout("flex", {}); - this.element.setAttribute("role", "menuitem"); - this.element.setAttribute("tabindex", "-1"); - this.element.addEventListener("keydown", e=>this.onkeydown(e)); - this.element.addEventListener("pointerdown", e=>this.onpointerdown(e)); - this.element.addEventListener("pointermove", e=>this.onpointermove(e)); - this.element.addEventListener("pointerup" , e=>this.onpointerup (e)); - - // Configure display text element - this.textElement = document.createElement("div"); - this.textElement.id = Toolkit.id(); - this.textElement.style.cursor = "default"; - this.textElement.style.flexGrow = "1"; - this.textElement.style.userSelect = "none"; - this.textElement.setAttribute("name", "text"); - this.element.appendChild(this.textElement); - this.element.setAttribute("aria-labelledby", this.textElement.id); - - // Configure properties - this.setEnabled(this.enabled); - this.setText (this.text); - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a callback for click events - addClickListener(listener) { - if (this.clickListeners.indexOf(listener) == -1) - this.clickListeners.push(listener); - } - - // Request focus on the appropriate element - focus() { - this.element.focus(); - } - - // Retrieve the item's display text - getText() { - return this.text; - } - - // Determine whether the item is enabled - isEnabled() { - return this.enabled; - } - - // Specify whether the item is enabled - setEnabled(enabled) { - this.enabled = enabled = !!enabled; - if (enabled) - this.element.removeAttribute("disabled"); - else this.element.setAttribute("disabled", ""); - } - - // Specify the item's display text - setText(text) { - this.text = text || ""; - this.localize(); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Configure this component to be a child of its parent - child() { - this.menuBar = this.parent; - while (!(this.menuBar instanceof Toolkit.MenuBar)) - this.menuBar = this.menuBar.parent; - this.menuItem = this instanceof Toolkit.Menu ? this : this.parent; - while (!(this.menuItem instanceof Toolkit.Menu)) - this.menuItem = this.menuItem.parent; - this.menuTop = this instanceof Toolkit.Menu ? this : this.parent; - while (this.menuTop.parent != this.menuBar) - this.menuTop = this.menuTop.parent; - } - - // Update display text with localized strings - localize() { - let text = this.text; - if (this.application) - text = this.application.translate(text, this); - this.textElement.innerText = text; - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // The menu item was activated - activate(e) { - if (!this.enabled) - return; - this.menuBar.restoreFocus(); - for (let listener of this.clickListeners) - listener(e, this); - } - - // Key press event handler - onkeydown(e) { - let index; - - // Processing by key - switch (e.key) { - - // Activate the item - case " ": - case "Enter": - this.activate(e); - break; - - // Select the next item - case "ArrowDown": - index = this.parent.nextChild( - this.parent.children.indexOf(this)); - if (index != -1) - this.parent.children[index].focus(); - break; - - // Conditional - case "ArrowLeft": - - // Move to the previous menu in the menu bar - if (this.menuItem.parent == this.menuBar) { - index = this.menuBar.previousChild( - this.menuBar.children.indexOf(this.menuItem)); - if (index != -1) { - let menu = this.menuBar.children[index]; - if (menu != this.menuTop) { - if (this.menuBar.expanded != null) - menu.activate(true); - else menu.focus(); - } - } - } - - // Close the containing submenu - else { - this.menuItem.setExpanded(false); - this.menuItem.focus(); - } - - break; - - // Move to the next menu in the menu bar - case "ArrowRight": - index = this.menuBar.nextChild( - this.menuBar.children.indexOf(this.menuTop)); - if (index != -1) { - let menu = this.menuBar.children[index]; - if (menu != this.menuTop) { - if (this.menuBar.expanded != null) - menu.activate(true); - else menu.focus(); - } - } - break; - - // Select the previous item - case "ArrowUp": - index = this.parent.previousChild( - this.parent.children.indexOf(this)); - if (index != -1) - this.parent.children[index].focus(); - break; - - // Conditional - case "End": - - // Select the last menu in the menu bar - if (this.parent == this.menuBar) { - index = this.menuBar.previousChild( - this.menuBar.children.length); - if (index != -1) { - let menu = this.menuBar.children[index]; - if (menu != this.menuTop) { - if (this.menuBar.expanded != null) - menu.activate(true); - else menu.focus(); - } - } - } - - // Select the last item in the menu - else { - index = this.menuItem.previousChild( - this.menuItem.children.length); - if (index != -1) - this.menuItem.children[index].focus(); - } - - break; - - // Return focus to the original element - case "Escape": - if (this.menuBar.expanded != null) - this.menuBar.expanded.setExpanded(false); - this.menuBar.restoreFocus(); - break; - - // Conditional - case "Home": - - // Select the first menu in the menu bar - if (this.parent == this.menuBar) { - index = this.menuBar.nextChild(-1); - if (index != -1) { - let menu = this.menuBar.children[index]; - if (menu != this.menuTop) { - if (this.menuBar.expanded != null) - menu.activate(true); - else menu.focus(); - } - } - } - - // Select the last item in the menu - else { - index = this.menuItem.nextChild(-1); - if (index != -1) - this.menuItem.children[index].focus(); - } - - break; - - default: return; - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Pointer down event handler - onpointerdown(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Configure focus - if (this.enabled) - this.focus(); - else return; - - // Error checking - if (e.button != 0 || this.element.hasPointerCapture(e.pointerId)) - return; - - // Configure element - this.element.setPointerCapture(e.pointerId); - this.element.setAttribute("active", ""); - } - - // Pointer move event handler - onpointermove(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Error checking - if (!this.element.hasPointerCapture(e.pointerid)) - return; - - // Working variables - let bounds = this.getBounds(); - let active = - e.x >= bounds.x && e.x < bounds.x + bounds.width && - e.y >= bounds.y && e.y < bounds.y + bounds.height - ; - - // Configure element - if (active) - this.element.setAttribute("active", ""); - else this.element.removeAttribute("active"); - } - - // Pointer up event handler - onpointerup(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Error checking - if (!this.element.hasPointerCapture(e.pointerId)) - return; - - // Configure element - this.element.releasePointerCapture(e.pointerId); - - // Activate the menu item if it is active - if (!this.element.hasAttribute("active")) - return; - this.element.removeAttribute("active"); - this.activate(e); - } - -}; diff --git a/app/toolkit/Panel.js b/app/toolkit/Panel.js deleted file mode 100644 index f8486bf..0000000 --- a/app/toolkit/Panel.js +++ /dev/null @@ -1,354 +0,0 @@ -"use strict"; - -// Box that can contain other components -Toolkit.Panel = class Panel extends Toolkit.Component { - - // Object constructor - constructor(application, options) { - super(application, "div", options); - options = options || {}; - - // Configure instance fields - this.alignCross = "start"; - this.alignMain = "start"; - this.application = application; - this.children = []; - this.columns = null; - this.direction = "row"; - this.focusable = "focusable" in options ? !!options.focusable:false; - this.hollow = "hollow" in options ? !!options.hollow : true; - this.layout = null; - this.name = options.name || ""; - this.overflowX = options.overflowX || "hidden"; - this.overflowY = options.overflowY || "hidden"; - this.rows = null; - this.wrap = false; - - // Configure properties - this.setFocusable(this.focusable); - this.setHollow (this.hollow); - this.setLayout (options.layout || null, options); - this.setName (this.name); - this.setOverflow (this.overflowX, this.overflowY); - if (this.application && this.focusable) - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Add a component as a child of this container - add(component, index) { - - // Determine the ordinal position of the element within the container - index = !(typeof index == "number") ? this.children.length : - Math.floor(Math.min(Math.max(0, index), this.children.length)); - - // Add the component to the container - let ref = this.children[index] || null; - component.parent = this; - this.element.insertBefore(component.element, - ref == null ? null : ref.element); - this.children.splice(index, 0, component); - - return component; - } - - // Request focus on the appropriate element - focus() { - if (this.focusable) - this.element.focus(); - } - - // Retrieve the component's accessible name - getName() { - return this.name; - } - - // Determine whether the component is focusable - isFocusable() { - return this.focusable; - } - - // Determine whether the component is hollow - isHollow() { - return this.hollow; - } - - // Create a Button and associate it with the application - newButton(options) { - return new Toolkit.Button(this.application, options); - } - - // Create a CheckBox and associate it with the application - newCheckBox(options) { - return new Toolkit.CheckBox(this.application, options); - } - - // Create a Label and associate it with the application - newLabel(options) { - return new Toolkit.Label(this.application, options); - } - - // Create a ListBox and associate it with the application - newListBox(options) { - return new Toolkit.ListBox(this.application, options); - } - - // Create a MenuBar and associate it with the application - newMenuBar(options) { - return new Toolkit.MenuBar(this.application, options); - } - - // Create a Panel and associate it with the application - newPanel(options) { - return new Toolkit.Panel(this.application, options); - } - - // Create a RadioButton and associate it with the application - newRadioButton(options) { - return new Toolkit.RadioButton(this.application, options); - } - - // Create a Splitter and associate it with the application - newSplitter(options) { - return new Toolkit.Splitter(this.application, options); - } - - // Create a TextBox and associate it with the application - newTextBox(options) { - return new Toolkit.TextBox(this.application, options); - } - - // Create a Window and associate it with the application - newWindow(options) { - return new Toolkit.Window(this.application, options); - } - - // Determine the index of the next visible child - nextChild(index) { - for (let x = 0; x <= this.children.length; x++) { - index = (index + 1) % this.children.length; - let comp = this.children[index]; - if (comp.isVisible()) - return index; - } - return -1; - } - - // Determine the index of the previous visible child - previousChild(index) { - for (let x = 0; x <= this.children.length; x++) { - index = (index + this.children.length - 1) % this.children.length; - let comp = this.children[index]; - if (comp.isVisible()) - return index; - } - return -1; - } - - // Remove a component from the container - remove(component) { - - // Locate the component in the children - let index = this.children.indexOf(component); - if (index == -1) - return; - - // Remove the component - component.parent = null; - component.element.remove(); - this.children.splice(index, 1); - } - - // Specify whether the component is focusable - setFocusable(focusable) { - this.focusable = focusable = !!focusable; - if (focusable) { - this.element.setAttribute("tabindex", "0"); - this.application && this.application.addComponent(this); - } else { - this.element.removeAttribute("aria-label"); - this.element.removeAttribute("tabindex"); - this.application && this.application.removeComponent(this); - } - } - - // Specify whether the component is hollow - setHollow(hollow) { - this.hollow = hollow = !!hollow; - if (hollow) { - this.element.style.minHeight = "0"; - this.element.style.minWidth = "0"; - } else { - this.element.style.removeProperty("min-height"); - this.element.style.removeProperty("min-width" ); - } - } - - // Configure the element's layout - setLayout(layout, options) { - - // Configure instance fields - this.layout = layout; - - // Processing by layout - options = options || {}; - switch (layout) { - case "block" : this.setBlockLayout (options); break; - case "desktop": this.setDesktopLayout(options); break; - case "flex" : this.setFlexLayout (options); break; - case "grid" : this.setGridLayout (options); break; - default : this.setNullLayout (options); break; - } - - } - - // Specify the component's accessible name - setName(name) { - this.name = name || ""; - if (this.focusable) - this.localize(); - } - - // Configure the panel's overflow scrolling behavior - setOverflow(x, y) { - this.element.style.overflowX = this.overflowX = x || "hidden"; - this.element.style.overflowY = this.overflowY = y || this.overflowX; - } - - // Specify the semantic role of the panel - setRole(role) { - if (!role) - this.element.removeAttribute("role"); - else this.element.setAttribute("role", "" + role); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Move a window to the foreground - bringToFront(wnd) { - for (let child of this.children) - child.element.style.zIndex = child == wnd ? "1" : "0"; - } - - // Update display text with localized strings - localize() { - let name = this.name; - if (this.application) - name = this.application.translate(name, this); - this.element.setAttribute("aria-label", name); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Resize event handler - onresize(desktop) { - - // Error checking - if (this.layout != "desktop") - return; - - // Ensure all child windows are visible in the viewport - for (let wnd of this.children) { - if (!wnd.isVisible()) - continue; - let bounds = wnd.getBounds(); - wnd.contain( - bounds.x - desktop.x, - bounds.y - desktop.y, - desktop, bounds - ); - } - - } - - // Configure a block layout - setBlockLayout(options) { - - // Configure instance fields - this.layout = "block"; - - // Configure element - this.setDisplay("block"); - } - - // Configure a desktop layout - setDesktopLayout(options) { - - // Configure instance fields - this.layout = "desktop"; - - // Configure element - this.setDisplay("block"); - this.element.style.position = "relative"; - if (this.resizeObserver == null) - this.addResizeListener(b=>this.onresize(b)); - } - - // Configure a flex layout - setFlexLayout(options) { - - // Configure instance fields - this.alignCross = options.alignCross || "start"; - this.alignMain = options.alignMain || "start"; - this.direction = options.direction || this.direction; - this.layout = "flex"; - this.wrap = !!options.wrap; - - // Working variables - let alignCross = this.alignCross; - let alignMain = this.alignMain; - if (alignCross == "start" || alignCross == "end") - alignCross = "flex-" + alignCross; - if (alignMain == "start" || alignMain == "end") - alignMain = "flex-" + alignMain; - - // Configure element - this.setDisplay("flex"); - this.element.style.alignItems = alignCross; - this.element.style.flexDirection = this.direction; - this.element.style.justifyContent = alignMain; - this.element.style.flexWrap = this.wrap ? "wrap" : "nowrap"; - } - - // Configure a grid layout - setGridLayout(options) { - - // Configure instance fields - this.columns = options.columns || null; - this.layout = "grid"; - this.rows = options.rows || null; - - // Configure element - this.setDisplay("grid"); - if (this.columns == null) - this.element.style.removeProperty("grid-template-columns"); - else this.element.style.gridTemplateColumns = this.columns; - if (this.rows == null) - this.element.style.removeProperty("grid-template-rows"); - else this.element.style.gridTemplateRows = this.rows; - } - - // Configure a null layout - setNullLayout(options) { - - // Configure instance fields - this.layout = null; - - // Configure element - this.setDisplay(null); - this.element.style.removeProperty("align-items" ); - this.element.style.removeProperty("flex-wrap" ); - this.element.style.removeProperty("grid-template-columns"); - this.element.style.removeProperty("grid-template-rows" ); - this.element.style.removeProperty("justify-content" ); - this.element.style.removeProperty("flex-direction" ); - } - -}; diff --git a/app/toolkit/RadioButton.js b/app/toolkit/RadioButton.js deleted file mode 100644 index c18d3b9..0000000 --- a/app/toolkit/RadioButton.js +++ /dev/null @@ -1,59 +0,0 @@ -"use strict"; - -// Select-only radio button -Toolkit.RadioButton = class RadioButton extends Toolkit.CheckBox { - - // Object constructor - constructor(application, options) { - super(application, options); - options = options || {}; - - // Configure instance fields - this.group = null; - - // Configure element - this.element.setAttribute("role", "radio"); - - // Configure properties - this.setGroup(options.group || null); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Retrieve the enclosing ButtonGroup - getGroup() { - return this.group; - } - - // Specify whether the component is checked (overrides superclass) - setChecked(checked, e) { - checked = !!checked; - if (e instanceof Event && !checked || checked == this.checked) - return; - - this.checked = checked; - this.element.setAttribute("aria-checked", checked); - if (this.group != null && e != this.group) - this.group.setChecked(this); - - if (e === undefined) - return; - for (let listener of this.changeListeners) - listener(e); - } - - // Specify the enclosing ButtonGroup - setGroup(group) { - group = group || null; - if (group == this.group) - return; - if (this.group != null) - this.group.remove(this); - this.group = group; - if (group != null) - group.add(this); - } - -}; diff --git a/app/toolkit/ScrollBar.js b/app/toolkit/ScrollBar.js new file mode 100644 index 0000000..889766b --- /dev/null +++ b/app/toolkit/ScrollBar.js @@ -0,0 +1,1149 @@ +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/Splitter.js b/app/toolkit/Splitter.js deleted file mode 100644 index 12fe4a7..0000000 --- a/app/toolkit/Splitter.js +++ /dev/null @@ -1,301 +0,0 @@ -"use strict"; - -// Interactive splitter -Toolkit.Splitter = class Splitter extends Toolkit.Component { - - // Object constructor - constructor(application, options) { - super(application, "div", options); - options = options || {}; - - // Configure instance fields - this.component = options.component || null; - this.dragPointer = null; - this.dragPos = 0; - this.dragSize = 0; - this.orientation = options.orientation || "horizontal"; - this.name = options.name || ""; - this.edge = options.edge || - (this.orientation == "horizontal" ? "top" : "left"); - - // Configure element - this.element.setAttribute("role" , "separator"); - this.element.setAttribute("tabindex" , "0"); - this.element.setAttribute("aria-valuemin", "0"); - this.element.addEventListener("keydown" , e=>this.onkeydown (e)); - this.element.addEventListener("pointerdown", e=>this.onpointerdown(e)); - this.element.addEventListener("pointermove", e=>this.onpointermove(e)); - this.element.addEventListener("pointerup" , e=>this.onpointerup (e)); - - // Configure properties - this.setComponent (this.component ); - this.setName (this.name ); - this.setOrientation(this.orientation); - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Request focus on the appropriate element - focus() { - this.element.focus(); - } - - // Retrieve the component managed by this Splitter - getComponent() { - return this.component; - } - - // Retrieve the component's accessible name - getName() { - return this.name; - } - - // Retrieve the component's orientation - getOrientation() { - return this.orientation; - } - - // Determine the current and maximum separator values - measure() { - let max = 0; - let now = 0; - - // Mesure the component - if (this.component != null && this.parent != null) { - let component = this.component.getBounds(); - let bounds = this.getBounds(); - let panel = this.parent.getBounds(); - - // Horizontal Splitter - if (this.orientation == "horizontal") { - max = panel.height - bounds.height; - now = Math.max(0, Math.min(max, component.height)); - this.component.setSize(null, now); - } - - // Vertical Splitter - else { - max = panel.width - bounds.width; - now = Math.max(0, Math.min(max, component.width)); - this.component.setSize(now, null); - } - - } - - // Configure element - this.element.setAttribute("aria-valuemax", max); - this.element.setAttribute("aria-valuenow", now); - } - - // Specify the component managed by this Splitter - setComponent(component) { - this.component = component = component || null; - this.element.setAttribute("aria-controls", - component == null ? "" : component.id); - this.measure(); - } - - // Specify the component's accessible name - setName(name) { - this.name = name || ""; - this.localize(); - } - - // Specify the component's orientation - setOrientation(orientation) { - switch (orientation) { - case "horizontal": - this.orientation = "horizontal"; - this.setSize(null, 3, true); - this.element.setAttribute("aria-orientation", "horizontal"); - this.element.style.cursor = "ew-resize"; - break; - case "vertical": - this.orientation = "vertical"; - this.setSize(3, null, true); - this.element.setAttribute("aria-orientation", "vertical"); - this.element.style.cursor = "ns-resize"; - } - this.measure(); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Update display text with localized strings - localize() { - let name = this.name; - if (this.application) { - name = this.application.translate(name, this); - } - this.element.setAttribute("aria-label", name); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Key press event handler - onkeydown(e) { - - // Error checking - if (this.component == null) - return; - - let pos = this.component.getBounds(); - let size = this .getBounds(); - let max = this.parent .getBounds(); - if (this.orientation == "horizontal") { - max = max .height; - pos = pos .height; - size = size.height; - } else { - max = max .width; - pos = pos .width; - size = size.width; - } - if (this.edge == "top" || this.edge == "left") - max -= size; - - // Processing by key - if (this.component != null) switch (e.key) { - case "ArrowDown": - switch (this.edge) { - case "bottom": - this.component.setSize(null, Math.min(max, pos - 6)); - break; - case "top": - this.component.setSize(null, Math.min(max, pos + 6)); - } - break; - case "ArrowLeft": - switch (this.edge) { - case "left": - this.component.setSize(Math.min(max, pos - 6), null); - break; - case "right": - this.component.setSize(Math.min(max, pos + 6), null); - } - break; - case "ArrowRight": - switch (this.edge) { - case "left": - this.component.setSize(Math.min(max, pos + 6), null); - break; - case "right": - this.component.setSize(Math.min(max, pos - 6), null); - } - break; - case "ArrowUp": - switch (this.edge) { - case "bottom": - this.component.setSize(null, Math.min(max, pos + 6)); - break; - case "top": - this.component.setSize(null, Math.min(max, pos - 6)); - } - break; - case "Escape": - if (this.dragPointer === null) - return; - this.element.releasePointerCapture(this.dragPointer); - this.dragPointer = null; - if (this.orientation == "horizontal") - this.component.setHeight(null, this.dragSize); - else this.component.setWidth(this.dragSize, null); - break; - default: return; - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Pointer down event handler - onpointerdown(e) { - - // Request focus - this.focus(); - - // Configure event - e.stopPropagation(); - - // Error checking - if ( - this.component == null || - e.button != 0 || - this.element.hasPointerCapture(e.pointerId) - ) return; - - // Capture the pointer - this.element.setPointerCapture(e.pointerId); - this.dragPointer = e.pointerId; - let bounds = this.component.getBounds(); - if (this.orientation == "horizontal") { - this.dragPos = e.y; - this.dragSize = bounds.height; - } else { - this.dragPos = e.x; - this.dragSize = bounds.width; - } - } - - // Pointer move event handler - onpointermove(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Error checking - if ( - this.component == null || - !this.element.hasPointerCapture(e.pointerId) - ) return; - - // Resize the component - let bounds = this.getBounds(); - let panel = this.parent.getBounds(); - switch (this.edge) { - case "bottom": - this.component.setSize(null, Math.max(0, Math.min( - this.dragSize - e.y + this.dragPos, - panel.height - bounds.height - ))); - break; - case "left": - this.component.setSize(Math.max(0, Math.min( - this.dragSize + e.x - this.dragPos, - panel.width - bounds.width - )), null); - break; - case "right": - this.component.setSize(Math.max(0, Math.min( - this.dragSize - e.x + this.dragPos, - panel.width - bounds.width - )), null); - break; - case "top": - this.component.setSize(null, Math.max(0, Math.min( - this.dragSize + e.y - this.dragPos, - panel.height - bounds.height - ))); - break; - } - this.measure(); - } - - // Pointer up event handler - onpointerup(e) { - e.preventDefault(); - e.stopPropagation(); - this.element.releasePointerCapture(e.pointerId); - this.dragPointer = null; - } - -}; diff --git a/app/toolkit/TextBox.js b/app/toolkit/TextBox.js index 3655d2e..b1d734b 100644 --- a/app/toolkit/TextBox.js +++ b/app/toolkit/TextBox.js @@ -1,138 +1,145 @@ -"use strict"; +import { Component } from /**/"./Component.js"; +let Toolkit; -Toolkit.TextBox = class TextBox extends Toolkit.Component { - constructor(application, options) { - super(application, "input", options); - options = options || {}; + +/////////////////////////////////////////////////////////////////////////////// +// 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.changeListeners = []; - this.commitListeners = []; - this.enabled = "enabled" in options ? !!options.enabled : true; - this.name = options.name || ""; - this.lastCommit = ""; + this.isEnabled = null; + this.maxLength = null; + this.pattern = null; - // Configure element - this.element.size = 1; - this.element.type = "text"; - this.element.addEventListener("blur" , e=>this.commit (e)); - this.element.addEventListener("input" , e=>this.onchange (e)); - this.element.addEventListener("keydown", e=>this.onkeydown(e)); + // 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 properties - this.setEnabled(this.enabled); - this.setName (this.name ); - this.setText (options.text || ""); - this.application.addComponent(this); + // 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 ////////////////////////////// - // Add a callback for change events - addChangeListener(listener) { - if (this.changeListeners.indexOf(listener) == -1) - this.changeListeners.push(listener); + // Programmatically commit the text box + commit() { + this.event("action"); } - // Add a callback for commit events - addCommitListener(listener) { - if (this.commitListeners.indexOf(listener) == -1) - this.commitListeners.push(listener); - } - - // Request focus on the appropriate element - focus() { - this.element.focus(); - } - - // Retrieve the component's accessible name - getName() { - return this.name; - } - - // Retrieve the component's display text + // Retrieve the control's value getText() { return this.element.value; } - // Determine whether the component is enabled - isEnabled() { - return this.enabled; - } - - // Specify whether the component is enabled + // Specify whether the button can be activated setEnabled(enabled) { - this.enabled = enabled = !!enabled; - this.element.setAttribute("aria-disabled", !enabled); - if (enabled) - this.element.removeAttribute("disabled"); - else this.element.setAttribute("disabled", ""); + this.isEnabled = enabled = !!enabled; + this.setAttribute("disabled", enabled ? null : "true"); } - // Specify the component's accessible name - setName(name) { - this.name = name || ""; - this.localize(); + // 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 the component's display text - setText(text) { - text = !text && text !== 0 ? "" : "" + text; - this.lastCommit = text; - this.element.value = text; + // 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 display text with localized strings - localize() { - let name = this.name; - if (this.application) - name = this.application.translate(name, this); - this.element.setAttribute("aria-label", name); + // Update the global Toolkit object + static setToolkit(toolkit) { + Toolkit = toolkit; } +} - ///////////////////////////// Private Methods ///////////////////////////// - // Input finalized - commit(e) { - let text = this.element.value || ""; - if (!this.enabled || text == this.lastCommit) - return; - this.lastCommit = text; - for (let listener of this.commitListeners) - listener(e, this); - } - - // Text changed event handler - onchange(e) { - e.stopPropagation(); - if (!this.enabled) - return; - for (let listener of this.changeListeners) - listener(e, this); - } - - // Key press event handler - onkeydown(e) { - - // Configure event - e.stopPropagation(); - - // Error checking - if (!this.enabled) - return; - - // The Enter key was pressed - if (e.key == "Enter") - this.commit(e); - } - -}; +export { TextBox }; diff --git a/app/toolkit/Toolkit.js b/app/toolkit/Toolkit.js index bf00ef4..fb314de 100644 --- a/app/toolkit/Toolkit.js +++ b/app/toolkit/Toolkit.js @@ -1,18 +1,334 @@ -"use strict"; +import { Component } from /**/"./Component.js"; +import { Button, CheckBox, Group, Radio } from /**/"./Button.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" ; -// Widget toolkit manager -(globalThis.Toolkit = class Toolkit { - // Static initializer + +/////////////////////////////////////////////////////////////////////////////// +// Toolkit // +/////////////////////////////////////////////////////////////////////////////// + +// Top-level user interface manager +let Toolkit = globalThis.Toolkit = (class GUI extends Component { + static initializer() { - // Static fields - Toolkit.lastId = 0; + // 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); + 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.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 "i" + (Toolkit.lastId++); + 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 index 998003e..40239f9 100644 --- a/app/toolkit/Window.js +++ b/app/toolkit/Window.js @@ -1,408 +1,699 @@ -"use strict"; +import { Component } from /**/"./Component.js"; +let Toolkit; -// Movable, sizeable child window -Toolkit.Window = class Window extends Toolkit.Panel { - // Object constructor - constructor(application, options) { - super(application, options ? { - height : options.height, - visible: true, - width : options.width - } : {}); - options = options || {}; + +/////////////////////////////////////////////////////////////////////////////// +// 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.closeListeners = []; - this.dragBounds = null; - this.dragClient = null; - this.dragCursor = { x: 0, y: 0 }; - this.dragEdge = null; - this.dragPointer = null; - this.lastFocus = this.element; - this.shown = "visible" in options ? !!options.visible : true; - this.visible = this.shown; + this.firstShown = false; + this.lastFocus = null; - // Configure element - this.setLayout("grid", { columns: "auto" }); - this.setRole("dialog"); - this.setLocation(0, 0); - this.element.style.position = "absolute"; - if (!this.shown) - this.element.style.visibility = "hidden"; - this.element.setAttribute("aria-modal", "false"); - this.element.setAttribute("focus" , "false"); - this.element.setAttribute("tabindex" , "0" ); - this.element.addEventListener( - "blur" , e=>this.onblur (e), { capture: true }); - this.element.addEventListener( - "focus", e=>this.onfocus(e), { capture: true }); - this.element.addEventListener("keydown" , e=>this.onkeydown (e)); - this.element.addEventListener("pointerdown", e=>this.onpointerdown(e)); - this.element.addEventListener("pointermove", e=>this.onpointermove(e)); - this.element.addEventListener("pointerup" , e=>this.onpointerup (e)); + // DOM container + this.contents = document.createElement("div"); + this.contents.style.display = "flex"; + this.contents.style.flexDirection = "column"; + this.element.append(this.contents); - // Primary visible container - this.body = this.add(this.newPanel({ - layout : "grid", - overflowX: "visible", - overflowY: "visible", - rows : "max-content auto" - })); - this.body.element.setAttribute("name", "body"); + // 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 = this.body.add(this.newPanel({ - layout : "grid", - columns: "max-content auto max-content", - hollow : false - })); - this.titleBar.element.setAttribute("name", "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 icon - this.titleIcon = this.titleBar.add(this.newPanel()); - this.titleIcon.element.setAttribute("name", "icon"); - this.titleIcon.element.setAttribute("aria-hidden", "true"); + // 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); - // Title bar title - this.title = this.titleBar.add(this.newLabel({ - localized: true, - text : options.title - })); - this.title.element.setAttribute("name", "title"); - this.title.setProperty("sim", ""); + // 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)); - // Title bar close - this.titleClose = this.titleBar.add(this.newButton({ - toolTip: "{app.close}" - })); - this.titleClose.element.setAttribute("name", "close"); - this.titleClose.element.setAttribute("aria-hidden", "true"); - this.titleClose.addClickListener(e=>this.onclose(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); - // Client area - this.client = this.body.add(this.newPanel()); - this.client.element.setAttribute("name", "client"); - this.setSize(options.width, options.height); + // 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 callback for close events - addCloseListener(listener) { - if (this.closeListeners.indexOf(listener) == -1) - this.closeListeners.push(listener); + // Add a DOM element to this component's element + append(child) { + let element = child instanceof Element ? child : child.element; + this.client.append(element); } - // Request focus on the appropriate element - focus() { - if (this.lastFocus != this) - this.lastFocus.focus(); - else this.element.focus(); + // 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) + ); } - // Retrieve the window's title text - getTitle() { - return this.title.getText(); + // Programmatically close the window + close() { + this.event("close"); } - // Specify the height of the component - setHeight(height, minimum) { - this.client && this.client.setHeight(height, minimum); + // 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 the window's title text - setTitle(title) { - this.title.setText(title); + // 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, focus) { - this.visible = visible = !!visible; - if (!visible) { - this.element.style.visibility = "hidden"; + setVisible(visible) { + super.setVisible(visible); + if (!visible || this.firstShown) return; - } - this.element.style.removeProperty("visibility"); - if (this.client === undefined) - return; - this.contain(); - if (focus) - this.focus(); - if (!this.shown) - this.firstShow(); + this.firstShown = true; + this.event("firstshow", this); } - // Specify the width of the component - setWidth(width, minimum) { - this.client && this.client.setWidth( - Math.max(64, width || 0), minimum); + + + ///////////////////////////// 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 ///////////////////////////// - // Position the window in the center of the desktop - center() { - let bounds = this.getBounds(); - let desktop = this.parent.getBounds(); - this.contain( - Math.floor((desktop.width - bounds.width ) / 2), - Math.ceil ((desktop.height - bounds.height) / 2), - desktop, bounds + // 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 ); } - // Position the window using a tentative location in the desktop - contain(x, y, desktop, bounds, client) { - desktop = desktop || this.parent.getBounds(); - bounds = bounds || this.getBounds(); - client = client || this.client.getBounds(); - if (x === undefined) - x = bounds.x - desktop.x; - if (y === undefined) - y = bounds.y - desktop.y; - - // Restrict window position - x = Math.min(x, desktop.width - 16); - x = Math.max(x, -bounds.width + 16); - y = Math.min(y, desktop.height - (client.y - bounds.y)); - y = Math.max(y, 0); - - // Configure element - this.setLocation(x, y); - } - - // Detect where in the window the pointer is - edge(e, bounds) { - bounds = bounds || this.getBounds(); - let x = e.x - bounds.x; - let y = e.y - bounds.y; - if (y < 3) { - if (x < 8) return "nw"; - if (x < bounds.width - 8) return "n" ; - return "ne"; - } - if (y >= bounds.height - 3) { - if (x < 8) return "sw"; - if (x < bounds.width - 8) return "s" ; - return "se"; - } - if (x < 3) { - if (y < 8) return "nw"; - if (y < bounds.height - 8) return "w" ; - return "sw"; - } - if (x >= bounds.width - 3) { - if (y < 8) return "ne"; - if (y < bounds.height - 8) return "e" ; - return "se"; - } - return null; - } - - // The window is being displayed for the first time - firstShow() { - this.shown = true; - } - - // Focus lost event capture - onblur(e) { - if (!this.contains(e.relatedTarget)) - this.element.setAttribute("focus", "false"); - } - - // Window close - onclose(e) { - for (let listener of this.closeListeners) - listener(e); - } - - // Focus gained event capture - onfocus(e) { - this.element.setAttribute("focus", "true"); - let target = e.target; - if (target == this.element) - target = this.lastFocus; - this.lastFocus = target; - if (target != e.target) - target.focus(); - this.parent.bringToFront(this); - } - - // Key pressed event handler - onkeydown(e) { - - // Only listening for Escape while dragging - if (e.key != "Escape" || this.dragPointer === null) - return; - - // Restore the window's position - let desktop = this.parent.getBounds(); - this.setLocation( - this.dragBounds.x - desktop.x, - this.dragBounds.y - desktop.y + // 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 ); - - // Restore the window's size - if (this.dragEdge != null) - this.setSize(this.dragClient.width, this.dragClient.height); - - // Configure instance fields - this.element.releasePointerCapture(this.dragPointer); - this.dragPointer = null; } - // Pointer down event handler - onpointerdown(e) { - - // Configure event - e.stopPropagation(); - - // Error checking - if ( - e.button != 0 || - this.element.hasPointerCapture(e.pointerId) - ) return; - - // Configure instance fields - this.dragBounds = this.getBounds(); - this.dragClient = this.client.getBounds(); - this.dragCursor.x = e.x; - this.dragCursor.y = e.y; - this.dragEdge = this.edge(e, this.dragBounds); - - // Don't perform a move if the cursor isn't in the title bar - let title = this.titleBar.getBounds(); - if (this.dragEdge == null && e.y >= title.y + title.height) - return; - - // Configure element - this.element.setPointerCapture(e.pointerId); - this.dragPointer = e.pointerId; + // 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 + ); } - // Pointer move event handler - onpointermove(e) { - - // Configure event - e.preventDefault(); - e.stopPropagation(); - - // Not dragging: set the cursor based on pointer location - if (!this.element.hasPointerCapture(e.pointerId)) { - let region = this.edge(e); - if (region == null) - this.element.style.removeProperty("cursor"); - else this.element.style.cursor = region + "-resize"; - return; - } - - // Working variables - let rX = e.x - this.dragCursor.x; - let rY = e.y - this.dragCursor.y; - let bounds = this.getBounds(); - let desktop = this.parent.getBounds(); - let client = this.client.getBounds(); - - // Move the window - if (this.dragEdge == null) { - this.contain( - this.dragBounds.x - desktop.x + rX, - this.dragBounds.y - desktop.y + rY, - desktop, bounds, client - ); - return; - } - - // Resizing on the north edge - if (this.dragEdge.startsWith("n")) { - let maxTop = desktop.height - client.y + bounds.y; - let top = this.dragBounds.y - desktop.y + rY; - let height = this.dragClient.height - rY; - - // Restrict window bounds - if (top > maxTop) { - height -= maxTop - top; - top = maxTop; - } - if (top < 0) { - height += top; - top = 0; - } - if (height < 0) { - top += height; - height = 0; - } - - // Configure element - this.setTop (top ); - this.setHeight(height); - } - - // Resizing on the west edge - if (this.dragEdge.endsWith("w")) { - let maxLeft = desktop.width - 16; - let left = this.dragBounds.x - desktop.x + rX; - let width = this.dragClient.width - rX; - - // Restrict window bounds - if (left > maxLeft) { - width -= maxLeft - left; - left = maxLeft; - } - if (width < 64) { - left -= 64 - width; - width = 64; - } - - // Configure element - this.setLeft (left ); - this.setWidth(width); - } - - // Resizing on the east edge - if (this.dragEdge.endsWith("e")) { - let width = this.dragClient.width + rX; - - // Restrict window bounds - width = Math.max(64, width); - width = Math.max(width, -this.dragClient.x + 16); - - // Configure element - this.setWidth(width); - } - - // Resizing on the south edge - if (this.dragEdge.startsWith("s")) { - let height = this.dragClient.height + rY; - - // Restrict window bounds - height = Math.max(0, height); - - // Configure element - this.setHeight(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); } - // Pointer up event handler - onpointerup(e) { + // Resize on the south border + resizeS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) { + this.setSize( + this.drag.startWidth, + this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar), + ); + } - // Configure event - e.preventDefault(); - e.stopPropagation(); + // 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) + ); + } - // Error checking - if (!this.element.hasPointerCapture(e.pointerId)) - return; + // 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) + ); + } - // Configure element - this.element.releasePointerCapture(e.pointerId); - this.dragPointer = null; + // 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/app/windows/CPUWindow.js b/app/windows/CPUWindow.js deleted file mode 100644 index b959edd..0000000 --- a/app/windows/CPUWindow.js +++ /dev/null @@ -1,667 +0,0 @@ -"use strict"; - -// CPU register and disassembler display -(globalThis.CPUWindow = class CPUWindow extends Toolkit.Window { - - // Static initializer - static initializer() { - - // System register IDs - this.ADTRE = 25; - this.CHCW = 24; - this.ECR = 4; - this.EIPC = 0; - this.EIPSW = 1; - this.FEPC = 2; - this.FEPSW = 3; - this.PC = -1; - this.PIR = 6; - this.PSW = 5; - this.TKCW = 7; - - // Program register names - this.PROGRAM = { - [ 2]: "hp", - [ 3]: "sp", - [ 4]: "gp", - [ 5]: "tp", - [31]: "lp" - }; - - } - - // Object constructor - constructor(debug, options) { - super(debug.gui, options); - - // Configure instance fields - this.address = 0xFFFFFFF0; - this.columns = [ 0, 0, 0, 0 ]; - this.debug = debug; - this.pendingDasm = { mode: null }; - this.pendingRegs = { mode: null }; - this.rows = []; - - // Configure properties - this.setProperty("sim", ""); - - // Configure elements - this.initDisassembler(); - this.initSystemRegisters(); - this.initProgramRegisters(); - this.initWindow(); - - // Disassembler on the left - this.mainWrap.add(this.dasmWrap); - - // Registers on the right - this.regs = this.newPanel({ - layout: "grid", - rows : "max-content max-content auto" - }); - this.regs.element.setAttribute("name", "wrap-registers"); - - // Splitter between disassembler and registers - this.mainSplit = this.newSplitter({ - component : this.regs, - orientation: "vertical", - edge : "right", - name : "{cpu.mainSplit}" - }); - this.mainSplit.element.setAttribute("name", "split-main"); - this.mainSplit.element.style.width = "3px"; - this.mainSplit.element.style.minWidth = "3px"; - this.mainSplit.element.style.cursor = "ew-resize"; - this.mainWrap.add(this.mainSplit); - - // Registers on the right - this.mainWrap.add(this.regs); - - // System registers on top - this.regs.add(this.sysWrap); - - // Splitter between system registers and program registers - this.regsSplit = this.regs.add(this.newSplitter({ - component : this.sysWrap, - orientation: "horizontal", - edge : "top", - name : "{cpu.regsSplit}" - })); - this.regsSplit.element.style.height = "3px"; - this.regsSplit.element.style.minHeight = "3px"; - this.regsSplit.element.style.cursor = "ns-resize"; - - // Program registers on the bottom - this.regs.add(this.proWrap); - } - - // Initialize disassembler pane - initDisassembler() { - - // Wrapping element to hide overflowing scrollbar - this.dasmWrap = this.newPanel({ - layout : "grid", - overflowX: "hidden", - overflowY: "hidden" - }); - this.dasmWrap.element.setAttribute("name", "wrap-disassembler"); - - // Main element - this.dasm = this.dasmWrap.add(this.newPanel({ - focusable: true, - name : "{cpu.disassembler}", - overflowX: "auto", - overflowY: "hidden" - })); - this.dasm.element.setAttribute("name", "disassembler"); - this.dasm.addResizeListener(b=>this.onresize(b)); - this.dasm.element.addEventListener("keydown", e=>this.onkeydasm(e)); - this.dasm.element.addEventListener("wheel" , e=>this.onwheel (e)); - - this.rows.push(this.dasm.add(new CPUWindow.Row(this.dasm))); - } - - // Initialize program registers pane - initProgramRegisters() { - - // Wrapping element to hide overflowing scrollbar - this.proWrap = this.newPanel({ - layout : "grid", - overflow: "hidden" - }); - this.proWrap.element.setAttribute("name", "wrap-program-registers"); - - // Main element - this.proRegs = this.proWrap.add(this.newPanel({ - overflowX: "auto", - overflowY: "scroll" - })); - this.proRegs.element.setAttribute("name", "program-registers"); - - // List of registers - this.proRegs.registers = {}; - for (let x = 0; x <= 31; x++) - this.addRegister(false, x, CPUWindow.PROGRAM[x] || "r" + x); - } - - // Initialize system registers pane - initSystemRegisters() { - - // Wrapping element to hide overflowing scrollbar - this.sysWrap = this.newPanel({ - layout : "grid", - overflow: "hidden" - }); - this.sysWrap.element.setAttribute("name", "wrap-system-registers"); - - // Main element - this.sysRegs = this.sysWrap.add(this.newPanel({ - overflowX: "auto", - overflowY: "scroll" - })); - this.sysRegs.element.setAttribute("name", "system-registers"); - - // List of registers - this.sysRegs.registers = {}; - this.addRegister(true, CPUWindow.PC , "PC" ); - this.addRegister(true, CPUWindow.PSW , "PSW" ); - this.addRegister(true, CPUWindow.ADTRE, "ADTRE"); - this.addRegister(true, CPUWindow.CHCW , "CHCW" ); - this.addRegister(true, CPUWindow.ECR , "ECR" ); - this.addRegister(true, CPUWindow.EIPC , "EIPC" ); - this.addRegister(true, CPUWindow.EIPSW, "EIPSW"); - this.addRegister(true, CPUWindow.FEPC , "FEPC" ); - this.addRegister(true, CPUWindow.FEPSW, "FEPSW"); - this.addRegister(true, CPUWindow.PIR , "PIR" ); - this.addRegister(true, CPUWindow.TKCW , "TKCW" ); - this.addRegister(true, 29 , "29" ); - this.addRegister(true, 30 , "30" ); - this.addRegister(true, 31 , "31" ); - this.sysRegs.registers[CPUWindow.PSW].setExpanded(true); - } - - // Initialize window and client - initWindow() { - - // Configure element - this.element.setAttribute("window", "cpu"); - - // Configure body - this.body.element.setAttribute("filter", ""); - - // Configure client - this.client.setLayout("grid", { - columns: "auto" - }); - - // Configure main wrapper - this.mainWrap = this.client.add(this.newPanel({ - layout : "grid", - columns: "auto max-content max-content" - })); - this.mainWrap.element.setAttribute("name", "wrap-main"); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Update the display with current emulation data - refresh(seekToPC, dasm, regs) { - if (dasm || dasm === undefined) - this.refreshDasm(this.address, 0, !!seekToPC); - if (regs || regs === undefined) - this.refreshRegs(); - } - - // Specify whether the component is visible - setVisible(visible, focus) { - let prev = this.visible - visible = !!visible; - super.setVisible(visible, focus); - if (visible && !prev) - this.refresh(); - } - - - - ///////////////////////////// Message Methods ///////////////////////////// - - // Message received - message(msg) { - switch (msg.command) { - case "GetRegisters": this.getRegisters(msg); break; - case "ReadBuffer" : this.readBuffer (msg); break; - case "SetRegister" : this.setRegister (msg); break; - case "RunNext": case "SingleStep": - this.debug.refresh(true); break; - } - } - - // Retrieved all register values - getRegisters(msg) { - - // Update controls - this.sysRegs.registers[CPUWindow.PC ] - .setValue(msg.pc, msg.pcFrom, msg.pcTo); - this.sysRegs.registers[CPUWindow.PSW ].setValue(msg.psw ); - this.sysRegs.registers[CPUWindow.ADTRE].setValue(msg.adtre); - this.sysRegs.registers[CPUWindow.CHCW ].setValue(msg.chcw ); - this.sysRegs.registers[CPUWindow.ECR ].setValue(msg.ecr ); - this.sysRegs.registers[CPUWindow.EIPC ].setValue(msg.eipc ); - this.sysRegs.registers[CPUWindow.EIPSW].setValue(msg.eipsw); - this.sysRegs.registers[CPUWindow.FEPC ].setValue(msg.fepc ); - this.sysRegs.registers[CPUWindow.FEPSW].setValue(msg.fepsw); - this.sysRegs.registers[CPUWindow.PIR ].setValue(msg.pir ); - this.sysRegs.registers[CPUWindow.TKCW ].setValue(msg.tkcw ); - this.sysRegs.registers[29 ].setValue(msg.sr29 ); - this.sysRegs.registers[30 ].setValue(msg.sr30 ); - this.sysRegs.registers[31 ].setValue(msg.sr31 ); - for (let x = 0; x <= 31; x++) - this.proRegs.registers[x].setValue(msg.program[x]); - - // Check for pending display updates - let mode = this.pendingDasm.mode; - this.pendingRegs.mode = null; - switch (mode) { - case "first": - case null : - return; - case "refresh": - this.refreshRegs(); - } - - } - - // Retrieved data for disassembly - readBuffer(msg) { - let lines = Math.min(msg.lines, this.rows.length); - - // Disassemble the visible instructions - let dasm = Disassembler.disassemble( - new Uint8Array(msg.buffer), 0, msg.address, msg.target, - msg.pc, msg.line, lines); - - // Ensure PC is visible if requested - let reseeking = false; - if (msg.seekToPC) { - let visible = this.lines(true); - let count = Math.min(msg.lines, visible); - let x; - - // Ensure PC is visible in the disassembly - for (x = 0; x < count; x++) - if (dasm[x].address == msg.pc) - break; - - // Seek to display PC in the view - if (x == count) { - reseeking = true; - this.seek(msg.pc, Math.floor(visible / 3)); - } - - } - - // Not seeking to PC - if (!reseeking) { - - // Configure instance fields - this.address = dasm[0].address; - - // Configure elements - for (let x = 0; x < lines; x++) - this.rows[x].update(dasm[x], this.columns, msg.pc); - for (let x = 0; x < lines; x++) - this.rows[x].setWidths(this.columns); - } - - // Check for pending display updates - let address = this.pendingDasm.address === null ? - this.address : this.pendingDasm.address; - let line = this.pendingDasm.line === null ? - 0 : this.pendingDasm.line ; - let mode = this.pendingDasm.mode; - this.pendingDasm.mode = null; - switch (mode) { - case "first": - case null : - return; - case "refresh": - case "scroll" : - case "seek" : - this.refreshDasm(address, line); - } - - } - - // Modified a register value - setRegister(msg) { - (msg.type == "program" ? this.proRegs : this.sysRegs) - .registers[msg.id].setValue(msg.value); - this.refreshDasm(this.address, 0); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Insert a register control to a register list - addRegister(system, id, name) { - let list = system ? this.sysRegs : this.proRegs; - let reg = new CPUWindow.Register(this.debug, list, system, id, name); - list.registers[id] = reg; - list.add(reg); - if (reg.expands) - list.add(reg.expansion); - } - - // The window is being displayed for the first time - firstShow() { - super.firstShow(); - this.center(); - this.mainSplit.measure(); - this.regsSplit.measure(); - this.seek(this.address, Math.floor(this.lines(true) / 3)); - } - - // Determine the height in pixels of one row of output - lineHeight() { - return Math.max(10, this.rows[0].address.getBounds().height); - } - - // Determine the number of rows of output - lines(fullyVisible) { - let gridHeight = this.dasm.getBounds().height; - let lineHeight = this.lineHeight(); - let ret = gridHeight / lineHeight; - ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret); - return Math.max(1, ret); - } - - // Key down event handler - onkeydasm(e) { - - // Control is pressed - if (e.ctrlKey) switch (e.key) { - - // Auto-fit - case "f": case "F": - for (let x = 0; x < this.columns.length; x++) - this.columns[x] = 0; - for (let row of this.rows) - row.setWidths(this.columns); - for (let row of this.rows) - row.measure(this.columns); - for (let row of this.rows) - row.setWidths(this.columns); - break; - - // Goto - case "g": case "G": - let addr = prompt(this.application.translate("{app.goto_}")); - if (addr === null) - break; - this.seek( - (parseInt(addr, 16) & 0xFFFFFFFE) >>> 0, - Math.floor(this.lines(true) / 3) - ); - break; - - default: return; - } - - // Processing by key - else switch (e.key) { - case "ArrowDown": this.scroll( 1); break; - case "ArrowUp" : this.scroll(-1); break; - case "PageDown" : this.scroll( this.lines(true)); break; - case "PageUp" : this.scroll(-this.lines(true)); break; - default: return; - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Key down event handler - onkeydown(e) { - - // Processing by key - switch (e.key) { - - // Run next - case "F10": - this.debug.core.postMessage({ - command : "RunNext", - dbgwnd : "CPU", - sim : this.debug.sim, - seekToPC: true - }); - break; - - // Single step - case "F11": - this.debug.core.postMessage({ - command : "SingleStep", - dbgwnd : "CPU", - sim : this.debug.sim, - seekToPC: true - }); - break; - - default: return super.onkeydown(e); - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Resize event handler - onresize(bounds) { - - // Update Splitters - this.mainSplit.measure(); - this.regsSplit.measure(); - - // Configure disassembler elements - let lines = this.lines(false); - for (let y = this.rows.length; y < lines; y++) - this.rows[y] = this.dasm.add(new CPUWindow.Row(this.dasm)); - for (let y = lines; y < this.rows.length; y++) - this.dasm.remove(this.rows[y]); - if (this.rows.length > lines) - this.rows.splice(lines, this.rows.length - lines); - this.refreshDasm(); - } - - // Mouse wheel event handler - onwheel(e) { - let sign = Math.sign(e.deltaY); - let mag = Math.abs (e.deltaY); - if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) - mag = Math.max(1, Math.floor(mag / this.lineHeight())); - - // Configure element - e.preventDefault(); - e.stopPropagation(); - - // Configure display - this.scroll(sign * mag); - } - - // Update the disassembler with current emulation data - refreshDasm(address, line, seekToPC) { - - // Do nothing while closed or already waiting to refresh - if (!this.isVisible() || this.pendingDasm.mode != null) - return; - - // Working variables - address = address !== undefined ? - (address & 0xFFFFFFFE) >>> 0 : this.address; - line = line || 0; - let lines = this.lines(false); - let start = -10 - Math.max(0, line); - let end = lines - Math.min(0, line); - - // Configure pending state - this.pendingDasm.mode = "first"; - this.pendingDasm.address = null; - this.pendingDasm.line = null; - - // Request bus data from the WebAssembly core - this.debug.core.postMessage({ - command : "ReadBuffer", - sim : this.debug.sim, - dbgwnd : "CPU", - address : (address + start * 4 & 0xFFFFFFFE) >>> 0, - line : line, - lines : lines, - target : address, - seekToPC: !!seekToPC, - size : (end - start + 1) * 4 - }); - } - - // Update the register list with current emulation data - refreshRegs() { - - // Schedule another refresh - if (this.pendingRegs.mode != null) - this.pendingRegs.mode = "refresh"; - - // Do nothing while closed or already waiting to refresh - if (!this.isVisible() || this.pendingRegs.mode != null) - return; - - // Configure pending state - this.pendingRegs.mode = "first"; - this.pendingRegs.address = null; - this.pendingRegs.line = null; - - // Request bus data from the WebAssembly core - this.debug.core.postMessage({ - command: "GetRegisters", - dbgwnd : "CPU", - sim : this.debug.sim - }); - } - - // Move to a new address relative to the current address - scroll(lines) { - switch (this.pendingDasm.mode) { - case "first" : - case "refresh": - this.pendingDasm.mode = "scroll"; - this.pendingDasm.line = -lines; - break; - case "seek" : - case "scroll": - this.pendingDasm.mode = "scroll"; - this.pendingDasm.line -= lines; - break; - case null: - this.refreshDasm(this.address, -lines); - } - } - - // Move to a new address positioned at a particular row of output - seek(address, line) { - switch (this.pendingDasm.mode) { - case "first" : - case "refresh": - this.pendingDasm.mode = "seek"; - this.pendingDasm.address = address; - this.pendingDasm.line = line; - break; - case "seek" : - case "scroll": - this.pendingDasm.mode = "seek"; - this.pendingDasm.address = address; - this.pendingDasm.line += line; - break; - case null: - this.refreshDasm(address, line); - } - } - -}).initializer(); - -// One row of disassembly -CPUWindow.Row = class Row extends Toolkit.Panel { - - // Object constructor - constructor(parent) { - super(parent.application, { - layout : "grid", - columns : "repeat(4, max-content)", - hollow : false, - overflowX: "visible", - overflowY: "visible" - }); - - // Configure element - this.element.style.justifyContent = "start"; - this.element.setAttribute("name", "row"); - - // Address column - this.address = this.add(parent.newLabel({ text: "\u00a0" })); - this.address.element.setAttribute("name", "address"); - - // Bytes column - this.bytes = this.add(parent.newLabel({ text: "\u00a0" })); - this.bytes.element.setAttribute("name", "bytes"); - - // Mnemonic column - this.mnemonic = this.add(parent.newLabel({ text: "\u00a0" })); - this.mnemonic.element.setAttribute("name", "mnemonic"); - - // Operands column - this.operands = this.add(parent.newLabel({ text: "\u00a0" })); - this.operands.element.setAttribute("name", "operands"); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Measure the content widths of each column of output - measure(columns) { - columns[0] = Math.max(columns[0], this.address .getBounds().width); - columns[1] = Math.max(columns[1], this.bytes .getBounds().width); - columns[2] = Math.max(columns[2], this.mnemonic.getBounds().width); - columns[3] = Math.max(columns[3], this.operands.getBounds().width); - } - - // Specify the column widths - setWidths(columns) { - this.address .element.style.minWidth = columns[0] + "px"; - this.bytes .element.style.minWidth = columns[1] + "px"; - this.mnemonic.element.style.minWidth = columns[2] + "px"; - this.operands.element.style.minWidth = columns[3] + "px"; - } - - // Update the output labels with emulation state content - update(line, columns, pc) { - if (pc == line.address) - this.element.setAttribute("pc", ""); - else this.element.removeAttribute("pc"); - - this.address.setText( - ("0000000" + line.address.toString(16).toUpperCase()).slice(-8)); - - let bytes = new Array(line.bytes.length); - for (let x = 0; x < bytes.length; x++) - bytes[x] = - ("0" + line.bytes[x].toString(16).toUpperCase()).slice(-2); - this.bytes.setText(bytes.join(" ")); - - this.mnemonic.setText(line.mnemonic); - this.operands.setText(line.operands); - - this.measure(columns); - } - -}; diff --git a/app/windows/Disassembler.js b/app/windows/Disassembler.js deleted file mode 100644 index 0b07f11..0000000 --- a/app/windows/Disassembler.js +++ /dev/null @@ -1,388 +0,0 @@ -"use strict"; - -// Decode and format instruction code -(globalThis.Disassembler = class Disassembler { - - // Static initializer - static initializer() { - - // Bcond conditions - this.BCONDS = [ - "BV" , "BL" , "BZ" , "BNH", "BN", "BR" , "BLT", "BLE", - "BNV", "BNL", "BNZ", "BH" , "BP", "NOP", "BGE", "BGT" - ]; - - // Mapping for bit string instruction IDs - this.BITSTRING = [ - "SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD", - null , null , null , null , - "ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" , - "ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" - ]; - - // Mapping for floating-point/Nintendo instruction IDs - this.FLOATENDO = [ - "CMPF.S" , null , "CVT.WS" , "CVT.SW" , - "ADDF.S" , "SUBF.S" , "MULF.S" , "DIVF.S" , - "XB" , "XH" , "REV" , "TRNC.SW", - "MPYHW" - ]; - - // Opcode definitions - this.OPDEFS = [ - { format: 1, mnemonic: "MOV" }, - { format: 1, mnemonic: "ADD" }, - { format: 1, mnemonic: "SUB" }, - { format: 1, mnemonic: "CMP" }, - { format: 1, mnemonic: "SHL" }, - { format: 1, mnemonic: "SHR" }, - { format: 1, mnemonic: "JMP" }, - { format: 1, mnemonic: "SAR" }, - { format: 1, mnemonic: "MUL" }, - { format: 1, mnemonic: "DIV" }, - { format: 1, mnemonic: "MULU" }, - { format: 1, mnemonic: "DIVU" }, - { format: 1, mnemonic: "OR" }, - { format: 1, mnemonic: "AND" }, - { format: 1, mnemonic: "XOR" }, - { format: 1, mnemonic: "NOT" }, - { format: 2, mnemonic: "MOV" }, - { format: 2, mnemonic: "ADD" }, - { format: 2, mnemonic: "SETF" }, - { format: 2, mnemonic: "CMP" }, - { format: 2, mnemonic: "SHL" }, - { format: 2, mnemonic: "SHR" }, - { format: 2, mnemonic: "CLI" }, - { format: 2, mnemonic: "SAR" }, - { format: 2, mnemonic: "TRAP" }, - { format: 2, mnemonic: "RETI" }, - { format: 2, mnemonic: "HALT" }, - { format: 0, mnemonic: null }, - { format: 2, mnemonic: "LDSR" }, - { format: 2, mnemonic: "STSR" }, - { format: 2, mnemonic: "SEI" }, - { format: 2, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 3, mnemonic: null }, - { format: 5, mnemonic: "MOVEA" }, - { format: 5, mnemonic: "ADDI" }, - { format: 4, mnemonic: "JR" }, - { format: 4, mnemonic: "JAL" }, - { format: 5, mnemonic: "ORI" }, - { format: 5, mnemonic: "ANDI" }, - { format: 5, mnemonic: "XORI" }, - { format: 5, mnemonic: "MOVHI" }, - { format: 6, mnemonic: "LD.B" }, - { format: 6, mnemonic: "LD.H" }, - { format: 0, mnemonic: null }, - { format: 6, mnemonic: "LD.W" }, - { format: 6, mnemonic: "ST.B" }, - { format: 6, mnemonic: "ST.H" }, - { format: 0, mnemonic: null }, - { format: 6, mnemonic: "ST.W" }, - { format: 6, mnemonic: "IN.B" }, - { format: 6, mnemonic: "IN.H" }, - { format: 6, mnemonic: "CAXI" }, - { format: 6, mnemonic: "IN.W" }, - { format: 6, mnemonic: "OUT.B" }, - { format: 6, mnemonic: "OUT.H" }, - { format: 7, mnemonic: null }, - { format: 6, mnemonic: "OUT.W" } - ]; - - // Program register names - this.PROREGNAMES = [ - "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" - ]; - - // SETF conditions - this.SETFS = [ - "V" , "L" , "Z" , "NH", "N", "T", "LT", "LE", - "NV", "NL", "NZ", "H" , "P", "F", "GE", "GT" - ]; - - // System register names - this.SYSREGNAMES = [ - "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 *****************************/ - - // Disassemble instructions as lines of text - static disassemble(buffer, doffset, address, target, pc, line, lines) { - let history, hIndex; - - // Two bytes before PC to ensure PC isn't skipped - let prePC = (pc - 2 & 0xFFFFFFFF) >>> 0; - - // Prepare history buffer - if (line > 0) { - history = new Array(line); - hIndex = 0; - } - - // Locate the line containing the target address - for (;;) { - - // Emergency error checking - if (doffset >= buffer.length) - throw "Error: Target address not in disassembly buffer"; - - // Determine the size of the current line of output - let size = address == prePC || - this.OPDEFS[buffer[doffset + 1] >>> 2].format < 4 ? 2 : 4; - - // The line contians the target address - if ((target - address & 0xFFFFFFFF) >>> 0 < size) - break; - - // Record the current line in the history - if (line > 0) { - let item = history[hIndex] = history[hIndex] || {}; - hIndex = hIndex < history.length - 1 ? hIndex + 1 : 0; - item.address = address; - item.doffset = doffset; - } - - // Advance to the next line - doffset += size; - address = (address + size & 0xFFFFFFFF) >>> 0; - } - - // The target address is before the first line of output - for (; line < 0; line++) { - let size = address == prePC || - this.OPDEFS[buffer[doffset + 1] >>> 2].format < 4 ? 2 : 4; - doffset += size; - address = (address + size & 0xFFFFFFFF) >>> 0; - } - - // The target address is after the first line of output - if (line > 0) { - let item = history[hIndex]; - - // Emergency error checking - if (!item) - throw "Error: First output not in disassembly history"; - - // Inherit the address of the first history item - address = item.address; - doffset = item.doffset; - } - - // Decode the lines of the output - let ret = new Array(lines); - for (let x = 0; x < lines; x++) { - let inst = ret[x] = this.decode(buffer, doffset, address); - let size = address == prePC ? 2 : inst.size; - doffset += size; - address = (address + size & 0xFFFFFFFF) >>> 0; - } - - return ret; - } - - - - /**************************** Private Methods ****************************/ - - // Retrieve the bits for an instruction - static bits(inst) { - return inst.size == 2 ? - inst.bytes[1] << 8 | inst.bytes[0] : ( - inst.bytes[1] << 24 | inst.bytes[0] << 16 | - inst.bytes[3] << 8 | inst.bytes[2] - ) >>> 0 - ; - } - - // Decode one line of output - static decode(buffer, doffset, address) { - let opcode = buffer[doffset + 1] >>> 2; - let opdef = this.OPDEFS[opcode]; - let size = opdef.format < 4 ? 2 : 4; - - // Emergency error checking - if (doffset + size > buffer.length) - throw "Error: Insufficient disassembly data"; - - // Produce output line object - let inst = { - address : address, - mnemonic: opdef.mnemonic, - opcode : opcode, - operands: null, - size : size, - bytes : new Uint8Array( - buffer.buffer.slice(doffset, doffset + size)) - }; - - // Processing by instruction format - switch (opdef.format) { - case 1: this.decodeFormat1(inst ); break; - case 2: this.decodeFormat2(inst ); break; - case 3: this.decodeFormat3(inst, address); break; - case 4: this.decodeFormat4(inst, address); break; - case 5: this.decodeFormat5(inst ); break; - case 6: this.decodeFormat6(inst ); break; - case 7: this.decodeFormat7(inst ); break; - } - - // Illegal opcode - if (inst.mnemonic == null) - inst.mnemonic = "---"; - - return inst; - } - - // Format I - static decodeFormat1(inst) { - let bits = this.bits(inst); - let reg1 = this.PROREGNAMES[bits & 31]; - switch (inst.opcode) { - case 0b000110: // JMP - inst.operands = "[" + reg1 + "]"; - break; - default: // All others - inst.operands = reg1 + ", " + this.PROREGNAMES[bits >> 5 & 31]; - } - } - - // Format II - static decodeFormat2(inst) { - let bits = this.bits(inst); - let reg2 = this.PROREGNAMES[bits >> 5 & 31]; - let other = bits & 31; - switch (inst.opcode) { - case 0b010010: // SETF - inst.operands = this.SETFS[other & 15] + ", " + reg2; - break; - case 0b011000: // TRAP - inst.operands = other; - break; - case 0b011100: // LDSR - inst.operands = reg2 + ", " + this.SYSREGNAMES[other]; - break; - case 0b011101: // STSR - inst.operands = this.SYSREGNAMES[other] + ", " + reg2; - break; - case 0b011111: // Bit string - inst.mnemonic = this.BITSTRING[other]; - break; - case 0b010110: // CLI - case 0b011001: // RETI - case 0b011010: // HALT - case 0b011110: // SEI - break; - case 0b010000: // MOV - case 0b010001: // ADD - case 0b010011: // CMP - inst.operands = this.signExtend(other, 5) + ", " + reg2; - break; - default: // SHL, SHR, SAR - inst.operands = other + ", " + reg2; - } - } - - // Format III - static decodeFormat3(inst, address) { - let bits = this.bits(inst); - let disp = this.signExtend(bits & 0x1FF, 9); - let cond = bits >> 9 & 15; - inst.mnemonic = this.BCONDS[cond]; - if (cond == 13) - return; // NOP - inst.operands = ("0000000" + ((address + disp & 0xFFFFFFFF) >>> 0) - .toString(16).toUpperCase()).slice(-8); - } - - // Format IV - static decodeFormat4(inst, address) { - let bits = this.bits(inst); - let disp = this.signExtend(bits & 0x3FFFFFF, 26); - inst.operands = ("0000000" + ((address + disp & 0xFFFFFFFF) >>> 0) - .toString(16).toUpperCase()).slice(-8); - } - - // Format V - static decodeFormat5(inst) { - let bits = this.bits(inst); - let reg2 = this.PROREGNAMES[bits >> 21 & 31]; - let reg1 = this.PROREGNAMES[bits >> 16 & 31]; - let imm = bits & 0xFFFF; - switch (inst.opcode) { - case 0b101001: // ADDI - inst.operands = - this.signExtend(imm, 16) + ", " + reg1 + ", " + reg2; - break; - default: // All others - inst.operands = "0x" + ("000" + imm.toString(16).toUpperCase()) - .slice(-4) + ", " + reg1 + ", " + reg2; - } - } - - // Format VI - static decodeFormat6(inst) { - let bits = this.bits(inst); - let reg2 = this.PROREGNAMES[bits >> 21 & 31]; - let reg1 = this.PROREGNAMES[bits >> 16 & 31]; - let disp = this.signExtend(bits & 0xFFFF, 16); - disp = disp == 0 ? "" : (disp < -255 || disp > 255) ? "0x" + - ("000" + (disp & 0xFFFF).toString(16).toUpperCase()).slice(-4) : - (disp < 0 ? "-" : "") + "0x" + - Math.abs(disp).toString(16).toUpperCase(); - switch (inst.opcode) { - case 0b110000: // LD.B - case 0b110001: // LD.H - case 0b110011: // LD.W - case 0b111000: // IN.B - case 0b111001: // IN.H - case 0b111010: // CAXI - case 0b111011: // IN.W - inst.operands = disp + "[" + reg1 + "], " + reg2; - break; - default: // Output and store - inst.operands = reg2 + ", " + disp + "[" + reg1 + "]"; - } - } - - // Format VII - static decodeFormat7(inst) { - let bits = this.bits(inst); - let reg2 = this.PROREGNAMES[bits >> 21 & 31]; - let reg1 = this.PROREGNAMES[bits >> 16 & 31]; - let subop = bits >> 10 & 63; - inst.mnemonic = this.FLOATENDO[subop]; - if (inst.mnemonic == null) - return; - switch (subop) { - case 0b001000: // XB - case 0b001001: // XH - inst.operands = reg2; - break; - default: // All others - inst.operands = reg1 + ", " + reg2; - } - } - - // Sign extend a value - static signExtend(value, bits) { - return value & 1 << bits - 1 ? value | -1 << bits : value; - } - -}).initializer(); diff --git a/app/windows/MemoryWindow.js b/app/windows/MemoryWindow.js deleted file mode 100644 index a33e7c3..0000000 --- a/app/windows/MemoryWindow.js +++ /dev/null @@ -1,502 +0,0 @@ -"use strict"; - -// Hex editor style memory viewer -globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { - - // Object constructor - constructor(debug, options) { - super(debug.gui, options); - - // Configure instance fields - this.address = 0x05000000; - this.debug = debug; - this.editDigit = null; - this.pending = { mode: null }; - this.rows = []; - this.selected = this.address; - - // Configure element - this.element.setAttribute("window", "memory"); - - // Configure body - this.body.element.setAttribute("filter", ""); - - // Configure client - this.client.setLayout("grid"); - - // Wrapping element to hide overflowing scrollbar - this.hexWrap = this.client.add(this.newPanel({ - layout : "grid", - columns: "auto" - })); - this.hexWrap.element.setAttribute("name", "wrap-hex"); - - // Configure hex viewer - this.hex = this.hexWrap.add(this.client.newPanel({ - focusable: true, - layout : "block", - hollow : false, - name : "{memory.hexEditor}", - overflowX: "auto", - overflowY: "hidden" - })); - this.hex.element.setAttribute("role", "grid"); - this.hex.element.setAttribute("name", "hex"); - this.hex.element.addEventListener("keydown", e=>this.onkeyhex(e)); - this.hex.element.addEventListener("wheel" , e=>this.onwheel (e)); - this.hex.addResizeListener(b=>this.onresize()); - - // Configure properties - this.setProperty("sim", ""); - this.rows.push(this.hex.add(new MemoryWindow.Row(this, this.hex, 0))); - this.application.addComponent(this); - } - - - - ///////////////////////////// Public Methods ////////////////////////////// - - // Update the display with current emulation data - refresh(address) { - - // Do nothing while closed or already waiting to refresh - if (!this.isVisible() || this.pending.mode !== null) - return; - - // Working variables - address = address === undefined ? this.address : address; - let lines = this.lines(false); - - // Configure pending state - this.pending.mode = "first"; - this.pending.address = null; - this.pending.line = null; - - // Request bus data from the WebAssembly core - this.debug.core.postMessage({ - command: "ReadBuffer", - sim : this.debug.sim, - dbgwnd : "Memory", - address: address, - lines : lines, - size : lines * 16 - }); - } - - // Specify whether the component is visible - setVisible(visible, focus) { - let prev = this.visible - visible = !!visible; - super.setVisible(visible, focus); - if (visible && !prev) - this.refresh(); - } - - - - ///////////////////////////// Message Methods ///////////////////////////// - - // Message received - message(msg) { - switch (msg.command) { - case "ReadBuffer": this.readBuffer(msg) ; break; - case "Write" : this.debug.refresh(false); break; - } - } - - // Received bytes from the bus - readBuffer(msg) { - let buffer = new Uint8Array(msg.buffer); - let lines = Math.min(msg.lines, this.rows.length); - - // Configure instance fields - this.address = msg.address; - - // Update display - for ( - let x = 0, address = msg.address, offset = 0; - x < lines && offset < buffer.length; - x++, address = (address + 16 & 0xFFFFFFF0) >>> 0, offset += 16 - ) this.rows[x].update(buffer, offset); - - // Check for pending display updates - let address = this.pending.address === null ? - this.address : this.pending.address; - let line = this.pending.line === null ? - 0 : this.pending.line ; - let mode = this.pending.mode; - this.pending.mode = null; - switch (mode) { - case "first": - case null : - return; - case "refresh": - case "scroll" : - case "seek" : - this.refresh((address + line * 16 & 0xFFFFFFF0) >>> 0); - } - - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Write the current edited value to the bus - commit(value) { - this.editDigit = null; - this.debug.core.postMessage({ - command: "Write", - sim : this.debug.sim, - dbgwnd : "Memory", - debug : true, - address: this.selected, - type : 0, - value : value - }); - } - - // The window is being displayed for the first time - firstShow() { - super.firstShow(); - this.center(); - } - - // Determine the height in pixels of one row of output - lineHeight() { - return Math.max(10, this.rows[0].addr.getBounds().height); - } - - // Determine the number of rows of output - lines(fullyVisible) { - let gridHeight = this.hex.getBounds().height; - let lineHeight = this.lineHeight(); - let ret = gridHeight / lineHeight; - ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret); - return Math.max(1, ret); - } - - // Focus lost event capture - onblur(e) { - super.onblur(e); - if (this.editDigit !== null && !this.contains(e.relatedTarget)) - this.commit(this.editDigit); - } - - // Key down event handler - onkeydown(e) { - let change = null; - let digit = null; - - // Processing by key - switch (e.key) { - case "ArrowDown" : change = 16 ; break; - case "ArrowLeft" : change = - 1 ; break; - case "ArrowRight": change = 1 ; break; - case "ArrowUp" : change = -16 ; break; - case "Enter" : change = 0 ; break; - case "PageDown" : change = visible; break; - case "PageUp" : change = -visible; break; - case "0": case "1": case "2": case "3": case "4": - case "5": case "6": case "7": case "8": case "9": - digit = e.key.codePointAt(0) - "0".codePointAt(0); - break; - case "a": case "b": case "c": case "d": case "e": case "f": - digit = e.key.codePointAt(0) - "a".codePointAt(0) + 10; - break; - case "A": case "B": case "C": case "D": case "E": case "F": - digit = e.key.codePointAt(0) - "A".codePointAt(0) + 10; - break; - default: return super.onkeydown(e); - } - - // Moving the selection - if (change !== null) { - if (this.editDigit !== null) - this.commit(this.editDigit); - this.setSelected((this.selected + change & 0xFFFFFFFF) >>> 0); - } - - // Entering a digit - if (digit !== null) { - let selected = this.selected; - if (this.editDigit !== null) { - this.commit(this.editDigit << 4 | digit); - selected++; - } else this.editDigit = digit; - if (!this.setSelected(selected)) - for (let row of this.rows) - row.update(); - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Key down event handler - onkeyhex(e) { - - // Control is not pressed - if (!e.ctrlKey) - return; - - // Processing by key - switch (e.key) { - case "g": case "G": - let addr = prompt(this.application.translate("{app.goto_}")); - if (addr === null) - break; - this.setSelected((parseInt(addr, 16) & 0xFFFFFFFF) >>> 0); - break; - default: return; - } - - // Configure event - e.preventDefault(); - e.stopPropagation(); - } - - // Resize event handler - onresize() { - let lines = this.lines(false); - for (let y = this.rows.length; y < lines; y++) - this.rows[y] = - this.hex.add(new MemoryWindow.Row(this, this.hex, y * 16)); - for (let y = lines; y < this.rows.length; y++) - this.hex.remove(this.rows[y]); - if (this.rows.length > lines) - this.rows.splice(lines, this.rows.length - lines); - this.refresh(); - } - - // Mouse wheel event handler - onwheel(e) { - let sign = Math.sign(e.deltaY); - let mag = Math.abs (e.deltaY); - if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) - mag = Math.max(1, Math.floor(mag / this.lineHeight())); - - // Configure element - e.preventDefault(); - e.stopPropagation(); - - // Configure display - this.scroll(sign * mag); - } - - // Move to a new address relative to the current address - scroll(lines) { - switch (this.pending.mode) { - case "first" : - case "refresh": - this.pending.mode = "scroll"; - this.pending.line = lines; - break; - case "scroll": - case "seek" : - this.pending.mode = "scroll"; - this.pending.line += lines; - break; - case null: - this.refresh((this.address + lines * 16 & 0xFFFFFFF0) >>> 0); - } - } - - // Move to a new address positioned at a particular row of output - seek(address, line) { - switch (this.pending.mode) { - case "first" : - case "refresh": - this.pending.mode = "seek"; - this.pending.address = address; - this.pending.line = line; - break; - case "scroll": - case "seek" : - this.pending.mode = "seek"; - this.pending.address = address; - this.pending.line += line; - break; - case null: - this.refresh((address - line * 16 & 0xFFFFFFF0) >>> 0); - } - } - - // Specify which byte value is selected - setSelected(selected) { - - // The selected cell is not changing - if (selected == this.selected) - return false; - - // An edit was in progress - if (this.editDigit !== null) - this.commit(this.editDigit); - - // Working variables - let pos = (selected - this.address & 0xFFFFFFFF) >>> 0; - let visible = this.lines(true) * 16; - - // The selected cell is visible - if (pos >= 0 && pos < visible) { - this.selected = selected; - for (let y = 0; y < this.rows.length; y++) - this.rows[y].checkSelected(); - return false; - } - - // Working variables - let down = (selected - this.address & 0xFFFFFFF0) >>> 0; - let up = (this.address - selected + 15 & 0xFFFFFFF0) >>> 0; - - // Seek to show the new selection in the view - this.selected = selected; - if (down <= up) { - this.seek((this.address + down & 0xFFFFFFFF) >>> 0, - visible / 16 - 1); - } else this.seek((this.address - up & 0xFFFFFFFF) >>> 0, 0); - return true; - } - -}; - -// One row of output -MemoryWindow.Row = class Row extends Toolkit.Panel { - - // Object constructor - constructor(wnd, parent, offset) { - super(parent.application, { - layout : "grid", - columns : "repeat(17, max-content)", - hollow : false, - overflowX: "visible", - overflowY: "visible" - }); - - // Configure instance fields - this.cells = new Array(16); - this.offset = offset; - this.wnd = wnd; - - // Configure element - this.element.setAttribute("role", "row"); - this.element.addEventListener("pointerdown", e=>this.onpointerdown(e)); - - // Address label - this.addr = this.add(parent.newLabel({ text: "\u00a0" })); - this.addr.element.setAttribute("role", "gridcell"); - this.addr.element.setAttribute("name", "address"); - - // Byte labels - for (let x = 0; x < 16; x++) - this.cells[x] = new MemoryWindow.Cell(wnd, this, offset + x); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Check whether any byte label is the selected byte - checkSelected() { - for (let cell of this.cells) - cell.checkSelected(); - } - - // Update the output labels with emulation state content - update(bytes, offset) { - this.addr.setText( - ("0000000" + this.address().toString(16).toUpperCase()).slice(-8)); - for (let cell of this.cells) - cell.update(bytes ? bytes[offset++] : cell.value); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Compute the current address of the row - address() { - return (this.wnd.address + this.offset & 0xFFFFFFFF) >>> 0; - } - - // Pointer down event handler - onpointerdown(e) { - - // Error checking - if (e.button != 0) - return; - - // Check whether the click is within the columns of cells - let next = this.cells[0].getBounds(); - if (e.x < next.left) - return; - - // Compute which cell was clicked - for (let x = 0; x < 16; x++) { - let cur = next; - if (x < 15) { - next = this.cells[x + 1].getBounds(); - if (e.x < (cur.right + next.left) / 2) - return this.wnd.setSelected(this.cells[x].address()); - } else if (e.x < cur.right) - return this.wnd.setSelected(this.cells[x].address()); - } - - } - -}; - -// One cell of output -MemoryWindow.Cell = class Cell extends Toolkit.Label { - - // Object constructor - constructor(wnd, parent, offset) { - super(wnd.application, { text: "\u00a0" }); - - // Configure instance fields - this.offset = offset; - this.wnd = wnd; - this.value = 0x00; - - // Configure element - this.element.setAttribute("role", "gridcell"); - this.element.setAttribute("name", "byte"); - - parent.add(this); - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Check whether this cell is the selected cell - checkSelected() { - let selected = this.address() == this.wnd.selected; - if (selected) - this.element.setAttribute("selected", ""); - else this.element.removeAttribute("selected"); - return selected; - } - - // Update the output with emulation state content - update(value) { - if (value === undefined) - value = this.value; - else this.value = value; - if (this.checkSelected() && this.wnd.editDigit !== null) { - this.setText("\u00a0" + - this.wnd.editDigit.toString(16).toUpperCase()); - } else this.setText(("0"+value.toString(16).toUpperCase()).slice(-2)); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Compute the current address of the cell - address() { - return (this.wnd.address + this.offset & 0xFFFFFFFF) >>> 0; - } - -}; diff --git a/app/windows/Register.js b/app/windows/Register.js deleted file mode 100644 index 7bcaf6c..0000000 --- a/app/windows/Register.js +++ /dev/null @@ -1,520 +0,0 @@ -"use strict"; - -// List item for CPU window register lists -(CPUWindow.Register = class Register extends Toolkit.Panel { - - // Static initializer - static initializer() { - let buffer = new ArrayBuffer(4); - this.F32 = new Float32Array(buffer); - this.S32 = new Int32Array (buffer); - this.U32 = new Uint32Array (buffer); - } - - // Object constructor - constructor(debug, parent, system, id, name) { - super(parent.application, { - layout : "grid", - columns : "auto max-content", - overflowX: "visible", - overflowY: "visible" - }); - - // Configure instance fields - this.debug = debug; - this.expanded = false; - this.expansion = null; - this.fields = {}; - this.format = "hex"; - this.id = id; - this.name = name; - this.parent = parent; - this.system = system; - this.value = 0x00000000; - - // Determine whether the register has expansion fields - this.expands = !system; - switch (id) { - case CPUWindow.CHCW: - case CPUWindow.ECR: - case CPUWindow.EIPSW: - case CPUWindow.FEPSW: - case CPUWindow.PC: - case CPUWindow.PIR: - case CPUWindow.PSW: - case CPUWindow.TKCW: - this.expands = true; - } - - // Configure element - this.element.setAttribute("name", "register"); - this.element.setAttribute("format", this.format); - - // Name/expansion "check box" - this.chkExpand = this.add(this.newCheckBox({ - enabled: this.expands, - text : name - })); - this.chkExpand.element.setAttribute("name", "expand"); - this.chkExpand.element.setAttribute("aria-expanded", "false"); - this.chkExpand.addChangeListener(e=> - this.setExpanded(this.chkExpand.isChecked())); - - // Value text box - this.txtValue = this.add(this.newTextBox({ text: "\u00a0" })); - this.txtValue.element.setAttribute("name", "value"); - this.txtValue.addCommitListener(e=>this.onvalue()); - - // Expansion controls - if (!system) - this.expansionProgram(); - else switch (id) { - case CPUWindow.CHCW : this.expansionCHCW(); break; - case CPUWindow.ECR : this.expansionECR (); break; - case CPUWindow.EIPSW: - case CPUWindow.FEPSW: - case CPUWindow.PSW : this.expansionPSW (); break; - case CPUWindow.PC : this.expansionPC (); break; - case CPUWindow.PIR : this.expansionPIR (); break; - case CPUWindow.TKCW : this.expansionTKCW(); break; - } - if (this.expands) { - this.chkExpand.element.setAttribute( - "aria-controls", this.expansion.id); - } - - } - - - - ///////////////////////////// Static Methods ////////////////////////////// - - // Force a 32-bit integer to be signed - static asSigned(value) { - this.U32[0] = value >>> 0; - return this.S32[0]; - } - - // Interpret a 32-bit integer as a float - static intBitsToFloat(value) { - this.U32[0] = value; - value = this.F32[0]; - return Number.isFinite(value) ? value : 0; - } - - // Interpret a float as a 32-bit integer - static floatToIntBits(value) { - if (!Number.isFinite(value)) - return 0; - this.F32[0] = value; - return this.U32[0]; - } - - - - ///////////////////////////// Package Methods ///////////////////////////// - - // Specify whether the expansion fields are visible - setExpanded(expanded) { - this.expanded = expanded = !!expanded; - this.expansion.setVisible(expanded); - this.chkExpand.setChecked(expanded); - this.chkExpand.element.setAttribute("aria-expanded", expanded); - } - - // Specify the display mode of the register value - setFormat(format) { - this.format = format; - this.setValue(this.value); - this.element.setAttribute("format", format.replace("_", "")); - } - - // Update the value of the register - setValue(value, pcFrom, pcTo) { - this.value = value; - - // Value text box - let text; - switch (this.format) { - case "float": - let e = value >> 23 & 0xFF; - let s = value & 0x007FFFFF; - - // Check for denormal number - if (e == 0x00 & s != 0) { - text = "Denormal"; - break; - } - - // Check for reserved operand - if (e == 0xFF) { - text = s!=0 ? "NaN" : value<0 ? "-Infinity" : "Infinity"; - break; - } - - // Check for negative zero - if ((value & 0xFFFFFFFF) >>> 0 == 0x80000000) { - text = "-0.0"; - break; - } - - // Format the number - text = CPUWindow.Register.intBitsToFloat(value).toFixed(6); - if (text.indexOf(".") == -1) - text += ".0"; - while (text.endsWith("0")) - text = text.substring(0, text.length - 1); - if (text.endsWith(".")) - text += "0"; - break; - case "hex": - text = ("0000000" + - (value >>> 0).toString(16).toUpperCase()).slice(-8); - break; - case "signed": - text = CPUWindow.Register.asSigned(value).toString(); - break; - case "unsigned": - text = (value >>> 0).toString(); - } - this.txtValue.setText(text); - - // Expansion fields - for (let field of Object.values(this.fields)) { - switch (field.type) { - case "bit": - field.setChecked(value >> field.bit & 1); - break; - case "decimal": - field.setText(value >> field.bit & field.mask); - break; - case "hex": - let digits = Math.max(1, Math.ceil(field.width / 4)); - field.setText(("0".repeat(digits) + - (value >> field.bit & field.mask) - .toString(16).toUpperCase() - ).slice(-digits)); - } - } - - // Special fields for PC - if (pcFrom === undefined) - return; - this.txtFrom.setText(("0000000" + - (pcFrom >>> 0).toString(16).toUpperCase()).slice(-8)); - this.txtTo .setText(("0000000" + - (pcTo >>> 0).toString(16).toUpperCase()).slice(-8)); - } - - - - ///////////////////////////// Private Methods ///////////////////////////// - - // Add a field component to the expansion area - addField(type, name, bit, width, readonly) { - let field, label, panel; - - // Processing by type - switch (type) { - - // Bit - case "bit": - field = this.newCheckBox({ - enabled: !readonly, - text : name - }); - field.addChangeListener(e=>this.onbit(field)); - this.expansion.add(field); - break; - - // Decimal number - case "decimal": - - // Field - field = this.newTextBox({ - enabled: !readonly, - name : name - }); - field.addCommitListener(e=>this.onnumber(field)); - label = this.newLabel({ - label: true, - text : name - }); - label.element.htmlFor = field.id; - if (readonly) - label.element.setAttribute("aria-disabled", "true"); - - // Enclose in a panel - panel = this.newPanel({ - layout : "flex", - alignCross: "center", - alignMain : "start", - direction : "row" - }); - panel.element.setAttribute("name", name); - panel.add(label); - panel.add(field); - this.expansion.add(panel); - break; - - // Hexadecimal number - case "hex": - field = this.newTextBox({ - enabled: !readonly, - name : name - }); - field.addCommitListener(e=>this.onnumber(field)); - label = this.newLabel({ - label: true, - text : name - }); - label.element.htmlFor = field.id; - if (readonly) - label.element.setAttribute("aria-disabled", "true"); - this.expansion.add(label); - this.expansion.add(field); - } - - // Configure field - field.bit = bit; - field.mask = (1 << width) - 1; - field.type = type; - field.width = width; - this.fields[name] = field; - } - - // Expansion controls for CHCW - expansionCHCW() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "block", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("name", "expansion"); - this.expansion.element.setAttribute("register", "chcw"); - - // Fields - this.addField("bit", "ICE", 1, 1, false); - } - - // Expansion controls for ECR - expansionECR() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "grid", - columns : "max-content auto", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("name", "expansion"); - this.expansion.element.setAttribute("register", "ecr"); - this.expansion.element.style.justifyContent = "start"; - - // Fields - this.addField("hex", "FECC", 16, 16, false); - this.addField("hex", "EICC", 0, 16, false); - } - - // Expansion controls for PC - expansionPC() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "grid", - columns : "auto max-content", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("name", "expansion"); - - // From text box - let lbl = this.expansion.add(this.newLabel({ - localized: true, - text : "{cpu.pcFrom}" - })); - this.txtFrom = this.expansion.add(this.newTextBox({ enabled: false })); - this.txtFrom.element.setAttribute("name", "value"); - - // To text box - lbl = this.expansion.add(this.newLabel({ - localized: true, - text : "{cpu.pcTo}" - })); - this.txtTo = this.expansion.add(this.newTextBox({ enabled: false })); - this.txtTo.element.setAttribute("name", "value"); - } - - // Expansion controls for PIR - expansionPIR() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "grid", - columns : "max-content auto", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("name", "expansion"); - this.expansion.element.setAttribute("register", "pir"); - - // Fields - this.addField("hex", "PT", 0, 16, true); - } - - // Expansion controls for program registers - expansionProgram() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "grid", - columns : "auto", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("role", "radiogroup"); - this.expansion.element.setAttribute("name", "expansion"); - this.expansion.element.setAttribute("register", "program"); - this.expansion.element.style.justifyContent = "start"; - - // Format selections - let group = new Toolkit.ButtonGroup(); - for (let opt of [ "hex", "signed", "unsigned", "float_" ]) { - let fmt = group.add(this.expansion.add(this.newRadioButton({ - checked: opt == "hex", - text : "{cpu." + opt + "}", - }))); - fmt.addChangeListener( - (opt=>e=>this.setFormat(opt)) - (opt.replace("_", "")) - ); - } - - } - - // Expansion controls for EIPSW, FEPSW and PSW - expansionPSW() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "grid", - columns : "max-content auto", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("name", "expansion"); - this.expansion.element.setAttribute("register", "psw"); - this.expansion.element.style.justifyContent = "start"; - - // Fields - this.addField("bit" , "CY" , 3, 1, false); - this.addField("bit" , "FRO", 9, 1, false); - this.addField("bit" , "OV" , 2, 1, false); - this.addField("bit" , "FIV", 8, 1, false); - this.addField("bit" , "S" , 1, 1, false); - this.addField("bit" , "FZD", 7, 1, false); - this.addField("bit" , "Z" , 0, 1, false); - this.addField("bit" , "FOV", 6, 1, false); - this.addField("bit" , "NP" , 15, 1, false); - this.addField("bit" , "FUD", 5, 1, false); - this.addField("bit" , "EP" , 14, 1, false); - this.addField("bit" , "FPR", 4, 1, false); - this.addField("bit" , "ID" , 12, 1, false); - this.addField("decimal", "I" , 16, 4, false); - this.addField("bit" , "AE" , 13, 1, false); - } - - // Expansion controls for TKCW - expansionTKCW() { - - // Expansion area - this.expansion = this.newPanel({ - layout : "grid", - columns : "max-content auto", - overflowX: "visible", - overflowY: "visible", - visible : false - }); - this.expansion.element.setAttribute("name", "expansion"); - this.expansion.element.setAttribute("register", "tkcw"); - this.expansion.element.style.justifyContent = "start"; - - // Fields - this.addField("bit" , "FIT", 7, 1, true); - this.addField("bit" , "FUT", 4, 1, true); - this.addField("bit" , "FZT", 6, 1, true); - this.addField("bit" , "FPT", 3, 1, true); - this.addField("bit" , "FVT", 5, 1, true); - this.addField("bit" , "OTM", 8, 1, true); - this.addField("bit" , "RDI", 2, 1, true); - this.addField("decimal", "RD" , 0, 2, true); - } - - // Bit check box change event handler - onbit(field) { - let mask = 1 << field.bit; - let value = this.value; - if (field.isChecked()) - value = (value | mask & 0xFFFFFFFF) >>> 0; - else value = (value & ~mask & 0xFFFFFFFF) >>> 0; - this.setRegister(value); - } - - // Number text box commit event handler - onnumber(field) { - let value = parseInt(field.getText(), - field.type == "decimal" ? 10 : 16); - if (value == NaN) - value = this.value; - this.setRegister(( - this.value & ~(field.mask << field.bit) | - (value & field.mask) << field.bit - ) >>> 0); - } - - // Value text box commit event handler - onvalue() { - - // Process the entered value - let value = this.txtValue.getText(); - switch (this.format) { - case "float": - value = parseFloat(value); - if (Number.isFinite(value)) - value = CPUWindow.Register.floatToIntBits(value); - break; - case "hex": - value = parseInt(value, 16); - break; - case "signed": - case "unsigned": - value = parseInt(value); - } - - // Update the value - if (!Number.isFinite(value)) - this.setValue(this.value); - else this.setRegister((value & 0xFFFFFFFF) >>> 0); - } - - // Update the value of the register - setRegister(value) { - this.debug.core.postMessage({ - command: "SetRegister", - dbgwnd : "CPU", - id : this.id, - sim : this.debug.sim, - type : this.system ? this.id == -1 ? "pc" : "system" : "program", - value : value - }); - } - -}).initializer(); diff --git a/core/bus.c b/core/bus.c index f9f6eb7..d2c1353 100644 --- a/core/bus.c +++ b/core/bus.c @@ -64,7 +64,7 @@ static void busWriteMemory(uint8_t *mem, int type, int32_t value) { /***************************** Module Functions ******************************/ /* Read a data unit from the bus */ -static int32_t busRead(VB *emu, uint32_t address, int type, int debug) { +static int32_t busRead(VB *sim, uint32_t address, int type, int debug) { /* Force address alignment */ address &= ~((uint32_t) TYPE_SIZES[type] - 1); @@ -77,20 +77,20 @@ static int32_t busRead(VB *emu, uint32_t address, int type, int debug) { case 3 : return 0; /* Unmapped */ case 4 : return 0; /* Game pak expansion */ case 5 : return /* WRAM */ - busReadMemory(&emu->wram[address & 0xFFFF], type); - case 6 : return emu->cart.sram == NULL ? 0 : /* Game pak RAM */ - busReadMemory(&emu->cart.sram - [address & (emu->cart.sramSize - 1)], type); - default: return emu->cart.rom == NULL ? 0 : /* Game pak ROM */ - busReadMemory(&emu->cart.rom - [address & (emu->cart.romSize - 1)], type); + busReadMemory(&sim->wram[address & 0xFFFF], type); + case 6 : return sim->cart.sram == NULL ? 0 : /* Game pak RAM */ + busReadMemory(&sim->cart.sram + [address & (sim->cart.sramSize - 1)], type); + default: return sim->cart.rom == NULL ? 0 : /* Game pak ROM */ + busReadMemory(&sim->cart.rom + [address & (sim->cart.romSize - 1)], type); } } /* Write a data unit to the bus */ static void busWrite( - VB *emu, uint32_t address, int type, uint32_t value, int debug) { + VB *sim, uint32_t address, int type, uint32_t value, int debug) { /* Force address alignment */ address &= ~((uint32_t) TYPE_SIZES[type] - 1); @@ -103,17 +103,17 @@ static void busWrite( case 3 : return; /* Unmapped */ case 4 : return; /* Game pak expansion */ case 5 : /* WRAM */ - busWriteMemory(&emu->wram[address & 0xFFFF], type, value); + busWriteMemory(&sim->wram[address & 0xFFFF], type, value); return; case 6 : /* Cartridge RAM */ - if (emu->cart.sram != NULL) - busWriteMemory(&emu->cart.sram - [address & (emu->cart.sramSize - 1)], type, value); + if (sim->cart.sram != NULL) + busWriteMemory(&sim->cart.sram + [address & (sim->cart.sramSize - 1)], type, value); return; default: /* Cartridge ROM */ - if (debug && emu->cart.rom != NULL) - busWriteMemory(&emu->cart.rom - [address & (emu->cart.romSize - 1)], type, value); + if (debug && sim->cart.rom != NULL) + busWriteMemory(&sim->cart.rom + [address & (sim->cart.romSize - 1)], type, value); } } diff --git a/core/cpu.c b/core/cpu.c index 1410a43..21a0562 100644 --- a/core/cpu.c +++ b/core/cpu.c @@ -158,59 +158,59 @@ static const uint8_t CPU_SIZES[] = { /***************************** Utility Functions *****************************/ /* Read a data unit from the bus */ -static int cpuRead(VB *emu, uint32_t address, int8_t type) { +static int cpuRead(VB *sim, uint32_t address, int8_t type) { /* TODO: Determine clock count here */ - emu->cpu.access.clocks = 0; + sim->cpu.access.clocks = 0; /* Not using a breakpoint callback */ - if (emu->onRead == NULL) { - emu->cpu.access.value = busRead(emu, address, type, 0); + if (sim->onRead == NULL) { + sim->cpu.access.value = busRead(sim, address, type, 0); return 0; } /* Invoke the breakpoint callback */ - emu->cpu.access.address = address; - emu->cpu.access.type = type; - return emu->onRead(emu, &emu->cpu.access); + sim->cpu.access.address = address; + sim->cpu.access.type = type; + return sim->onRead(sim, &sim->cpu.access); } /* Read a data unit from the bus for the purpose of an instruction fetch */ -static int cpuReadFetch(VB *emu, uint32_t address) { +static int cpuReadFetch(VB *sim, uint32_t address) { /* TODO: Determine clock count here */ - emu->cpu.access.clocks = 0; + sim->cpu.access.clocks = 0; /* Not using a breakpoint callback */ - if (emu->onFetch == NULL) { - emu->cpu.access.value = busRead(emu, address, VB_U16, 0); + if (sim->onFetch == NULL) { + sim->cpu.access.value = busRead(sim, address, VB_U16, 0); return 0; } /* Invoke the breakpoint callback */ - emu->cpu.access.address = address; - emu->cpu.access.type = VB_U16; - return emu->onFetch(emu, emu->cpu.fetch, &emu->cpu.access); + sim->cpu.access.address = address; + sim->cpu.access.type = VB_U16; + return sim->onFetch(sim, sim->cpu.fetch, &sim->cpu.access); } /* Write a data unit to the bus */ -static int cpuWrite(VB *emu, uint32_t address, int8_t type, int32_t value) { +static int cpuWrite(VB *sim, uint32_t address, int8_t type, int32_t value) { /* TODO: Determine clock count here */ - emu->cpu.access.clocks = 0; - emu->cpu.access.type = type; + sim->cpu.access.clocks = 0; + sim->cpu.access.type = type; /* Using a breakpoint callback */ - if (emu->onWrite != NULL) { - emu->cpu.access.address = address; - emu->cpu.access.value = value; - if (emu->onWrite(emu, &emu->cpu.access)) + if (sim->onWrite != NULL) { + sim->cpu.access.address = address; + sim->cpu.access.value = value; + if (sim->onWrite(sim, &sim->cpu.access)) return 1; } /* Write the value if the operation wasn't cancelled */ - if (emu->cpu.access.type != VB_CANCEL) - busWrite(emu, address, type, value, 0); + if (sim->cpu.access.type != VB_CANCEL) + busWrite(sim, address, type, value, 0); return 0; } @@ -219,57 +219,57 @@ static int cpuWrite(VB *emu, uint32_t address, int8_t type, int32_t value) { /**************************** Instruction Helpers ****************************/ /* Add two numbers and update the flags */ -static int32_t cpuAdd(VB *emu, int32_t left, int32_t right) { +static int32_t cpuAdd(VB *sim, int32_t left, int32_t right) { int32_t result = left + right; - emu->cpu.clocks = 1; - emu->cpu.psw.cy = (uint32_t) result < (uint32_t) left; - emu->cpu.psw.ov = (~(left ^ right) & (left ^ result)) >> 31 & 1; - emu->cpu.psw.s = result < 0; - emu->cpu.psw.z = result == 0; + sim->cpu.clocks = 1; + sim->cpu.psw.cy = (uint32_t) result < (uint32_t) left; + sim->cpu.psw.ov = (~(left ^ right) & (left ^ result)) >> 31 & 1; + sim->cpu.psw.s = result < 0; + sim->cpu.psw.z = result == 0; return result; } /* Bit string search */ -static int cpuBitSearch(VB *emu, int32_t bit, int32_t dir) { +static int cpuBitSearch(VB *sim, int32_t bit, int32_t dir) { int32_t offset; /* Bit offset in source word */ int32_t value; /* Alias of source word */ /* Read the source word */ - if (emu->cpu.busWait == 0) { + if (sim->cpu.busWait == 0) { /* Initialize state */ - emu->cpu.program[30] &= 0xFFFFFFFC; - emu->cpu.program[27] &= 0x0000001F; - emu->cpu.psw.z = 1; + sim->cpu.program[30] &= 0xFFFFFFFC; + sim->cpu.program[27] &= 0x0000001F; + sim->cpu.psw.z = 1; /* The bit string is of zero length */ - if (emu->cpu.program[28] == 0) { - emu->cpu.clocks = dir == 1 ? 13 : 15; + if (sim->cpu.program[28] == 0) { + sim->cpu.clocks = dir == 1 ? 13 : 15; return 0; } /* Read the data unit from the bus */ - if (cpuRead(emu, emu->cpu.program[30], VB_S32)) + if (cpuRead(sim, sim->cpu.program[30], VB_S32)) return 1; /* Update state */ - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Update state */ - emu->cpu.busWait = 0; - emu->cpu.substring = 0; + sim->cpu.busWait = 0; + sim->cpu.substring = 0; /* Search the bit string */ for ( - offset = emu->cpu.program[27], value = emu->cpu.access.value; - emu->cpu.program[28] != 0 && (offset & 31) == offset; - offset += dir, emu->cpu.program[28]--, emu->cpu.program[29]++ + offset = sim->cpu.program[27], value = sim->cpu.access.value; + sim->cpu.program[28] != 0 && (offset & 31) == offset; + offset += dir, sim->cpu.program[28]--, sim->cpu.program[29]++ ) { /* The current bit does not match */ @@ -277,99 +277,99 @@ static int cpuBitSearch(VB *emu, int32_t bit, int32_t dir) { continue; /* The current bit matches */ - emu->cpu.program[27] = offset; - emu->cpu.clocks = dir == 1 ? 45 : 50; - emu->cpu.psw.z = 0; + sim->cpu.program[27] = offset; + sim->cpu.clocks = dir == 1 ? 45 : 50; + sim->cpu.psw.z = 0; return 0; } /* No bit in the current word matches */ - emu->cpu.program[27] = offset & 31; - if (emu->cpu.program[27] != offset) - emu->cpu.program[30] += (uint32_t) dir << 2; - if (emu->cpu.program[28] != 0) { - emu->cpu.substring = 1; - emu->cpu.clocks = 5; - } else emu->cpu.clocks = dir == 1 ? 46 : 51; + sim->cpu.program[27] = offset & 31; + if (sim->cpu.program[27] != offset) + sim->cpu.program[30] += (uint32_t) dir << 2; + if (sim->cpu.program[28] != 0) { + sim->cpu.substring = 1; + sim->cpu.clocks = 5; + } else sim->cpu.clocks = dir == 1 ? 46 : 51; return 0; } /* Bit string bitwise operation */ -static int cpuBitString(VB *emu, VB_INSTRUCTION *inst) { +static int cpuBitString(VB *sim, VB_INSTRUCTION *inst) { uint64_t bits; /* Shift register */ int32_t dest; /* Destination word value */ int32_t mask; /* Bit mask */ int32_t src; /* Source word value */ /* Initial invocation */ - if (emu->cpu.busWait == 0) { + if (sim->cpu.busWait == 0) { /* Initialize state */ - emu->cpu.program[30] &= 0xFFFFFFFC; - emu->cpu.program[29] &= 0xFFFFFFFC; - emu->cpu.program[27] &= 0x0000001F; - emu->cpu.program[26] &= 0x0000001F; + sim->cpu.program[30] &= 0xFFFFFFFC; + sim->cpu.program[29] &= 0xFFFFFFFC; + sim->cpu.program[27] &= 0x0000001F; + sim->cpu.program[26] &= 0x0000001F; /* The bit string is of zero length */ - if (emu->cpu.program[28] == 0) { - emu->cpu.clocks = 20; + if (sim->cpu.program[28] == 0) { + sim->cpu.clocks = 20; return 0; } /* Read the data unit from the bus */ - if (cpuRead(emu, emu->cpu.program[30], VB_S32)) + if (cpuRead(sim, sim->cpu.program[30], VB_S32)) return 1; /* Update state */ - inst->aux[0] = emu->cpu.access.value; - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + inst->aux[0] = sim->cpu.access.value; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Read the next source word */ - if (emu->cpu.busWait == 1) { + if (sim->cpu.busWait == 1) { /* Read the data unit from the bus */ - if (cpuRead(emu, emu->cpu.program[30] + 4, VB_S32)) + if (cpuRead(sim, sim->cpu.program[30] + 4, VB_S32)) return 1; /* Update state */ - inst->aux[1] = emu->cpu.access.value; - emu->cpu.busWait = 2; - emu->cpu.clocks = emu->cpu.access.clocks; + inst->aux[1] = sim->cpu.access.value; + sim->cpu.busWait = 2; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Read the destination word */ - if (emu->cpu.busWait == 2) { + if (sim->cpu.busWait == 2) { /* Read the data unit from the bus */ - if (cpuRead(emu, emu->cpu.program[29], VB_S32)) + if (cpuRead(sim, sim->cpu.program[29], VB_S32)) return 1; /* Update state */ - emu->cpu.busWait = 3; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 3; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Compute and store the destination word */ - if (emu->cpu.busWait == 3) { + if (sim->cpu.busWait == 3) { bits = ((uint64_t) inst->aux[1] << 32 | (uint32_t) inst->aux[0]); - dest = emu->cpu.access.value; - src = emu->cpu.program[27] <= emu->cpu.program[26] ? - bits << (emu->cpu.program[26] - emu->cpu.program[27]) : - bits >> (emu->cpu.program[27] - emu->cpu.program[26]) + dest = sim->cpu.access.value; + src = sim->cpu.program[27] <= sim->cpu.program[26] ? + bits << (sim->cpu.program[26] - sim->cpu.program[27]) : + bits >> (sim->cpu.program[27] - sim->cpu.program[26]) ; /* Perform the operation */ @@ -385,84 +385,84 @@ static int cpuBitString(VB *emu, VB_INSTRUCTION *inst) { } /* Incorporate only the bits occupied by the bit string */ - mask = (uint32_t) 0xFFFFFFFF << emu->cpu.program[26]; - if ((uint32_t)emu->cpu.program[28] < (uint32_t)32-emu->cpu.program[26]) - mask &= (1 << (emu->cpu.program[28] + emu->cpu.program[26])) - 1; - dest = (dest & mask) | (emu->cpu.access.value & ~mask); + mask = (uint32_t) 0xFFFFFFFF << sim->cpu.program[26]; + if ((uint32_t)sim->cpu.program[28] < (uint32_t)32-sim->cpu.program[26]) + mask &= (1 << (sim->cpu.program[28] + sim->cpu.program[26])) - 1; + dest = (dest & mask) | (sim->cpu.access.value & ~mask); /* Write the data unit to the bus */ - if (cpuWrite(emu, emu->cpu.program[29], VB_S32, dest)) + if (cpuWrite(sim, sim->cpu.program[29], VB_S32, dest)) return 1; /* Update state */ - emu->cpu.busWait = 4; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 4; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Working variables */ - mask = 32 - emu->cpu.program[26]; /* Bits processed this invocation */ - if ((uint32_t) emu->cpu.program[28] < (uint32_t) mask) - mask = emu->cpu.program[28]; - dest = emu->cpu.program[26] + mask; /* New destination bit offset */ - src = emu->cpu.program[27] + mask; /* New source bit offset */ + mask = 32 - sim->cpu.program[26]; /* Bits processed this invocation */ + if ((uint32_t) sim->cpu.program[28] < (uint32_t) mask) + mask = sim->cpu.program[28]; + dest = sim->cpu.program[26] + mask; /* New destination bit offset */ + src = sim->cpu.program[27] + mask; /* New source bit offset */ /* Update state */ - emu->cpu.busWait = 0; - emu->cpu.substring = mask != emu->cpu.program[28]; - emu->cpu.program[26] = dest & 31; - emu->cpu.program[27] = src & 31; - emu->cpu.program[28] -= mask; + sim->cpu.busWait = 0; + sim->cpu.substring = mask != sim->cpu.program[28]; + sim->cpu.program[26] = dest & 31; + sim->cpu.program[27] = src & 31; + sim->cpu.program[28] -= mask; if (dest >= 32) - emu->cpu.program[29] += 4; + sim->cpu.program[29] += 4; if (src >= 32) - emu->cpu.program[30] += 4; - if (emu->cpu.substring) { - emu->cpu.busWait = 1; - emu->cpu.clocks = 6; + sim->cpu.program[30] += 4; + if (sim->cpu.substring) { + sim->cpu.busWait = 1; + sim->cpu.clocks = 6; inst->aux[0] = inst->aux[1]; } else { - emu->cpu.busWait = 0; - emu->cpu.clocks = 36; + sim->cpu.busWait = 0; + sim->cpu.clocks = 36; } return 0; } /* Common processing for most bitwise operations */ -static int32_t cpuBitwise(VB *emu, int32_t result) { - emu->cpu.clocks = 1; - emu->cpu.psw.ov = 0; - emu->cpu.psw.s = result < 0; - emu->cpu.psw.z = result == 0; +static int32_t cpuBitwise(VB *sim, int32_t result) { + sim->cpu.clocks = 1; + sim->cpu.psw.ov = 0; + sim->cpu.psw.s = result < 0; + sim->cpu.psw.z = result == 0; return result; } /* Test a condition code */ -static int cpuCondition(VB *emu, int cond) { +static int cpuCondition(VB *sim, int cond) { /* Falsey condition */ if (cond > 7) - return !cpuCondition(emu, cond & 7); + return !cpuCondition(sim, cond & 7); /* Truthy condition */ switch (cond) { - case 0: return emu->cpu.psw.ov; /* V */ - case 1: return emu->cpu.psw.cy; /* L */ - case 2: return emu->cpu.psw.z; /* Z */ - case 3: return emu->cpu.psw.cy | emu->cpu.psw.z; /* NH */ - case 4: return emu->cpu.psw.s; /* N */ + case 0: return sim->cpu.psw.ov; /* V */ + case 1: return sim->cpu.psw.cy; /* L */ + case 2: return sim->cpu.psw.z; /* Z */ + case 3: return sim->cpu.psw.cy | sim->cpu.psw.z; /* NH */ + case 4: return sim->cpu.psw.s; /* N */ case 5: return 1; /* T */ - case 6: return emu->cpu.psw.ov ^ emu->cpu.psw.s; /* LT */ - default:return(emu->cpu.psw.ov^emu->cpu.psw.s)|emu->cpu.psw.z; /* LE */ + case 6: return sim->cpu.psw.ov ^ sim->cpu.psw.s; /* LT */ + default:return(sim->cpu.psw.ov^sim->cpu.psw.s)|sim->cpu.psw.z; /* LE */ } } /* Check for a floating-point reserved operand */ -static int cpuFloatReserved(VB *emu, int32_t bits) { +static int cpuFloatReserved(VB *sim, int32_t bits) { uint8_t e = bits >> 23; /* Exponent field */ /* Not reserved */ @@ -470,31 +470,31 @@ static int cpuFloatReserved(VB *emu, int32_t bits) { return 0; /* Reserved */ - emu->cpu.causeCode = 0xFF60; - emu->cpu.psw.fro = 1; + sim->cpu.causeCode = 0xFF60; + sim->cpu.psw.fro = 1; return 1; } /* Convert a 32-bit integer to the represented floating-point value */ -static double cpuFloatOperand(VB *emu, int32_t bits) { - return cpuFloatReserved(emu, bits) ? 0 : (double) *(float *) &bits; +static double cpuFloatOperand(VB *sim, int32_t bits) { + return cpuFloatReserved(sim, bits) ? 0 : (double) *(float *) &bits; } /* Convert a floating-point result to bits as a 32-bit integer */ -static int32_t cpuFloatResult(VB *emu, double resultd) { +static int32_t cpuFloatResult(VB *sim, double resultd) { float resultf; /* 32-bit conversion of result */ int32_t ret; /* Output value */ /* Overflow */ if (resultd < -FLT_MAX || resultd > FLT_MAX) { - emu->cpu.causeCode = 0xFF64; - emu->cpu.psw.fov = 1; + sim->cpu.causeCode = 0xFF64; + sim->cpu.psw.fov = 1; return 0; } /* Underflow */ if (resultd > -FLT_MIN && resultd < FLT_MIN) { - emu->cpu.psw.fud = 1; + sim->cpu.psw.fud = 1; ret = 0; } @@ -507,20 +507,20 @@ static int32_t cpuFloatResult(VB *emu, double resultd) { /* Precision degradation */ if (resultf != resultd) - emu->cpu.psw.fpr = 1; + sim->cpu.psw.fpr = 1; } /* Update state */ - emu->cpu.psw.cy = ret < 0; - emu->cpu.psw.ov = 0; - emu->cpu.psw.s = ret < 0; - emu->cpu.psw.z = ret == 0; + sim->cpu.psw.cy = ret < 0; + sim->cpu.psw.ov = 0; + sim->cpu.psw.s = ret < 0; + sim->cpu.psw.z = ret == 0; return ret; } /* Convert a floating-point value to a word value */ -static void cpuFloatToWord(VB *emu, VB_INSTRUCTION *inst, int truncate) { - int32_t bits = emu->cpu.program[inst->bits[0] & 0x1F]; +static void cpuFloatToWord(VB *sim, VB_INSTRUCTION *inst, int truncate) { + int32_t bits = sim->cpu.program[inst->bits[0] & 0x1F]; int32_t result; /* Output value */ int32_t x; /* Working variable */ int32_t y; /* Working variable */ @@ -539,15 +539,15 @@ static void cpuFloatToWord(VB *emu, VB_INSTRUCTION *inst, int truncate) { /* Reserved operand */ if (x == 0xFF || x == 0x00) { - emu->cpu.causeCode = 0xFF60; - emu->cpu.psw.fro = 1; + sim->cpu.causeCode = 0xFF60; + sim->cpu.psw.fro = 1; return; } /* Invalid operation */ if (x >= 158) { - emu->cpu.causeCode = 0xFF70; - emu->cpu.psw.fiv = 1; + sim->cpu.causeCode = 0xFF70; + sim->cpu.psw.fiv = 1; return; } @@ -566,7 +566,7 @@ static void cpuFloatToWord(VB *emu, VB_INSTRUCTION *inst, int truncate) { /* Precision degradation */ if ((y & ((1 << x) - 1)) != 0) { - emu->cpu.psw.fpr = 1; + sim->cpu.psw.fpr = 1; /* Apply rounding */ if (!truncate) @@ -581,148 +581,147 @@ static void cpuFloatToWord(VB *emu, VB_INSTRUCTION *inst, int truncate) { } /* Update state */ - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = result; - emu->cpu.psw.ov = 0; - emu->cpu.psw.s = result < 0; - emu->cpu.psw.z = result == 0; - emu->cpu.clocks = 14; + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = result; + sim->cpu.psw.ov = 0; + sim->cpu.psw.s = result < 0; + sim->cpu.psw.z = result == 0; + sim->cpu.clocks = 14; } /* Perform a jump */ -static void cpuJump(VB *emu, VB_INSTRUCTION *inst, uint32_t address) { - emu->cpu.pcFrom = emu->cpu.pc; - emu->cpu.pc = address & 0xFFFFFFFE; - emu->cpu.pcTo = emu->cpu.pc; - emu->cpu.clocks = 3; +static void cpuJump(VB *sim, VB_INSTRUCTION *inst, uint32_t address) { + sim->cpu.pc = address & 0xFFFFFFFE; + sim->cpu.clocks = 3; inst->size = 0; } /* Perform an input or load */ -static int cpuLoad(VB *emu,VB_INSTRUCTION *inst,uint8_t type,uint32_t clocks) { +static int cpuLoad(VB *sim,VB_INSTRUCTION *inst,uint8_t type,uint32_t clocks) { /* Initiate the read */ - if (emu->cpu.busWait == 0) { + if (sim->cpu.busWait == 0) { /* Read the data unit from the bus */ if (cpuRead( - emu, - emu->cpu.program[inst->bits[0] & 0x1F] + + sim, + sim->cpu.program[inst->bits[0] & 0x1F] + SignExtend((int32_t) inst->bits[1], 16), type )) return 1; /* Update state */ - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Complete the read */ - emu->cpu.busWait = 0; - emu->cpu.clocks = clocks; - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = emu->cpu.access.value; + sim->cpu.busWait = 0; + sim->cpu.clocks = clocks; + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = sim->cpu.access.value; return 0; } /* Specify a new value for a system register */ -static uint32_t cpuSetSystemRegister(VB *emu,int id,uint32_t value,int debug) { +static uint32_t cpuSetSystemRegister(VB *sim,int id,uint32_t value,int debug) { switch (id) { - case VB_ADTRE: return emu->cpu.adtre = value & 0xFFFFFFFE; - case VB_EIPC : return emu->cpu.eipc = value & 0xFFFFFFFE; - case VB_EIPSW: return emu->cpu.eipsw = value & 0x000FF3FF; - case VB_FEPC : return emu->cpu.fepc = value & 0xFFFFFFFE; - case VB_FEPSW: return emu->cpu.fepsw = value & 0x000FF3FF; + case VB_ADTRE: return sim->cpu.adtre = value & 0xFFFFFFFE; + case VB_EIPC : return sim->cpu.eipc = value & 0xFFFFFFFE; + case VB_EIPSW: return sim->cpu.eipsw = value & 0x000FF3FF; + case VB_FEPC : return sim->cpu.fepc = value & 0xFFFFFFFE; + case VB_FEPSW: return sim->cpu.fepsw = value & 0x000FF3FF; case VB_PIR : return 0x00005346; case VB_TKCW : return 0x000000E0; - case 29 : return emu->cpu.sr29 = value & 0x00000001; + case 29 : return sim->cpu.sr29 = value & 0x00000001; + case 30 : return 0x00000004; case 31 : if (!debug && (int32_t) value < 0) value = ~value + 1; - return emu->cpu.sr31 = value; + return sim->cpu.sr31 = value; case VB_CHCW : - emu->cpu.chcw.ice = value >> 1 & 1; + sim->cpu.chcw.ice = value >> 1 & 1; /* TODO: Perform dump/restore operations */ return value & 0x00000002; case VB_ECR : if (debug) { - emu->cpu.ecr.fecc = value >> 16; - emu->cpu.ecr.eicc = value; + sim->cpu.ecr.fecc = value >> 16; + sim->cpu.ecr.eicc = value; return value; } - return vbGetSystemRegister(emu, id); + return vbGetSystemRegister(sim, id); case VB_PSW : - emu->cpu.psw.i = value >> 16 & 15; - emu->cpu.psw.np = value >> 15 & 1; - emu->cpu.psw.ep = value >> 14 & 1; - emu->cpu.psw.ae = value >> 13 & 1; - emu->cpu.psw.id = value >> 12 & 1; - emu->cpu.psw.fro = value >> 9 & 1; - emu->cpu.psw.fiv = value >> 8 & 1; - emu->cpu.psw.fzd = value >> 7 & 1; - emu->cpu.psw.fov = value >> 6 & 1; - emu->cpu.psw.fud = value >> 5 & 1; - emu->cpu.psw.fpr = value >> 4 & 1; - emu->cpu.psw.cy = value >> 3 & 1; - emu->cpu.psw.ov = value >> 2 & 1; - emu->cpu.psw.s = value >> 1 & 1; - emu->cpu.psw.z = value & 1; + sim->cpu.psw.i = value >> 16 & 15; + sim->cpu.psw.np = value >> 15 & 1; + sim->cpu.psw.ep = value >> 14 & 1; + sim->cpu.psw.ae = value >> 13 & 1; + sim->cpu.psw.id = value >> 12 & 1; + sim->cpu.psw.fro = value >> 9 & 1; + sim->cpu.psw.fiv = value >> 8 & 1; + sim->cpu.psw.fzd = value >> 7 & 1; + sim->cpu.psw.fov = value >> 6 & 1; + sim->cpu.psw.fud = value >> 5 & 1; + sim->cpu.psw.fpr = value >> 4 & 1; + sim->cpu.psw.cy = value >> 3 & 1; + sim->cpu.psw.ov = value >> 2 & 1; + sim->cpu.psw.s = value >> 1 & 1; + sim->cpu.psw.z = value & 1; return value & 0x000FF3FF; } return 0; } /* Perform a right shift */ -static int cpuShiftRight(VB *emu, int32_t value, int bits, int arithmetic) { +static int cpuShiftRight(VB *sim, int32_t value, int bits, int arithmetic) { if (bits != 0) { - emu->cpu.psw.cy = (value >> (bits - 1)) & 1; + sim->cpu.psw.cy = (value >> (bits - 1)) & 1; value = value >> bits & (((uint32_t) 1 << (32 - bits)) - 1); if (arithmetic) value = SignExtend(value, 32 - bits); - } else emu->cpu.psw.cy = 0; - return cpuBitwise(emu, value); + } else sim->cpu.psw.cy = 0; + return cpuBitwise(sim, value); } /* Perform an output or store */ -static int cpuStore(VB *emu,VB_INSTRUCTION *inst,uint8_t type,uint32_t clocks){ +static int cpuStore(VB *sim,VB_INSTRUCTION *inst,uint8_t type,uint32_t clocks){ /* Initiate the write */ - if (emu->cpu.busWait == 0) { + if (sim->cpu.busWait == 0) { /* Read the data unit from the bus */ if (cpuWrite( - emu, - emu->cpu.program[inst->bits[0] & 0x1F] + + sim, + sim->cpu.program[inst->bits[0] & 0x1F] + SignExtend((int32_t) inst->bits[1], 16), type, - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] )) return 1; /* Update state */ - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Complete the write */ - emu->cpu.busWait = 0; - emu->cpu.clocks = clocks; + sim->cpu.busWait = 0; + sim->cpu.clocks = clocks; return 0; } /* Subtract two numbers and update the flags */ -static int32_t cpuSubtract(VB *emu, int32_t left, int32_t right) { +static int32_t cpuSubtract(VB *sim, int32_t left, int32_t right) { int32_t result = left - right; - emu->cpu.clocks = 1; - emu->cpu.psw.cy = (uint32_t) result > (uint32_t) left; - emu->cpu.psw.ov = ((left ^ right) & (left ^ result)) >> 31 & 1; - emu->cpu.psw.s = result < 0; - emu->cpu.psw.z = result == 0; + sim->cpu.clocks = 1; + sim->cpu.psw.cy = (uint32_t) result > (uint32_t) left; + sim->cpu.psw.ov = ((left ^ right) & (left ^ result)) >> 31 & 1; + sim->cpu.psw.s = result < 0; + sim->cpu.psw.z = result == 0; return result; } @@ -731,222 +730,222 @@ static int32_t cpuSubtract(VB *emu, int32_t left, int32_t right) { /************************ Instruction Implementations ************************/ /* Add Immediate */ -static void cpuADD_IMM(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; +static void cpuADD_IMM(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; int32_t right = inst->bits[0] & 0x1F; - *reg2 = cpuAdd(emu, *reg2, SignExtend(right, 5)); + *reg2 = cpuAdd(sim, *reg2, SignExtend(right, 5)); } /* Add Register */ -static void cpuADD_REG(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuAdd(emu, *reg2, emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuADD_REG(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuAdd(sim, *reg2, sim->cpu.program[inst->bits[0] & 0x1F]); } /* Add Floating Short */ -static void cpuADDF_S(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - double left = cpuFloatOperand(emu, *reg2); - double right = cpuFloatOperand(emu,emu->cpu.program[inst->bits[0]&0x1F]); +static void cpuADDF_S(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + double left = cpuFloatOperand(sim, *reg2); + double right = cpuFloatOperand(sim,sim->cpu.program[inst->bits[0]&0x1F]); int32_t result; /* Output bits */ /* Perform the operation */ - if (emu->cpu.causeCode == 0) - result = cpuFloatResult(emu, left + right); - if (emu->cpu.causeCode != 0) + if (sim->cpu.causeCode == 0) + result = cpuFloatResult(sim, left + right); + if (sim->cpu.causeCode != 0) return; /* Update state */ *reg2 = result; - emu->cpu.clocks = 28; + sim->cpu.clocks = 28; } /* Add Immediate */ -static void cpuADDI(VB *emu, VB_INSTRUCTION *inst) { +static void cpuADDI(VB *sim, VB_INSTRUCTION *inst) { int32_t right = inst->bits[1]; - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuAdd(emu, - emu->cpu.program[inst->bits[0] & 0x1F], SignExtend(right, 16)); + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuAdd(sim, + sim->cpu.program[inst->bits[0] & 0x1F], SignExtend(right, 16)); } /* And */ -static void cpuAND(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuBitwise(emu, *reg2 & emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuAND(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuBitwise(sim, *reg2 & sim->cpu.program[inst->bits[0] & 0x1F]); } /* And Bit String Upward */ -#define cpuANDBSU(emu, inst) cpuBitString(emu, inst) +#define cpuANDBSU(sim, inst) cpuBitString(sim, inst) /* And Immediate */ -static void cpuANDI(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuBitwise(emu, - emu->cpu.program[inst->bits[0] & 0x1F] & inst->bits[1]); +static void cpuANDI(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuBitwise(sim, + sim->cpu.program[inst->bits[0] & 0x1F] & inst->bits[1]); } /* And Not Bit String Upward */ -#define cpuANDNBSU(emu, inst) cpuBitString(emu, inst) +#define cpuANDNBSU(sim, inst) cpuBitString(sim, inst) /* Conditional Branch */ -static void cpuBCOND(VB *emu, VB_INSTRUCTION *inst) { +static void cpuBCOND(VB *sim, VB_INSTRUCTION *inst) { int32_t disp; /* Target address displacement */ /* Branch to the target address */ - if (cpuCondition(emu, inst->bits[0] >> 9 & 15)) { + if (cpuCondition(sim, inst->bits[0] >> 9 & 15)) { disp = inst->bits[0] & 0x1FF; - cpuJump(emu, inst, emu->cpu.pc + SignExtend(disp, 9)); + cpuJump(sim, inst, sim->cpu.pc + SignExtend(disp, 9)); } /* Do not branch */ - else emu->cpu.clocks = 1; + else sim->cpu.clocks = 1; } /* Compare And Exchahge Interlocked */ -static int cpuCAXI(VB *emu, VB_INSTRUCTION *inst) { +static int cpuCAXI(VB *sim, VB_INSTRUCTION *inst) { int32_t *reg2; /* Program register reg2 */ /* Read the lock word */ - if (emu->cpu.busWait == 0) { + if (sim->cpu.busWait == 0) { /* Compute the address of the lock word */ - inst->aux[0] = emu->cpu.program[inst->bits[0] & 0x1F] + + inst->aux[0] = sim->cpu.program[inst->bits[0] & 0x1F] + SignExtend((int32_t) inst->bits[1], 16); /* Read the data unit from the bus */ - if (cpuRead(emu, inst->aux[0], VB_S32)) + if (cpuRead(sim, inst->aux[0], VB_S32)) return 1; /* Update state */ - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Compare and exchange */ - if (emu->cpu.busWait == 1) { + if (sim->cpu.busWait == 1) { /* Process the lock word */ - reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - cpuSubtract(emu, *reg2, emu->cpu.access.value); - *reg2 = emu->cpu.access.value; + reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + cpuSubtract(sim, *reg2, sim->cpu.access.value); + *reg2 = sim->cpu.access.value; /* Store the exchange value to the bus */ - if (cpuWrite(emu, inst->aux[0], VB_S32, - emu->cpu.psw.z ? emu->cpu.program[30] : emu->cpu.access.value)) + if (cpuWrite(sim, inst->aux[0], VB_S32, + sim->cpu.psw.z ? sim->cpu.program[30] : sim->cpu.access.value)) return 1; /* Update state */ - emu->cpu.clocks = emu->cpu.access.clocks; - emu->cpu.busWait = 2; + sim->cpu.clocks = sim->cpu.access.clocks; + sim->cpu.busWait = 2; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Update state */ - emu->cpu.busWait = 0; - emu->cpu.clocks = 26; + sim->cpu.busWait = 0; + sim->cpu.clocks = 26; return 0; } /* Clear Interrupt Disable Flag */ -static void cpuCLI(VB *emu) { - emu->cpu.psw.id = 0; - emu->cpu.clocks = 12; +static void cpuCLI(VB *sim) { + sim->cpu.psw.id = 0; + sim->cpu.clocks = 12; } /* Compare Immediate */ -static void cpuCMP_IMM(VB *emu, VB_INSTRUCTION *inst) { +static void cpuCMP_IMM(VB *sim, VB_INSTRUCTION *inst) { int32_t right = inst->bits[0] & 0x1F; - cpuSubtract(emu, emu->cpu.program[inst->bits[0] >> 5 & 0x1F], + cpuSubtract(sim, sim->cpu.program[inst->bits[0] >> 5 & 0x1F], SignExtend(right, 5)); } /* Compare Register */ -static void cpuCMP_REG(VB *emu, VB_INSTRUCTION *inst) { - cpuSubtract(emu, emu->cpu.program[inst->bits[0] >> 5 & 0x1F], - emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuCMP_REG(VB *sim, VB_INSTRUCTION *inst) { + cpuSubtract(sim, sim->cpu.program[inst->bits[0] >> 5 & 0x1F], + sim->cpu.program[inst->bits[0] & 0x1F]); } /* Compare Floating Short */ -static void cpuCMPF_S(VB *emu, VB_INSTRUCTION *inst) { - double left =cpuFloatOperand(emu,emu->cpu.program[inst->bits[0]>>5&0x1F]); - double right=cpuFloatOperand(emu,emu->cpu.program[inst->bits[0] &0x1F]); +static void cpuCMPF_S(VB *sim, VB_INSTRUCTION *inst) { + double left =cpuFloatOperand(sim,sim->cpu.program[inst->bits[0]>>5&0x1F]); + double right=cpuFloatOperand(sim,sim->cpu.program[inst->bits[0] &0x1F]); /* Perform the operation */ - if (emu->cpu.causeCode == 0) - cpuFloatResult(emu, left - right); - if (emu->cpu.causeCode != 0) + if (sim->cpu.causeCode == 0) + cpuFloatResult(sim, left - right); + if (sim->cpu.causeCode != 0) return; /* Update state */ - emu->cpu.clocks = 10; + sim->cpu.clocks = 10; } /* Convert Short Floating to Word Integer */ -#define cpuCVT_SW(emu, inst) cpuFloatToWord(emu, inst, 0) +#define cpuCVT_SW(sim, inst) cpuFloatToWord(sim, inst, 0) /* Convert Word Integer to Short Floating */ -static void cpuCVT_WS(VB *emu, VB_INSTRUCTION *inst) { - int32_t value = emu->cpu.program[inst->bits[0] & 0x1F]; +static void cpuCVT_WS(VB *sim, VB_INSTRUCTION *inst) { + int32_t value = sim->cpu.program[inst->bits[0] & 0x1F]; float result = (float) value; - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = *(int32_t *) &result; + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = *(int32_t *) &result; if (result != value) - emu->cpu.psw.fpr = 1; - emu->cpu.psw.cy = result < 0; - emu->cpu.psw.ov = 0; - emu->cpu.psw.s = result < 0; - emu->cpu.psw.z = result == 0; - emu->cpu.clocks = 16; + sim->cpu.psw.fpr = 1; + sim->cpu.psw.cy = result < 0; + sim->cpu.psw.ov = 0; + sim->cpu.psw.s = result < 0; + sim->cpu.psw.z = result == 0; + sim->cpu.clocks = 16; } /* Divide */ -static void cpuDIV(VB *emu, VB_INSTRUCTION *inst) { - int32_t right = emu->cpu.program[inst->bits[0] & 0x1F]; +static void cpuDIV(VB *sim, VB_INSTRUCTION *inst) { + int32_t right = sim->cpu.program[inst->bits[0] & 0x1F]; int32_t *r30; /* Program register r30 */ int32_t *reg2; /* Program register reg2 */ /* Zero division */ if (right == 0) { - emu->cpu.causeCode = 0xFF80; + sim->cpu.causeCode = 0xFF80; return; } /* Special case */ - reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; + reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; if (*reg2 == INT32_MIN && right == -1) { - emu->cpu.program[30] = 0; - emu->cpu.psw.ov = 1; + sim->cpu.program[30] = 0; + sim->cpu.psw.ov = 1; } /* Perform the operation */ else { - r30 = &emu->cpu.program[30]; + r30 = &sim->cpu.program[30]; *r30 = *reg2 % right; if ((*r30 ^ *reg2) < 0) *r30 = -*r30; *reg2 /= right; - emu->cpu.psw.ov = 0; + sim->cpu.psw.ov = 0; } /* Update state */ - emu->cpu.psw.s = *reg2 < 0; - emu->cpu.psw.z = *reg2 == 0; - emu->cpu.clocks = 38; + sim->cpu.psw.s = *reg2 < 0; + sim->cpu.psw.z = *reg2 == 0; + sim->cpu.clocks = 38; } /* Divide Floating Short */ -static void cpuDIVF_S(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - double left = cpuFloatOperand(emu, *reg2); - double right = cpuFloatOperand(emu,emu->cpu.program[inst->bits[0]&0x1F]); +static void cpuDIVF_S(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + double left = cpuFloatOperand(sim, *reg2); + double right = cpuFloatOperand(sim,sim->cpu.program[inst->bits[0]&0x1F]); int32_t result; /* Output bits */ /* Reserved operand */ - if (emu->cpu.causeCode != 0) + if (sim->cpu.causeCode != 0) return; /* Zero division */ @@ -954,446 +953,429 @@ static void cpuDIVF_S(VB *emu, VB_INSTRUCTION *inst) { /* Invalid operation */ if (left == 0) { - emu->cpu.causeCode = 0xFF70; - emu->cpu.psw.fiv = 1; + sim->cpu.causeCode = 0xFF70; + sim->cpu.psw.fiv = 1; } /* Zero division */ else { - emu->cpu.causeCode = 0xFF68; - emu->cpu.psw.fzd = 1; + sim->cpu.causeCode = 0xFF68; + sim->cpu.psw.fzd = 1; } return; } /* Perform the operation */ - result = cpuFloatResult(emu, left / right); - if (emu->cpu.causeCode != 0) + result = cpuFloatResult(sim, left / right); + if (sim->cpu.causeCode != 0) return; /* Update state */ *reg2 = result; - emu->cpu.clocks = 44; + sim->cpu.clocks = 44; } /* Divide Unsigned */ -static void cpuDIVU(VB *emu, VB_INSTRUCTION *inst) { - uint32_t right = emu->cpu.program[inst->bits[0] & 0x1F]; +static void cpuDIVU(VB *sim, VB_INSTRUCTION *inst) { + uint32_t right = sim->cpu.program[inst->bits[0] & 0x1F]; uint32_t *reg2; /* Program register reg2 */ /* Zero division */ if (right == 0) { - emu->cpu.causeCode = 0xFF80; + sim->cpu.causeCode = 0xFF80; return; } /* Perform the operation */ - reg2 = (uint32_t *) &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - emu->cpu.program[30] = *reg2 % right; + reg2 = (uint32_t *) &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + sim->cpu.program[30] = *reg2 % right; *reg2 /= right; /* Update state */ - emu->cpu.psw.ov = 0; - emu->cpu.psw.s = *reg2 >> 31 & 1; - emu->cpu.psw.z = *reg2 == 0; - emu->cpu.clocks = 36; + sim->cpu.psw.ov = 0; + sim->cpu.psw.s = *reg2 >> 31 & 1; + sim->cpu.psw.z = *reg2 == 0; + sim->cpu.clocks = 36; } /* Halt */ -static void cpuHALT(VB *emu) { - emu->cpu.state = CPU_HALTED; - /* emu->cpu.clocks = ? */ +static void cpuHALT(VB *sim) { + sim->cpu.state = CPU_HALTED; + /* sim->cpu.clocks = ? */ } /* Input Byte */ -#define cpuIN_B(emu, inst) cpuLoad(emu, inst, VB_U8, 5) +#define cpuIN_B(sim, inst) cpuLoad(sim, inst, VB_U8, 5) /* Input Halfword */ -#define cpuIN_H(emu, inst) cpuLoad(emu, inst, VB_U16, 5) +#define cpuIN_H(sim, inst) cpuLoad(sim, inst, VB_U16, 5) /* Input Word */ -#define cpuIN_W(emu, inst) cpuLoad(emu, inst, VB_S32, 5) +#define cpuIN_W(sim, inst) cpuLoad(sim, inst, VB_S32, 5) /* Jump and Link */ -static void cpuJAL(VB *emu, VB_INSTRUCTION *inst) { +static void cpuJAL(VB *sim, VB_INSTRUCTION *inst) { int32_t disp = ((int32_t) inst->bits[0]<<16 | inst->bits[1]) & 0x03FFFFFF; - emu->cpu.program[31] = emu->cpu.pc + 4; - cpuJump(emu, inst, emu->cpu.pc + SignExtend(disp, 26)); + sim->cpu.program[31] = sim->cpu.pc + 4; + cpuJump(sim, inst, sim->cpu.pc + SignExtend(disp, 26)); } /* Jump Register */ -#define cpuJMP(emu,inst) cpuJump(emu,inst,emu->cpu.program[inst->bits[0]&0x1F]) +#define cpuJMP(sim,inst) cpuJump(sim,inst,sim->cpu.program[inst->bits[0]&0x1F]) /* Jump Relative */ -static void cpuJR(VB *emu, VB_INSTRUCTION *inst) { +static void cpuJR(VB *sim, VB_INSTRUCTION *inst) { int32_t disp = ((int32_t) inst->bits[0]<<16 | inst->bits[1]) & 0x03FFFFFF; - cpuJump(emu, inst, emu->cpu.pc + SignExtend(disp, 26)); + cpuJump(sim, inst, sim->cpu.pc + SignExtend(disp, 26)); } /* Load Byte */ -#define cpuLD_B(emu, inst) cpuLoad(emu, inst, VB_S8, 5) +#define cpuLD_B(sim, inst) cpuLoad(sim, inst, VB_S8, 5) /* Load Halfword */ -#define cpuLD_H(emu, inst) cpuLoad(emu, inst, VB_S16, 5) +#define cpuLD_H(sim, inst) cpuLoad(sim, inst, VB_S16, 5) /* Load Word */ -#define cpuLD_W(emu, inst) cpuLoad(emu, inst, VB_S32, 5) +#define cpuLD_W(sim, inst) cpuLoad(sim, inst, VB_S32, 5) /* Load to System Register */ -static void cpuLDSR(VB *emu, VB_INSTRUCTION *inst) { - cpuSetSystemRegister(emu, inst->bits[0] & 0x1F, - emu->cpu.program[inst->bits[0] >> 5 & 0x1F], 0); - emu->cpu.clocks = 8; +static void cpuLDSR(VB *sim, VB_INSTRUCTION *inst) { + cpuSetSystemRegister(sim, inst->bits[0] & 0x1F, + sim->cpu.program[inst->bits[0] >> 5 & 0x1F], 0); + sim->cpu.clocks = 8; } /* Move Immediate */ -static void cpuMOV_IMM(VB *emu, VB_INSTRUCTION *inst) { +static void cpuMOV_IMM(VB *sim, VB_INSTRUCTION *inst) { int32_t value = inst->bits[0] & 0x1F; - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = SignExtend(value, 5); - emu->cpu.clocks = 1; + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = SignExtend(value, 5); + sim->cpu.clocks = 1; } /* Move Register */ -static void cpuMOV_REG(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = - emu->cpu.program[inst->bits[0] & 0x1F]; - emu->cpu.clocks = 1; +static void cpuMOV_REG(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = + sim->cpu.program[inst->bits[0] & 0x1F]; + sim->cpu.clocks = 1; } /* Move Bit String Upward */ -#define cpuMOVBSU(emu, inst) cpuBitString(emu, inst) +#define cpuMOVBSU(sim, inst) cpuBitString(sim, inst) /* Add */ -static void cpuMOVEA(VB *emu, VB_INSTRUCTION *inst) { +static void cpuMOVEA(VB *sim, VB_INSTRUCTION *inst) { int32_t right = inst->bits[1]; - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = - emu->cpu.program[inst->bits[0] & 0x1F] + SignExtend(right, 16); - emu->cpu.clocks = 1; + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = + sim->cpu.program[inst->bits[0] & 0x1F] + SignExtend(right, 16); + sim->cpu.clocks = 1; } /* Add */ -static void cpuMOVHI(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = - emu->cpu.program[inst->bits[0] & 0x1F] + ((int32_t)inst->bits[1]<<16); - emu->cpu.clocks = 1; +static void cpuMOVHI(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = + sim->cpu.program[inst->bits[0] & 0x1F] + ((int32_t)inst->bits[1]<<16); + sim->cpu.clocks = 1; } /* Multiply Halfword */ -static void cpuMPYHW(VB *emu, VB_INSTRUCTION *inst) { - int32_t right = emu->cpu.program[inst->bits[0] & 0x1F] & 0x0001FFFF; - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] *= SignExtend(right, 17); - emu->cpu.clocks = 9; +static void cpuMPYHW(VB *sim, VB_INSTRUCTION *inst) { + int32_t right = sim->cpu.program[inst->bits[0] & 0x1F] & 0x0001FFFF; + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] *= SignExtend(right, 17); + sim->cpu.clocks = 9; } /* Multiply */ -static void cpuMUL(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; +static void cpuMUL(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; int64_t result = (int64_t) *reg2 * - emu->cpu.program[inst->bits[0] & 0x1F]; - emu->cpu.program[30] = result >> 32; + sim->cpu.program[inst->bits[0] & 0x1F]; + sim->cpu.program[30] = result >> 32; *reg2 = result; - emu->cpu.psw.ov = *reg2 != result; - emu->cpu.psw.s = *reg2 < 0; - emu->cpu.psw.z = *reg2 == 0; - emu->cpu.clocks = 13; + sim->cpu.psw.ov = *reg2 != result; + sim->cpu.psw.s = *reg2 < 0; + sim->cpu.psw.z = *reg2 == 0; + sim->cpu.clocks = 13; } /* Multiply Floating Short */ -static void cpuMULF_S(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - double left = cpuFloatOperand(emu, *reg2); - double right = cpuFloatOperand(emu,emu->cpu.program[inst->bits[0]&0x1F]); +static void cpuMULF_S(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + double left = cpuFloatOperand(sim, *reg2); + double right = cpuFloatOperand(sim,sim->cpu.program[inst->bits[0]&0x1F]); int32_t result; /* Output bits */ /* Perform the operation */ - if (emu->cpu.causeCode == 0) - result = cpuFloatResult(emu, left * right); - if (emu->cpu.causeCode != 0) + if (sim->cpu.causeCode == 0) + result = cpuFloatResult(sim, left * right); + if (sim->cpu.causeCode != 0) return; /* Update state */ *reg2 = result; - emu->cpu.clocks = 30; + sim->cpu.clocks = 30; } /* Multiply Unsigned */ -static void cpuMULU(VB *emu, VB_INSTRUCTION *inst) { - uint32_t *reg2 = (uint32_t *) &emu->cpu.program[inst->bits[0]>>5&0x1F]; +static void cpuMULU(VB *sim, VB_INSTRUCTION *inst) { + uint32_t *reg2 = (uint32_t *) &sim->cpu.program[inst->bits[0]>>5&0x1F]; uint64_t result = (uint64_t) *reg2 * - (uint32_t) emu->cpu.program[inst->bits[0] & 0x1F]; - emu->cpu.program[30] = result >> 32; + (uint32_t) sim->cpu.program[inst->bits[0] & 0x1F]; + sim->cpu.program[30] = result >> 32; *reg2 = result; - emu->cpu.psw.ov = *reg2 != result; - emu->cpu.psw.s = *reg2 >> 31 & 1; - emu->cpu.psw.z = *reg2 == 0; - emu->cpu.clocks = 13; + sim->cpu.psw.ov = *reg2 != result; + sim->cpu.psw.s = *reg2 >> 31 & 1; + sim->cpu.psw.z = *reg2 == 0; + sim->cpu.clocks = 13; } /* Not */ -static void cpuNOT(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = - cpuBitwise(emu, ~emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuNOT(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = + cpuBitwise(sim, ~sim->cpu.program[inst->bits[0] & 0x1F]); } /* Not Bit String Upward */ -#define cpuNOTBSU(emu, inst) cpuBitString(emu, inst) +#define cpuNOTBSU(sim, inst) cpuBitString(sim, inst) /* Or */ -static void cpuOR(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuBitwise(emu, *reg2 | emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuOR(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuBitwise(sim, *reg2 | sim->cpu.program[inst->bits[0] & 0x1F]); } /* Or Bit String Upward */ -#define cpuORBSU(emu, inst) cpuBitString(emu, inst) +#define cpuORBSU(sim, inst) cpuBitString(sim, inst) /* Or Immediate */ -static void cpuORI(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuBitwise(emu, - emu->cpu.program[inst->bits[0] & 0x1F] | inst->bits[1]); +static void cpuORI(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuBitwise(sim, + sim->cpu.program[inst->bits[0] & 0x1F] | inst->bits[1]); } /* Or Not Bit String Upward */ -#define cpuORNBSU(emu, inst) cpuBitString(emu, inst) +#define cpuORNBSU(sim, inst) cpuBitString(sim, inst) /* Output Byte */ -#define cpuOUT_B(emu, inst) cpuStore(emu, inst, VB_U8, 4) +#define cpuOUT_B(sim, inst) cpuStore(sim, inst, VB_U8, 4) /* Output Halfword */ -#define cpuOUT_H(emu, inst) cpuStore(emu, inst, VB_U16, 4) +#define cpuOUT_H(sim, inst) cpuStore(sim, inst, VB_U16, 4) /* Output Word */ -#define cpuOUT_W(emu, inst) cpuStore(emu, inst, VB_S32, 4) +#define cpuOUT_W(sim, inst) cpuStore(sim, inst, VB_S32, 4) /* Return from Trap or Interrupt */ -static void cpuRETI(VB *emu, VB_INSTRUCTION *inst) { +static void cpuRETI(VB *sim, VB_INSTRUCTION *inst) { /* Duplexed exception */ - if (emu->cpu.psw.np) { - emu->cpu.pc = emu->cpu.fepc; - emu->cpu.pcFrom = emu->cpu.fepcFrom; - emu->cpu.pcTo = emu->cpu.fepcTo; - cpuSetSystemRegister(emu, VB_PSW, emu->cpu.fepsw, 0); + if (sim->cpu.psw.np) { + sim->cpu.pc = sim->cpu.fepc; + cpuSetSystemRegister(sim, VB_PSW, sim->cpu.fepsw, 0); } /* Non-duplexed exception */ else { - - /* Exception */ - if (emu->cpu.psw.ep) { - emu->cpu.pc = emu->cpu.eipc; - emu->cpu.pcFrom = emu->cpu.eipcFrom; - emu->cpu.pcTo = emu->cpu.eipcTo; - } - - /* No exception */ - else { - emu->cpu.pcFrom = emu->cpu.pc; - emu->cpu.pc = emu->cpu.eipc; - emu->cpu.pcTo = emu->cpu.pc; - } - - /* Update state */ - cpuSetSystemRegister(emu, VB_PSW, emu->cpu.eipsw, 0); + sim->cpu.pc = sim->cpu.eipc; + cpuSetSystemRegister(sim, VB_PSW, sim->cpu.eipsw, 0); } /* Update state */ - emu->cpu.clocks = 10; + sim->cpu.clocks = 10; inst->size = 0; } /* Reverse Bits in Word */ -static void cpuREV(VB *emu, VB_INSTRUCTION *inst) { - int32_t value = emu->cpu.program[inst->bits[0] & 0x1F]; +static void cpuREV(VB *sim, VB_INSTRUCTION *inst) { + int32_t value = sim->cpu.program[inst->bits[0] & 0x1F]; value = (value >> 16 & 0x0000FFFF) | (value << 16 & (int32_t) 0xFFFF0000); value = (value >> 8 & 0x00FF00FF) | (value << 8 & (int32_t) 0xFF00FF00); value = (value >> 4 & 0x0F0F0F0F) | (value << 4 & (int32_t) 0xF0F0F0F0); value = (value >> 2 & 0x33333333) | (value << 2 & (int32_t) 0xCCCCCCCC); - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = (value >> 1 & 0x55555555) | (value << 1 & (int32_t) 0xAAAAAAAA); - emu->cpu.clocks = 22; + sim->cpu.clocks = 22; } /* Shift Arithmetic Right by Immediate */ -static void cpuSAR_IMM(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuShiftRight(emu, *reg2, inst->bits[0] & 0x1F, 1); +static void cpuSAR_IMM(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuShiftRight(sim, *reg2, inst->bits[0] & 0x1F, 1); } /* Shift Arithmetic Right by Register */ -static void cpuSAR_REG(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuShiftRight(emu, *reg2, - emu->cpu.program[inst->bits[0] & 0x1F] & 0x1F, 1); +static void cpuSAR_REG(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuShiftRight(sim, *reg2, + sim->cpu.program[inst->bits[0] & 0x1F] & 0x1F, 1); } /* Search Bit 0 Downward */ -#define cpuSCH0BSD(emu) cpuBitSearch(emu, 0, -1) +#define cpuSCH0BSD(sim) cpuBitSearch(sim, 0, -1) /* Search Bit 0 Upward */ -#define cpuSCH0BSU(emu) cpuBitSearch(emu, 0, 1) +#define cpuSCH0BSU(sim) cpuBitSearch(sim, 0, 1) /* Search Bit 1 Downward */ -#define cpuSCH1BSD(emu) cpuBitSearch(emu, 1, -1) +#define cpuSCH1BSD(sim) cpuBitSearch(sim, 1, -1) /* Search Bit 1 Upward */ -#define cpuSCH1BSU(emu) cpuBitSearch(emu, 1, 1) +#define cpuSCH1BSU(sim) cpuBitSearch(sim, 1, 1) /* Set Interrupt Disable Flag */ -static void cpuSEI(VB *emu) { - emu->cpu.psw.id = 1; - emu->cpu.clocks = 12; +static void cpuSEI(VB *sim) { + sim->cpu.psw.id = 1; + sim->cpu.clocks = 12; } /* Set Flag Condition */ -static void cpuSETF(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = - cpuCondition(emu, inst->bits[0] & 15); - emu->cpu.clocks = 1; +static void cpuSETF(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = + cpuCondition(sim, inst->bits[0] & 15); + sim->cpu.clocks = 1; } /* Shift Logical Left by Immediate */ -static void cpuSHL_IMM(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; +static void cpuSHL_IMM(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; int32_t bits = inst->bits[0] & 0x1F; - emu->cpu.psw.cy = bits == 0 ? 0 : *reg2 >> (32 - bits) & 1; - *reg2 = cpuBitwise(emu, *reg2 << bits); + sim->cpu.psw.cy = bits == 0 ? 0 : *reg2 >> (32 - bits) & 1; + *reg2 = cpuBitwise(sim, *reg2 << bits); } /* Shift Logical Left by Register */ -static void cpuSHL_REG(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - int32_t bits = emu->cpu.program[inst->bits[0] & 0x1F] & 0x1F; - emu->cpu.psw.cy = bits == 0 ? 0 : *reg2 >> (32 - bits) & 1; - *reg2 = cpuBitwise(emu, *reg2 << bits); +static void cpuSHL_REG(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + int32_t bits = sim->cpu.program[inst->bits[0] & 0x1F] & 0x1F; + sim->cpu.psw.cy = bits == 0 ? 0 : *reg2 >> (32 - bits) & 1; + *reg2 = cpuBitwise(sim, *reg2 << bits); } /* Shift Logical Right by Immediate */ -static void cpuSHR_IMM(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuShiftRight(emu, *reg2, inst->bits[0] & 0x1F, 0); +static void cpuSHR_IMM(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuShiftRight(sim, *reg2, inst->bits[0] & 0x1F, 0); } /* Shift Logical Right by Register */ -static void cpuSHR_REG(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuShiftRight(emu, *reg2, - emu->cpu.program[inst->bits[0] & 0x1F] & 0x1F, 0); +static void cpuSHR_REG(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuShiftRight(sim, *reg2, + sim->cpu.program[inst->bits[0] & 0x1F] & 0x1F, 0); } /* Store Byte */ -#define cpuST_B(emu, inst) cpuStore(emu, inst, VB_S8, 4) +#define cpuST_B(sim, inst) cpuStore(sim, inst, VB_S8, 4) /* Store Halfword */ -#define cpuST_H(emu, inst) cpuStore(emu, inst, VB_S16, 4) +#define cpuST_H(sim, inst) cpuStore(sim, inst, VB_S16, 4) /* Store Word */ -#define cpuST_W(emu, inst) cpuStore(emu, inst, VB_S32, 4) +#define cpuST_W(sim, inst) cpuStore(sim, inst, VB_S32, 4) /* Store Contents of System Register */ -static void cpuSTSR(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = - vbGetSystemRegister(emu, inst->bits[0] & 0x1F); - emu->cpu.clocks = 8; +static void cpuSTSR(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = + vbGetSystemRegister(sim, inst->bits[0] & 0x1F); + sim->cpu.clocks = 8; } /* Subtract */ -static void cpuSUB(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuSubtract(emu, *reg2, emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuSUB(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuSubtract(sim, *reg2, sim->cpu.program[inst->bits[0] & 0x1F]); } /* Subtract Floating Short */ -static void cpuSUBF_S(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - double left = cpuFloatOperand(emu, *reg2); - double right = cpuFloatOperand(emu,emu->cpu.program[inst->bits[0]&0x1F]); +static void cpuSUBF_S(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + double left = cpuFloatOperand(sim, *reg2); + double right = cpuFloatOperand(sim,sim->cpu.program[inst->bits[0]&0x1F]); int32_t result; /* Output bits */ /* Perform the operation */ - if (emu->cpu.causeCode == 0) - result = cpuFloatResult(emu, left - right); - if (emu->cpu.causeCode != 0) + if (sim->cpu.causeCode == 0) + result = cpuFloatResult(sim, left - right); + if (sim->cpu.causeCode != 0) return; /* Update state */ *reg2 = result; - emu->cpu.clocks = 28; + sim->cpu.clocks = 28; } /* Trap */ -static void cpuTRAP(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.causeCode = 0xFFA0 + (inst->bits[0] & 0x1F); - emu->cpu.clocks = 15; - emu->cpu.pc += 2; +static void cpuTRAP(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.causeCode = 0xFFA0 + (inst->bits[0] & 0x1F); + sim->cpu.clocks = 15; + sim->cpu.pc += 2; } /* Truncate Short Floating to Word Integer */ -#define cpuTRNC_SW(emu, inst) cpuFloatToWord(emu, inst, 1) +#define cpuTRNC_SW(sim, inst) cpuFloatToWord(sim, inst, 1) /* Exchange Byte */ -static void cpuXB(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; +static void cpuXB(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; *reg2 = (*reg2 >> 8 & (int32_t) 0x000000FF) | (*reg2 << 8 & (int32_t) 0x0000FF00) | (*reg2 & (int32_t) 0xFFFF0000) ; - emu->cpu.clocks = 6; + sim->cpu.clocks = 6; } /* Exchange Halfword */ -static void cpuXH(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; +static void cpuXH(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; *reg2 = (*reg2 >> 16 & 0x0000FFFF) | *reg2 << 16; - emu->cpu.clocks = 1; + sim->cpu.clocks = 1; } /* Exclusive Or */ -static void cpuXOR(VB *emu, VB_INSTRUCTION *inst) { - int32_t *reg2 = &emu->cpu.program[inst->bits[0] >> 5 & 0x1F]; - *reg2 = cpuBitwise(emu, *reg2 ^ emu->cpu.program[inst->bits[0] & 0x1F]); +static void cpuXOR(VB *sim, VB_INSTRUCTION *inst) { + int32_t *reg2 = &sim->cpu.program[inst->bits[0] >> 5 & 0x1F]; + *reg2 = cpuBitwise(sim, *reg2 ^ sim->cpu.program[inst->bits[0] & 0x1F]); } /* Exclusive Or Bit String Upward */ -#define cpuXORBSU(emu, inst) cpuBitString(emu, inst) +#define cpuXORBSU(sim, inst) cpuBitString(sim, inst) /* Exclusive Or Immediate */ -static void cpuXORI(VB *emu, VB_INSTRUCTION *inst) { - emu->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuBitwise(emu, - emu->cpu.program[inst->bits[0] & 0x1F] ^ inst->bits[1]); +static void cpuXORI(VB *sim, VB_INSTRUCTION *inst) { + sim->cpu.program[inst->bits[0] >> 5 & 0x1F] = cpuBitwise(sim, + sim->cpu.program[inst->bits[0] & 0x1F] ^ inst->bits[1]); } /* Exclusive Or Not Bit String Upward */ -#define cpuXORNBSU(emu, inst) cpuBitString(emu, inst) +#define cpuXORNBSU(sim, inst) cpuBitString(sim, inst) /***************************** Module Functions ******************************/ /* Check for interrupts */ -static int cpuCheckIRQs(VB *emu) { +static int cpuCheckIRQs(VB *sim) { int x; /* Iterator */ /* Interrupts are masked */ - if (emu->cpu.psw.np || emu->cpu.psw.ep || emu->cpu.psw.id) + if (sim->cpu.psw.np || sim->cpu.psw.ep || sim->cpu.psw.id) return 0; /* Check for interrupts */ - for (x = 4; x >= emu->cpu.psw.i; x--) { + for (x = 4; x >= sim->cpu.psw.i; x--) { /* The interrupt request line is low */ - if (!emu->cpu.irq[x]) + if (!sim->cpu.irq[x]) continue; /* Cause a pending HALT instruction to complete */ - if (emu->cpu.state == CPU_HALTED) - emu->cpu.pc += 2; + if (sim->cpu.state == CPU_HALTED) + sim->cpu.pc += 2; /* Trigger an interrupt */ - emu->cpu.causeCode = 0xFE00 | x << 4; - emu->cpu.state = CPU_EXCEPTION; + sim->cpu.causeCode = 0xFE00 | x << 4; + sim->cpu.state = CPU_EXCEPTION; return 1; } @@ -1402,276 +1384,270 @@ static int cpuCheckIRQs(VB *emu) { } /* Perform instruction execute operations */ -static int cpuExecute(VB *emu) { +static int cpuExecute(VB *sim) { int broke; /* Application break occurred */ VB_INSTRUCTION *inst; /* Shorthand reference */ /* Check for address trap */ - if (emu->cpu.psw.ae && emu->cpu.adtre == emu->cpu.pc) { - emu->cpu.causeCode = 0xFFC0; - emu->cpu.state = CPU_EXCEPTION; + if (sim->cpu.psw.ae && sim->cpu.adtre == sim->cpu.pc) { + sim->cpu.causeCode = 0xFFC0; + sim->cpu.state = CPU_EXCEPTION; return 0; } /* Prepare state */ - emu->cpu.causeCode = 0; - inst = &emu->cpu.inst; + sim->cpu.causeCode = 0; + inst = &sim->cpu.inst; /* Check for application break */ - if (emu->onExecute != NULL && emu->onExecute(emu, inst)) + if (sim->onExecute != NULL && sim->onExecute(sim, inst)) return 1; /* Processing by ID */ broke = 0; switch (inst->id) { - case CPU_ADD_IMM: cpuADD_IMM(emu, inst); break; - case CPU_ADD_REG: cpuADD_REG(emu, inst); break; - case CPU_ADDF_S : cpuADDF_S (emu, inst); break; - case CPU_ADDI : cpuADDI (emu, inst); break; - case CPU_AND : cpuAND (emu, inst); break; - case CPU_ANDBSU : broke = cpuANDBSU (emu, inst); break; - case CPU_ANDI : cpuANDI (emu, inst); break; - case CPU_ANDNBSU: broke = cpuANDNBSU(emu, inst); break; - case CPU_BCOND : cpuBCOND (emu, inst); break; - case CPU_CAXI : broke = cpuCAXI (emu, inst); break; - case CPU_CLI : cpuCLI (emu ); break; - case CPU_CMP_IMM: cpuCMP_IMM(emu, inst); break; - case CPU_CMP_REG: cpuCMP_REG(emu, inst); break; - case CPU_CMPF_S : cpuCMPF_S (emu, inst); break; - case CPU_CVT_SW : cpuCVT_SW (emu, inst); break; - case CPU_CVT_WS : cpuCVT_WS (emu, inst); break; - case CPU_DIV : cpuDIV (emu, inst); break; - case CPU_DIVF_S : cpuDIVF_S (emu, inst); break; - case CPU_DIVU : cpuDIVU (emu, inst); break; - case CPU_HALT : cpuHALT (emu ); break; - case CPU_IN_B : broke = cpuIN_B (emu, inst); break; - case CPU_IN_H : broke = cpuIN_H (emu, inst); break; - case CPU_IN_W : broke = cpuIN_W (emu, inst); break; - case CPU_JAL : cpuJAL (emu, inst); break; - case CPU_JMP : cpuJMP (emu, inst); break; - case CPU_JR : cpuJR (emu, inst); break; - case CPU_LD_B : broke = cpuLD_B (emu, inst); break; - case CPU_LD_H : broke = cpuLD_H (emu, inst); break; - case CPU_LD_W : broke = cpuLD_W (emu, inst); break; - case CPU_LDSR : cpuLDSR (emu, inst); break; - case CPU_MOV_IMM: cpuMOV_IMM(emu, inst); break; - case CPU_MOV_REG: cpuMOV_REG(emu, inst); break; - case CPU_MOVBSU : broke = cpuMOVBSU (emu, inst); break; - case CPU_MOVEA : cpuMOVEA (emu, inst); break; - case CPU_MOVHI : cpuMOVHI (emu, inst); break; - case CPU_MPYHW : cpuMPYHW (emu, inst); break; - case CPU_MUL : cpuMUL (emu, inst); break; - case CPU_MULF_S : cpuMULF_S (emu, inst); break; - case CPU_MULU : cpuMULU (emu, inst); break; - case CPU_NOT : cpuNOT (emu, inst); break; - case CPU_NOTBSU : broke = cpuNOTBSU (emu, inst); break; - case CPU_OR : cpuOR (emu, inst); break; - case CPU_ORBSU : broke = cpuORBSU (emu, inst); break; - case CPU_ORI : cpuORI (emu, inst); break; - case CPU_ORNBSU : broke = cpuORNBSU (emu, inst); break; - case CPU_OUT_B : broke = cpuOUT_B (emu, inst); break; - case CPU_OUT_H : broke = cpuOUT_H (emu, inst); break; - case CPU_OUT_W : broke = cpuOUT_W (emu, inst); break; - case CPU_RETI : cpuRETI (emu, inst); break; - case CPU_REV : cpuREV (emu, inst); break; - case CPU_SAR_IMM: cpuSAR_IMM(emu, inst); break; - case CPU_SAR_REG: cpuSAR_REG(emu, inst); break; - case CPU_SCH0BSD: cpuSCH0BSD(emu ); break; - case CPU_SCH0BSU: cpuSCH0BSU(emu ); break; - case CPU_SCH1BSD: cpuSCH1BSD(emu ); break; - case CPU_SCH1BSU: cpuSCH1BSU(emu ); break; - case CPU_SEI : cpuSEI (emu ); break; - case CPU_SETF : cpuSETF (emu, inst); break; - case CPU_SHL_IMM: cpuSHL_IMM(emu, inst); break; - case CPU_SHL_REG: cpuSHL_REG(emu, inst); break; - case CPU_SHR_IMM: cpuSHR_IMM(emu, inst); break; - case CPU_SHR_REG: cpuSHR_REG(emu, inst); break; - case CPU_ST_B : broke = cpuST_B (emu, inst); break; - case CPU_ST_H : broke = cpuST_H (emu, inst); break; - case CPU_ST_W : broke = cpuST_W (emu, inst); break; - case CPU_STSR : cpuSTSR (emu, inst); break; - case CPU_SUB : cpuSUB (emu, inst); break; - case CPU_SUBF_S : cpuSUBF_S (emu, inst); break; - case CPU_TRAP : cpuTRAP (emu, inst); break; - case CPU_TRNC_SW: cpuTRNC_SW(emu, inst); break; - case CPU_XB : cpuXB (emu, inst); break; - case CPU_XH : cpuXH (emu, inst); break; - case CPU_XOR : cpuXOR (emu, inst); break; - case CPU_XORBSU : broke = cpuXORBSU (emu, inst); break; - case CPU_XORI : cpuXORI (emu, inst); break; - case CPU_XORNBSU: broke = cpuXORNBSU(emu, inst); break; - default: /* CPU_ILLEGAL */ emu->cpu.causeCode = 0xFF90; + case CPU_ADD_IMM: cpuADD_IMM(sim, inst); break; + case CPU_ADD_REG: cpuADD_REG(sim, inst); break; + case CPU_ADDF_S : cpuADDF_S (sim, inst); break; + case CPU_ADDI : cpuADDI (sim, inst); break; + case CPU_AND : cpuAND (sim, inst); break; + case CPU_ANDBSU : broke = cpuANDBSU (sim, inst); break; + case CPU_ANDI : cpuANDI (sim, inst); break; + case CPU_ANDNBSU: broke = cpuANDNBSU(sim, inst); break; + case CPU_BCOND : cpuBCOND (sim, inst); break; + case CPU_CAXI : broke = cpuCAXI (sim, inst); break; + case CPU_CLI : cpuCLI (sim ); break; + case CPU_CMP_IMM: cpuCMP_IMM(sim, inst); break; + case CPU_CMP_REG: cpuCMP_REG(sim, inst); break; + case CPU_CMPF_S : cpuCMPF_S (sim, inst); break; + case CPU_CVT_SW : cpuCVT_SW (sim, inst); break; + case CPU_CVT_WS : cpuCVT_WS (sim, inst); break; + case CPU_DIV : cpuDIV (sim, inst); break; + case CPU_DIVF_S : cpuDIVF_S (sim, inst); break; + case CPU_DIVU : cpuDIVU (sim, inst); break; + case CPU_HALT : cpuHALT (sim ); break; + case CPU_IN_B : broke = cpuIN_B (sim, inst); break; + case CPU_IN_H : broke = cpuIN_H (sim, inst); break; + case CPU_IN_W : broke = cpuIN_W (sim, inst); break; + case CPU_JAL : cpuJAL (sim, inst); break; + case CPU_JMP : cpuJMP (sim, inst); break; + case CPU_JR : cpuJR (sim, inst); break; + case CPU_LD_B : broke = cpuLD_B (sim, inst); break; + case CPU_LD_H : broke = cpuLD_H (sim, inst); break; + case CPU_LD_W : broke = cpuLD_W (sim, inst); break; + case CPU_LDSR : cpuLDSR (sim, inst); break; + case CPU_MOV_IMM: cpuMOV_IMM(sim, inst); break; + case CPU_MOV_REG: cpuMOV_REG(sim, inst); break; + case CPU_MOVBSU : broke = cpuMOVBSU (sim, inst); break; + case CPU_MOVEA : cpuMOVEA (sim, inst); break; + case CPU_MOVHI : cpuMOVHI (sim, inst); break; + case CPU_MPYHW : cpuMPYHW (sim, inst); break; + case CPU_MUL : cpuMUL (sim, inst); break; + case CPU_MULF_S : cpuMULF_S (sim, inst); break; + case CPU_MULU : cpuMULU (sim, inst); break; + case CPU_NOT : cpuNOT (sim, inst); break; + case CPU_NOTBSU : broke = cpuNOTBSU (sim, inst); break; + case CPU_OR : cpuOR (sim, inst); break; + case CPU_ORBSU : broke = cpuORBSU (sim, inst); break; + case CPU_ORI : cpuORI (sim, inst); break; + case CPU_ORNBSU : broke = cpuORNBSU (sim, inst); break; + case CPU_OUT_B : broke = cpuOUT_B (sim, inst); break; + case CPU_OUT_H : broke = cpuOUT_H (sim, inst); break; + case CPU_OUT_W : broke = cpuOUT_W (sim, inst); break; + case CPU_RETI : cpuRETI (sim, inst); break; + case CPU_REV : cpuREV (sim, inst); break; + case CPU_SAR_IMM: cpuSAR_IMM(sim, inst); break; + case CPU_SAR_REG: cpuSAR_REG(sim, inst); break; + case CPU_SCH0BSD: cpuSCH0BSD(sim ); break; + case CPU_SCH0BSU: cpuSCH0BSU(sim ); break; + case CPU_SCH1BSD: cpuSCH1BSD(sim ); break; + case CPU_SCH1BSU: cpuSCH1BSU(sim ); break; + case CPU_SEI : cpuSEI (sim ); break; + case CPU_SETF : cpuSETF (sim, inst); break; + case CPU_SHL_IMM: cpuSHL_IMM(sim, inst); break; + case CPU_SHL_REG: cpuSHL_REG(sim, inst); break; + case CPU_SHR_IMM: cpuSHR_IMM(sim, inst); break; + case CPU_SHR_REG: cpuSHR_REG(sim, inst); break; + case CPU_ST_B : broke = cpuST_B (sim, inst); break; + case CPU_ST_H : broke = cpuST_H (sim, inst); break; + case CPU_ST_W : broke = cpuST_W (sim, inst); break; + case CPU_STSR : cpuSTSR (sim, inst); break; + case CPU_SUB : cpuSUB (sim, inst); break; + case CPU_SUBF_S : cpuSUBF_S (sim, inst); break; + case CPU_TRAP : cpuTRAP (sim, inst); break; + case CPU_TRNC_SW: cpuTRNC_SW(sim, inst); break; + case CPU_XB : cpuXB (sim, inst); break; + case CPU_XH : cpuXH (sim, inst); break; + case CPU_XOR : cpuXOR (sim, inst); break; + case CPU_XORBSU : broke = cpuXORBSU (sim, inst); break; + case CPU_XORI : cpuXORI (sim, inst); break; + case CPU_XORNBSU: broke = cpuXORNBSU(sim, inst); break; + default: /* CPU_ILLEGAL */ sim->cpu.causeCode = 0xFF90; } /* Instructions cannot modify r0 */ - emu->cpu.program[0] = 0x00000000; + sim->cpu.program[0] = 0x00000000; /* An application break was requested */ if (broke) return 1; /* Post-instruction tasks */ - if (emu->cpu.causeCode == 0 && emu->cpu.busWait == 0) { + if (sim->cpu.causeCode == 0 && sim->cpu.busWait == 0) { /* Advance to next instruction */ - if (emu->cpu.state != CPU_HALTED && !emu->cpu.substring) - emu->cpu.pc += inst->size; + if (sim->cpu.state != CPU_HALTED && !sim->cpu.substring) + sim->cpu.pc += inst->size; /* Check for interrupts */ - cpuCheckIRQs(emu); + cpuCheckIRQs(sim); } /* An exception or interrupt occurred */ - if (emu->cpu.causeCode != 0) { - emu->cpu.state = CPU_EXCEPTION; - emu->cpu.substring = 0; + if (sim->cpu.causeCode != 0) { + sim->cpu.state = CPU_EXCEPTION; + sim->cpu.substring = 0; } /* Switch to fetch mode */ - else if (emu->cpu.state != CPU_HALTED && - emu->cpu.busWait == 0 && !emu->cpu.substring) { - emu->cpu.state = CPU_FETCH; + else if (sim->cpu.state != CPU_HALTED && + sim->cpu.busWait == 0 && !sim->cpu.substring) { + sim->cpu.state = CPU_FETCH; } return 0; } /* Enter an exception state */ -static int cpuException(VB *emu) { - uint16_t causeCode = emu->cpu.causeCode; +static int cpuException(VB *sim) { + uint16_t causeCode = sim->cpu.causeCode; int irq = causeCode < 0xFF00; /* Fatal exception */ - if (emu->cpu.psw.np) { + if (sim->cpu.psw.np) { /* Write the cause code for debugging */ - if (emu->cpu.busWait == 0) { - if (cpuWrite(emu, 0x00000000, VB_S32, 0xFFFF0000 | causeCode)) + if (sim->cpu.busWait == 0) { + if (cpuWrite(sim, 0x00000000, VB_S32, 0xFFFF0000 | causeCode)) return 1; /* Update state */ - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Write PSW for debugging */ - if (emu->cpu.busWait == 1) { - if (cpuWrite(emu, 0x00000000, VB_S32, - vbGetSystemRegister(emu, VB_PSW))) + if (sim->cpu.busWait == 1) { + if (cpuWrite(sim, 0x00000000, VB_S32, + vbGetSystemRegister(sim, VB_PSW))) return 1; /* Update state */ - emu->cpu.busWait = 2; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 2; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Write PC for debugging */ - if (emu->cpu.busWait == 2) { - if (cpuWrite(emu, 0x00000000, VB_S32, emu->cpu.pc)) + if (sim->cpu.busWait == 2) { + if (cpuWrite(sim, 0x00000000, VB_S32, sim->cpu.pc)) return 1; /* Update state */ - emu->cpu.busWait = 3; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 3; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Update state */ - emu->cpu.busWait = 0; - emu->cpu.causeCode = 0; - emu->cpu.state = CPU_FATAL; + sim->cpu.busWait = 0; + sim->cpu.causeCode = 0; + sim->cpu.state = CPU_FATAL; return 0; } /* Duplexed exception */ - if (emu->cpu.psw.ep) { - emu->cpu.ecr.fecc = causeCode; - emu->cpu.fepsw = vbGetSystemRegister(emu, VB_PSW); - emu->cpu.fepc = emu->cpu.pc; - emu->cpu.fepcFrom = emu->cpu.pcFrom; - emu->cpu.fepcTo = emu->cpu.pcTo; - emu->cpu.psw.np = 1; - emu->cpu.pc = 0xFFFFFFD0; + if (sim->cpu.psw.ep) { + sim->cpu.ecr.fecc = causeCode; + sim->cpu.fepsw = vbGetSystemRegister(sim, VB_PSW); + sim->cpu.fepc = sim->cpu.pc; + sim->cpu.psw.np = 1; + sim->cpu.pc = 0xFFFFFFD0; } /* Exception or interrupt */ else { - emu->cpu.ecr.eicc = causeCode; - emu->cpu.eipsw = vbGetSystemRegister(emu, VB_PSW); - emu->cpu.eipc = emu->cpu.pc + (irq ? 2 : 0); - emu->cpu.eipcFrom = emu->cpu.pcFrom; - emu->cpu.eipcTo = emu->cpu.pcTo; - emu->cpu.psw.ep = 1; - emu->cpu.pc = (causeCode & 0x0040) != 0 ? + sim->cpu.ecr.eicc = causeCode; + sim->cpu.eipsw = vbGetSystemRegister(sim, VB_PSW); + sim->cpu.eipc = sim->cpu.pc + (irq ? 2 : 0); + sim->cpu.psw.ep = 1; + sim->cpu.pc = (causeCode & 0x0040) != 0 ? 0xFFFFFF60 : ((uint32_t) 0xFFFF0000 | (causeCode & 0xFFF0)); } /* Interrupt */ if (irq) - emu->cpu.psw.i += emu->cpu.psw.i == 15 ? 0 : 1; + sim->cpu.psw.i += sim->cpu.psw.i == 15 ? 0 : 1; /* Update state */ - emu->cpu.causeCode = 0; - emu->cpu.state = CPU_FETCH; - emu->cpu.psw.id = 1; - emu->cpu.psw.ae = 0; - emu->cpu.pcFrom = emu->cpu.pc; - emu->cpu.pcTo = emu->cpu.pc; - /* emu->cpu.clocks = ? */ + sim->cpu.causeCode = 0; + sim->cpu.state = CPU_FETCH; + sim->cpu.psw.id = 1; + sim->cpu.psw.ae = 0; + /* sim->cpu.clocks = ? */ /* Call the breakpoint handler if available */ - return emu->onException != NULL && emu->onException(emu, causeCode); + return sim->onException != NULL && sim->onException(sim, causeCode); } /* Perform instruction fetch operations */ -static int cpuFetch(VB *emu) { - VB_INSTRUCTION *inst; /* Reference to emu->cpu.inst */ +static int cpuFetch(VB *sim) { + VB_INSTRUCTION *inst; /* Reference to sim->cpu.inst */ uint8_t opcode; /* 6-bit instruction opcode */ /* Need to read a data unit */ - if (emu->cpu.busWait == 0) { + if (sim->cpu.busWait == 0) { /* Read the data unit from the bus */ - if (cpuReadFetch(emu, emu->cpu.pc + (emu->cpu.fetch << 1))) + if (cpuReadFetch(sim, sim->cpu.pc + (sim->cpu.fetch << 1))) return 1; /* Update state */ - emu->cpu.busWait = 1; - emu->cpu.clocks = emu->cpu.access.clocks; + sim->cpu.busWait = 1; + sim->cpu.clocks = sim->cpu.access.clocks; /* Wait for the bus access to complete */ - if (emu->cpu.clocks > 0) + if (sim->cpu.clocks > 0) return 0; } /* Update state */ - inst = &emu->cpu.inst; - inst->bits[emu->cpu.fetch] = emu->cpu.access.value; - emu->cpu.busWait = 0; + inst = &sim->cpu.inst; + inst->bits[sim->cpu.fetch] = sim->cpu.access.value; + sim->cpu.busWait = 0; /* Working variables */ opcode = inst->bits[0] >> 10 & 0x3F; /* First fetch */ - if (emu->cpu.fetch == 0) { + if (sim->cpu.fetch == 0) { /* Update state */ inst->size = CPU_SIZES[opcode]; /* A second fetch is needed */ if (inst->size == 4) { - emu->cpu.fetch = 1; + sim->cpu.fetch = 1; return 0; } @@ -1685,38 +1661,38 @@ static int cpuFetch(VB *emu) { } /* Update state */ - emu->cpu.fetch = 0; - emu->cpu.state = CPU_EXECUTE; + sim->cpu.fetch = 0; + sim->cpu.state = CPU_EXECUTE; return 0; } /* Process the simulation for some number of clocks */ -static int cpuEmulate(VB *emu, uint32_t clocks) { +static int cpuEmulate(VB *sim, uint32_t clocks) { /* Fatal halt: cannot break */ - if (emu->cpu.state == CPU_FATAL) + if (sim->cpu.state == CPU_FATAL) return 0; /* Process all clocks */ do { /* The next operation is after the remaining clocks */ - if (clocks < emu->cpu.clocks) { - emu->cpu.clocks -= clocks; - break; + if (clocks < sim->cpu.clocks) { + sim->cpu.clocks -= clocks; + return 0; } /* Update remaining clocks */ - clocks -= emu->cpu.clocks; - emu->cpu.clocks = 0; + clocks -= sim->cpu.clocks; + sim->cpu.clocks = 0; /* Processing by operations state */ - switch (emu->cpu.state) { - case CPU_EXCEPTION: if ( cpuException(emu)) return 1; break; - case CPU_EXECUTE : if ( cpuExecute (emu)) return 1; break; - case CPU_FATAL : return 0; break; - case CPU_FETCH : if ( cpuFetch (emu)) return 1; break; - case CPU_HALTED : if (!cpuCheckIRQs(emu)) return 0; break; + switch (sim->cpu.state) { + case CPU_EXCEPTION: if ( cpuException(sim)) return 1; break; + case CPU_EXECUTE : if ( cpuExecute (sim)) return 1; break; + case CPU_FATAL : return 0; + case CPU_FETCH : if ( cpuFetch (sim)) return 1; break; + case CPU_HALTED : if (!cpuCheckIRQs(sim)) return 0; break; } } while (clocks > 0); @@ -1726,63 +1702,49 @@ static int cpuEmulate(VB *emu, uint32_t clocks) { } /* Simulate a hardware reset */ -static void cpuReset(VB *emu) { +static void cpuReset(VB *sim) { int x; /* Iterator */ /* Registers */ - vbSetProgramCounter(emu, 0xFFFFFFF0); - vbSetSystemRegister(emu, VB_ECR, 0x0000FFF0); - vbSetSystemRegister(emu, VB_PSW, 0x00008000); + vbSetProgramCounter(sim, 0xFFFFFFF0); + vbSetSystemRegister(sim, VB_ECR, 0x0000FFF0); + vbSetSystemRegister(sim, VB_PSW, 0x00008000); /* Cache */ - vbSetSystemRegister(emu, VB_CHCW, 0x00000000); + vbSetSystemRegister(sim, VB_CHCW, 0x00000000); /* Other state */ - emu->cpu.busWait = 0; - emu->cpu.causeCode = 0; - emu->cpu.clocks = 0; - emu->cpu.fetch = 0; - emu->cpu.state = CPU_FETCH; - emu->cpu.substring = 0; for (x = 0; x < 5; x++) - emu->cpu.irq[x] = 0; + sim->cpu.irq[x] = 0; /* Other registers (the hardware does not do this) */ for (x = 0; x < 32; x++) - emu->cpu.program[x] = 0x00000000; - emu->cpu.adtre = 0x00000000; - emu->cpu.eipc = 0x00000000; - emu->cpu.eipsw = 0x00000000; - emu->cpu.fepc = 0x00000000; - emu->cpu.fepsw = 0x00000000; - emu->cpu.sr29 = 0x00000000; - emu->cpu.sr31 = 0x00000000; - - /* History tracking */ - emu->cpu.eipcFrom = 0xFFFFFFF0; - emu->cpu.eipcTo = 0xFFFFFFF0; - emu->cpu.fepcFrom = 0xFFFFFFF0; - emu->cpu.fepcTo = 0xFFFFFFF0; - emu->cpu.pcFrom = 0xFFFFFFF0; - emu->cpu.pcTo = 0xFFFFFFF0; + 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 *emu, uint32_t clocks) { +static uint32_t cpuUntil(VB *sim, uint32_t clocks) { /* Cannot break */ if ( - emu->cpu.state == CPU_HALTED || - emu->cpu.state == CPU_FATAL || ( - emu->onException == NULL && - emu->onExecute == NULL && - emu->onFetch == NULL && - emu->onRead == NULL && - emu->onWrite == NULL + 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 emu->cpu.clocks < clocks ? emu->cpu.clocks : clocks; + return sim->cpu.clocks < clocks ? sim->cpu.clocks : clocks; } diff --git a/core/vb.c b/core/vb.c index b93c54f..7aad9be 100644 --- a/core/vb.c +++ b/core/vb.c @@ -1,4 +1,4 @@ -#define VBAPI +#define VBAPI VB_EXPORT /* Header includes */ #include @@ -33,15 +33,15 @@ static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 }; /***************************** Module Functions ******************************/ /* Process a simulation for some number of clocks */ -static int sysEmulate(VB *emu, uint32_t clocks) { +static int sysEmulate(VB *sim, uint32_t clocks) { int broke; - broke = cpuEmulate(emu, clocks); + broke = cpuEmulate(sim, clocks); return broke; } /* Determine the number of clocks before a break condititon could occur */ -static uint32_t sysUntil(VB *emu, uint32_t clocks) { - clocks = cpuUntil(emu, clocks); +static uint32_t sysUntil(VB *sim, uint32_t clocks) { + clocks = cpuUntil(sim, clocks); return clocks; } @@ -49,242 +49,253 @@ static uint32_t sysUntil(VB *emu, uint32_t clocks) { /******************************* API Functions *******************************/ -/* Associate two simulations as peers */ -void vbConnect(VB *emu1, VB *emu2) { +/* Associate two simulations as peers, or remove an association */ +void vbConnect(VB *a, VB *b) { + + /* Disconnect */ + if (b == NULL) { + if (a->peer != NULL) + a->peer->peer = NULL; + a->peer = NULL; + return; + } + + /* The simulations are already linked */ + if (a->peer == b && b->peer == a) + return; /* Disconnect any existing link associations */ - if (emu1->peer != NULL && emu1->peer != emu2) - emu1->peer->peer = NULL; - if (emu2->peer != NULL && emu2->peer != emu1) - emu2->peer->peer = NULL; + if (a->peer != NULL && a->peer != b) + a->peer->peer = NULL; + if (b->peer != NULL && b->peer != a) + b->peer->peer = NULL; /* Link the two simulations */ - emu1->peer = emu2; - emu2->peer = emu1; + a->peer = b; + b->peer = a; } -/* Disassociate linked peers */ -void vbDisconnect(VB *emu) { - if (emu->peer != NULL) - emu->peer->peer = NULL; - emu->peer = NULL; -} - -/* Process one or two simulations */ -int vbEmulate(VB *emu1, VB *emu2, uint32_t *clocks) { +/* Process one simulation */ +int vbEmulate(VB *sim, uint32_t *clocks) { int broke; /* The simulation requested an application break */ uint32_t until; /* Maximum clocks before a break could happen */ + int x; /* Iterator */ - /* Processing one simulaiton */ - if (emu2 == NULL) { - do { - until = sysUntil (emu1, *clocks); - broke = sysEmulate(emu1, until ); - *clocks -= until; - } while (!broke && *clocks > 0); - } + /* Process the simulation until a break condition occurs */ + do { + until = *clocks; + until = sysUntil (sim, until); + broke = sysEmulate(sim, until); + *clocks -= until; + } while (!broke && *clocks > 0); - /* Processing two simulations */ - else { - do { - until = sysUntil (emu1, *clocks); - until = sysUntil (emu2, until ); - broke = sysEmulate(emu1, until ); - broke |= sysEmulate(emu2, until ); - *clocks -= until; - } while (!broke && *clocks > 0); - } + return broke; +} + +/* Process multiple simulations */ +int vbEmulateMulti(VB **sims, int count, uint32_t *clocks) { + int broke; /* The simulation requested an application break */ + uint32_t until; /* Maximum clocks before a break could happen */ + int x; /* Iterator */ + + /* Process simulations until a break condition occurs */ + do { + broke = 0; + until = *clocks; + for (x = 0; x < count; x++) + until = sysUntil (sims[x], until); + for (x = 0; x < count; x++) + broke |= sysEmulate(sims[x], until); + *clocks -= until; + } while (!broke && *clocks > 0); return broke; } /* Retrieve a current breakpoint callback */ -void* vbGetCallback(VB *emu, int type) { +void* vbGetCallback(VB *sim, int type) { /* -Wpedantic ignored for pointer conversion because no alternative */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" switch (type) { - case VB_ONEXCEPTION: return emu->onException; - case VB_ONEXECUTE : return emu->onExecute; - case VB_ONFETCH : return emu->onFetch; - case VB_ONREAD : return emu->onRead; - case VB_ONWRITE : return emu->onWrite; + case VB_ONEXCEPTION: return sim->onException; + case VB_ONEXECUTE : return sim->onExecute; + case VB_ONFETCH : return sim->onFetch; + case VB_ONREAD : return sim->onRead; + case VB_ONWRITE : return sim->onWrite; } #pragma GCC diagnostic pop return NULL; } /* Retrieve the value of PC */ -uint32_t vbGetProgramCounter(VB *emu, int type) { - switch (type) { - case VB_PC : return emu->cpu.pc; - case VB_PC_FROM: return emu->cpu.pcFrom; - case VB_PC_TO : return emu->cpu.pcTo; - } - return 0; +uint32_t vbGetProgramCounter(VB *sim) { + return sim->cpu.pc; } /* Retrieve the value of a program register */ -int32_t vbGetProgramRegister(VB *emu, int id) { - return id < 1 || id > 31 ? 0 : emu->cpu.program[id]; +int32_t vbGetProgramRegister(VB *sim, int id) { + return id < 1 || id > 31 ? 0 : sim->cpu.program[id]; } /* Retrieve the ROM buffer */ -void* vbGetROM(VB *emu, uint32_t *size) { +void* vbGetROM(VB *sim, uint32_t *size) { if (size != NULL) - *size = emu->cart.romSize; - return emu->cart.rom; + *size = sim->cart.romSize; + return sim->cart.rom; } /* Retrieve the SRAM buffer */ -void* vbGetSRAM(VB *emu, uint32_t *size) { +void* vbGetSRAM(VB *sim, uint32_t *size) { if (size != NULL) - *size = emu->cart.sramSize; - return emu->cart.sram; + *size = sim->cart.sramSize; + return sim->cart.sram; } /* Retrieve the value of a system register */ -uint32_t vbGetSystemRegister(VB *emu, int id) { +uint32_t vbGetSystemRegister(VB *sim, int id) { switch (id) { - case VB_ADTRE: return emu->cpu.adtre; - case VB_CHCW : return emu->cpu.chcw.ice << 1; - case VB_EIPC : return emu->cpu.eipc; - case VB_EIPSW: return emu->cpu.eipsw; - case VB_FEPC : return emu->cpu.fepc; - case VB_FEPSW: return emu->cpu.fepsw; + case VB_ADTRE: return sim->cpu.adtre; + case VB_CHCW : return sim->cpu.chcw.ice << 1; + case VB_EIPC : return sim->cpu.eipc; + case VB_EIPSW: return sim->cpu.eipsw; + case VB_FEPC : return sim->cpu.fepc; + case VB_FEPSW: return sim->cpu.fepsw; case VB_PIR : return 0x00005346; case VB_TKCW : return 0x000000E0; - case 29 : return emu->cpu.sr29; + case 29 : return sim->cpu.sr29; case 30 : return 0x00000004; - case 31 : return emu->cpu.sr31; + case 31 : return sim->cpu.sr31; case VB_ECR : return - (uint32_t) emu->cpu.ecr.fecc << 16 | emu->cpu.ecr.eicc; + (uint32_t) sim->cpu.ecr.fecc << 16 | sim->cpu.ecr.eicc; case VB_PSW : return - (uint32_t) emu->cpu.psw.i << 16 | - (uint32_t) emu->cpu.psw.np << 15 | - (uint32_t) emu->cpu.psw.ep << 14 | - (uint32_t) emu->cpu.psw.ae << 13 | - (uint32_t) emu->cpu.psw.id << 12 | - (uint32_t) emu->cpu.psw.fro << 9 | - (uint32_t) emu->cpu.psw.fiv << 8 | - (uint32_t) emu->cpu.psw.fzd << 7 | - (uint32_t) emu->cpu.psw.fov << 6 | - (uint32_t) emu->cpu.psw.fud << 5 | - (uint32_t) emu->cpu.psw.fpr << 4 | - (uint32_t) emu->cpu.psw.cy << 3 | - (uint32_t) emu->cpu.psw.ov << 2 | - (uint32_t) emu->cpu.psw.s << 1 | - (uint32_t) emu->cpu.psw.z + (uint32_t) sim->cpu.psw.i << 16 | + (uint32_t) sim->cpu.psw.np << 15 | + (uint32_t) sim->cpu.psw.ep << 14 | + (uint32_t) sim->cpu.psw.ae << 13 | + (uint32_t) sim->cpu.psw.id << 12 | + (uint32_t) sim->cpu.psw.fro << 9 | + (uint32_t) sim->cpu.psw.fiv << 8 | + (uint32_t) sim->cpu.psw.fzd << 7 | + (uint32_t) sim->cpu.psw.fov << 6 | + (uint32_t) sim->cpu.psw.fud << 5 | + (uint32_t) sim->cpu.psw.fpr << 4 | + (uint32_t) sim->cpu.psw.cy << 3 | + (uint32_t) sim->cpu.psw.ov << 2 | + (uint32_t) sim->cpu.psw.s << 1 | + (uint32_t) sim->cpu.psw.z ; } return 0; } /* Prepare a simulation state instance for use */ -void vbInit(VB *emu) { +void vbInit(VB *sim) { /* Breakpoint callbacks */ - emu->onException = NULL; - emu->onExecute = NULL; - emu->onFetch = NULL; - emu->onRead = NULL; - emu->onWrite = NULL; + sim->onException = NULL; + sim->onExecute = NULL; + sim->onFetch = NULL; + sim->onRead = NULL; + sim->onWrite = NULL; /* System */ - emu->peer = NULL; + sim->peer = NULL; /* Cartridge */ - emu->cart.rom = NULL; - emu->cart.romSize = 0; - emu->cart.sram = NULL; - emu->cart.sramSize = 0; + sim->cart.rom = NULL; + sim->cart.romSize = 0; + sim->cart.sram = NULL; + sim->cart.sramSize = 0; /* Everything else */ - vbReset(emu); + vbReset(sim); } /* Read a data unit from the bus */ -int32_t vbRead(VB *emu, uint32_t address, int type, int debug) { +int32_t vbRead(VB *sim, uint32_t address, int type, int debug) { return type < 0 || type >= (int) sizeof TYPE_SIZES ? 0 : - busRead(emu, address, type, debug); + busRead(sim, address, type, debug); } /* Simulate a hardware reset */ -void vbReset(VB *emu) { +void vbReset(VB *sim) { uint32_t x; /* Iterator */ /* Subsystem components */ - cpuReset(emu); + cpuReset(sim); /* WRAM (the hardware does not do this) */ for (x = 0; x < 0x10000; x++) - emu->wram[x] = 0x00; + sim->wram[x] = 0x00; } /* Specify a breakpoint callback */ -void vbSetCallback(VB *emu, int type, void *callback) { +void vbSetCallback(VB *sim, int type, void *callback) { /* -Wpedantic ignored for pointer conversion because no alternative */ #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-Wpedantic" switch (type) { - case VB_ONEXCEPTION: emu->onException=(VB_EXCEPTIONPROC)callback;break; - case VB_ONEXECUTE : emu->onExecute =(VB_EXECUTEPROC )callback;break; - case VB_ONFETCH : emu->onFetch =(VB_FETCHPROC )callback;break; - case VB_ONREAD : emu->onRead =(VB_READPROC )callback;break; - case VB_ONWRITE : emu->onWrite =(VB_WRITEPROC )callback;break; + case VB_ONEXCEPTION: sim->onException=(VB_EXCEPTIONPROC)callback;break; + case VB_ONEXECUTE : sim->onExecute =(VB_EXECUTEPROC )callback;break; + case VB_ONFETCH : sim->onFetch =(VB_FETCHPROC )callback;break; + case VB_ONREAD : sim->onRead =(VB_READPROC )callback;break; + case VB_ONWRITE : sim->onWrite =(VB_WRITEPROC )callback;break; } #pragma GCC diagnostic pop } /* Specify a new value for PC */ -uint32_t vbSetProgramCounter(VB *emu, uint32_t value) { +uint32_t vbSetProgramCounter(VB *sim, uint32_t value) { value &= 0xFFFFFFFE; - emu->cpu.causeCode = 0; - emu->cpu.fetch = 0; - emu->cpu.pc = value; - emu->cpu.state = CPU_FETCH; - emu->cpu.substring = 0; + sim->cpu.busWait = 0; + sim->cpu.causeCode = 0; + sim->cpu.clocks = 0; + sim->cpu.fetch = 0; + sim->cpu.pc = value; + sim->cpu.state = CPU_FETCH; + sim->cpu.substring = 0; return value; } /* Specify a new value for a program register */ -int32_t vbSetProgramRegister(VB *emu, int id, int32_t value) { - return id < 1 || id > 31 ? 0 : (emu->cpu.program[id] = value); +int32_t vbSetProgramRegister(VB *sim, int id, int32_t value) { + return id < 1 || id > 31 ? 0 : (sim->cpu.program[id] = value); } /* Supply a ROM buffer */ -int vbSetROM(VB *emu, void *rom, uint32_t size) { +int vbSetROM(VB *sim, void *rom, uint32_t size) { /* Check the buffer size */ if (size < 1024 || size > 0x1000000 || ((size - 1) & size) != 0) return 0; /* Configure the ROM buffer */ - emu->cart.rom = (uint8_t *) rom; - emu->cart.romSize = size; + sim->cart.rom = (uint8_t *) rom; + sim->cart.romSize = size; return 1; } /* Supply an SRAM buffer */ -int vbSetSRAM(VB *emu, void *sram, uint32_t size) { +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 */ - emu->cart.sram = (uint8_t *) sram; - emu->cart.sramSize = size; + sim->cart.sram = (uint8_t *) sram; + sim->cart.sramSize = size; return 1; } /* Specify a new value for a system register */ -uint32_t vbSetSystemRegister(VB *emu, int id, uint32_t value) { - return cpuSetSystemRegister(emu, id, value, 1); +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 *emu, uint32_t address, int type, int32_t value, int debug) { +void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) { if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES) - busWrite(emu, address, type, value, debug); + busWrite(sim, address, type, value, debug); } diff --git a/core/vb.h b/core/vb.h index 8fffc77..fbca175 100644 --- a/core/vb.h +++ b/core/vb.h @@ -141,14 +141,6 @@ struct VB { uint32_t pc; /* Program counter */ int32_t program[32]; /* program registers */ - /* History tracking */ - uint32_t eipcFrom; /* Source of most recent jump */ - uint32_t eipcTo; /* Destination of most recent jump */ - uint32_t fepcFrom; /* Source of most recent jump */ - uint32_t fepcTo; /* Destination of most recent jump */ - uint32_t pcFrom; /* Source of most recent jump */ - uint32_t pcTo; /* Destination of most recent jump */ - /* Other fields */ VB_ACCESS access; /* Memory access descriptor */ VB_INSTRUCTION inst; /* Instruction descriptor */ @@ -175,27 +167,27 @@ struct VB { -/**************************** Function Prototypes ****************************/ +/******************************* API Commands ********************************/ -VBAPI void vbConnect (VB *emu1, VB *emu2); -VBAPI void vbDisconnect (VB *emu); -VBAPI int vbEmulate (VB *emu1, VB *emu2, uint32_t *clocks); -VBAPI void* vbGetCallback (VB *emu, int type); -VBAPI uint32_t vbGetProgramCounter (VB *emu, int type); -VBAPI int32_t vbGetProgramRegister (VB *emu, int id); -VBAPI void* vbGetROM (VB *emu, uint32_t *size); -VBAPI void* vbGetSRAM (VB *emu, uint32_t *size); -VBAPI uint32_t vbGetSystemRegister (VB *emu, int id); -VBAPI void vbInit (VB *emu); -VBAPI int32_t vbRead (VB *emu, uint32_t address, int type, int debug); -VBAPI void vbReset (VB *emu); -VBAPI void vbSetCallback (VB *emu, int type, void *callback); -VBAPI uint32_t vbSetProgramCounter (VB *emu, uint32_t value); -VBAPI int32_t vbSetProgramRegister (VB *emu, int id, int32_t value); -VBAPI int vbSetROM (VB *emu, void *rom, uint32_t size); -VBAPI int vbSetSRAM (VB *emu, void *sram, uint32_t size); -VBAPI uint32_t vbSetSystemRegister (VB *emu, int id, uint32_t value); -VBAPI void vbWrite (VB *emu, uint32_t address, int type, int32_t value, int debug); +VBAPI void vbConnect (VB *sim1, VB *sim2); +VBAPI int vbEmulate (VB *sim, uint32_t *clocks); +VBAPI int vbEmulateMulti (VB **sims, int count, uint32_t *clocks); +VBAPI void* vbGetCallback (VB *sim, int type); +VBAPI uint32_t vbGetProgramCounter (VB *sim); +VBAPI int32_t vbGetProgramRegister (VB *sim, int id); +VBAPI void* vbGetROM (VB *sim, uint32_t *size); +VBAPI void* vbGetSRAM (VB *sim, uint32_t *size); +VBAPI uint32_t vbGetSystemRegister (VB *sim, int id); +VBAPI void vbInit (VB *sim); +VBAPI int32_t vbRead (VB *sim, uint32_t address, int type, int debug); +VBAPI void vbReset (VB *sim); +VBAPI void vbSetCallback (VB *sim, int type, void *callback); +VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value); +VBAPI int32_t vbSetProgramRegister (VB *sim, int id, int32_t value); +VBAPI int vbSetROM (VB *sim, void *rom, uint32_t size); +VBAPI int vbSetSRAM (VB *sim, void *sram, uint32_t size); +VBAPI uint32_t vbSetSystemRegister (VB *sim, int id, uint32_t value); +VBAPI void vbWrite (VB *sim, uint32_t address, int type, int32_t value, int debug); diff --git a/license.txt b/license.txt index 546ca0a..3fd8c86 100644 --- a/license.txt +++ b/license.txt @@ -1,4 +1,4 @@ -Copyright (C) 2021 Guy Perfect +Copyright (C) 2022 Guy Perfect This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages diff --git a/makefile b/makefile index 3a30ed8..f923d31 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ .PHONY: help help: @echo - @echo "Virtual Boy Emulator - September 30, 2021" + @echo "Virtual Boy Emulator - April 14, 2022" @echo @echo "Target build environment is any Debian with the following packages:" @echo " emscripten" @@ -31,7 +31,7 @@ bundle: .PHONY: clean clean: - @rm -f vbemu_*.html core.wasm + @rm -f vbemu_*.html app/core/core.wasm .PHONY: core core: @@ -42,7 +42,9 @@ core: .PHONY: wasm wasm: - @emcc -o core.wasm wasm/wasm.c core/vb.c -Icore -D VB_LITTLEENDIAN \ - --no-entry -O2 -flto -s WASM=1 -s EXPORTED_RUNTIME_METHODS=[] \ - -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing - @rm -f *.wasm.tmp* + @emcc -o app/core/core.wasm wasm/wasm.c core/vb.c -Icore \ + -D VB_LITTLEENDIAN --no-entry -O2 -flto -s WASM=1 \ + -D "VB_EXPORT=__attribute__((used))" \ + -s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \ + -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing + @rm -f app/core/*.wasm.tmp* diff --git a/wasm/wasm.c b/wasm/wasm.c index 9ab614d..44a734f 100644 --- a/wasm/wasm.c +++ b/wasm/wasm.c @@ -1,21 +1,31 @@ #include #include #include +#include #include #include -//////////////////////////////// Static Memory //////////////////////////////// - -static VB sims[2]; // Hardware simulations - - - /////////////////////////////// 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) { +EMSCRIPTEN_KEEPALIVE void Free(void *ptr) { free(ptr); } @@ -24,116 +34,97 @@ EMSCRIPTEN_KEEPALIVE void* Malloc(int size) { return malloc(size); } -// Read multiple data units from the bus +// 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( - int sim, uint8_t *dest, uint32_t address, uint32_t size, int debug) { + VB* sim, uint8_t *dest, uint32_t address, uint32_t size) { for (; size > 0; address++, size--, dest++) - *dest = vbRead(&sims[sim], address, VB_U8, debug); + *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 *emu, int fetch, VB_ACCESS *acc) { - if (fetch == 0 && vbGetProgramCounter(emu, VB_PC) != SingleStepPC) +static int SingleStepProc(VB *sim, int fetch, VB_ACCESS *acc) { + if (fetch == 0 && vbGetProgramCounter(sim) != SingleStepPC) return 1; - acc->value = vbRead(emu, acc->address, acc->type, 0); + acc->value = vbRead(sim, acc->address, acc->type, 0); return 0; } -EMSCRIPTEN_KEEPALIVE void SingleStep(int sim) { +EMSCRIPTEN_KEEPALIVE void SingleStep(VB *sim0, VB *sim1) { uint32_t clocks = 400000; // 1/50s - VB *emu = &sims[sim]; - SingleStepPC = vbGetProgramCounter(emu, VB_PC); - emu->onFetch = &SingleStepProc; - vbEmulate(emu, NULL, &clocks); - emu->onFetch = NULL; + 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); } -// Attempt to execute until the following instruction -static uint32_t RunNextPC; -static int RunNextProcB(VB *emu, int fetch, VB_ACCESS *acc) { - if (fetch == 0 && vbGetProgramCounter(emu, VB_PC) == RunNextPC) - return 1; - acc->value = vbRead(emu, acc->address, acc->type, 0); - return 0; -} -static int RunNextProcA(VB *emu, VB_INSTRUCTION *inst) { - RunNextPC = vbGetProgramCounter(emu, VB_PC) + inst->size; - emu->onExecute = NULL; - emu->onFetch = &RunNextProcB; - return 0; -} -EMSCRIPTEN_KEEPALIVE void RunNext(int sim) { - uint32_t clocks = 400000; // 1/50s - VB *emu = &sims[sim]; - emu->onExecute = &RunNextProcA; - vbEmulate(emu, NULL, &clocks); - emu->onFetch = NULL; -} - - - -//////////////////////////////// Core Commands //////////////////////////////// - -// Retrieve the value of PC -EMSCRIPTEN_KEEPALIVE uint32_t GetProgramCounter(int sim, int type) { - return vbGetProgramCounter(&sims[sim], type); -} - -// Retrieve the value of a program register -EMSCRIPTEN_KEEPALIVE int32_t GetProgramRegister(int sim, int id) { - return vbGetProgramRegister(&sims[sim], id); -} - -// Retrieve the value of a system register -EMSCRIPTEN_KEEPALIVE uint32_t GetSystemRegister(int sim, int id) { - return vbGetSystemRegister(&sims[sim], id); -} - -// Prepare simulation state instances for use -EMSCRIPTEN_KEEPALIVE void Init() { - vbInit(&sims[0]); - vbInit(&sims[1]); -} - -// Read a data unit from the bus -EMSCRIPTEN_KEEPALIVE int32_t Read(int sim,uint32_t address,int type,int debug){ - return vbRead(&sims[sim], address, type, debug); -} - -// Simulate a hardware reset -EMSCRIPTEN_KEEPALIVE void Reset(int sim) { - vbReset(&sims[sim]); -} - -// Specify a new value for PC -EMSCRIPTEN_KEEPALIVE uint32_t SetProgramCounter(int sim, uint32_t value) { - return vbSetProgramCounter(&sims[sim], value); -} - -// Specify a new value for a program register -EMSCRIPTEN_KEEPALIVE int32_t SetProgramRegister(int sim,int id,int32_t value) { - return vbSetProgramRegister(&sims[sim], id, value); -} - -// Supply a ROM buffer -EMSCRIPTEN_KEEPALIVE int SetROM(int sim, void *rom, uint32_t size) { - free(vbGetROM(&sims[sim], NULL)); - return vbSetROM(&sims[sim], rom, size); -} - -// Supply an SRAM buffer -EMSCRIPTEN_KEEPALIVE int SetSRAM(int sim, void *sram, uint32_t size) { - free(vbGetSRAM(&sims[sim], NULL)); - return vbSetSRAM(&sims[sim], sram, size); -} - -// Specify a new value for a system register -EMSCRIPTEN_KEEPALIVE uint32_t SetSystemRegister(int sim,int id,uint32_t value){ - return vbSetSystemRegister(&sims[sim], id, value); -} - -// Write a data unit to the bus -EMSCRIPTEN_KEEPALIVE void Write( - int sim, uint32_t address, int type, int32_t value, int debug) { - vbWrite(&sims[sim], address, type, value, debug); +EMSCRIPTEN_KEEPALIVE uint32_t Clocks(VB *sim) { + return sim->cpu.clocks; }