Full front-end rewrite

This commit is contained in:
Guy Perfect 2022-04-14 20:51:09 -05:00
parent fccb799a9c
commit 3cf006ba13
59 changed files with 9424 additions and 7228 deletions

View File

@ -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);
}
};

View File

@ -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");

View File

@ -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();
}
};

View File

@ -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();

View File

@ -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,49 +10,139 @@ 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 //////////////////////////////
// Adds a bundled file from loaded file data
static add(name, data) {
return Bundle.files[name] = new Bundle(name, data);
// 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;
}
// 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(".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" :
@ -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);
}
let ret = new FontFace(name, "url('"+
(Bundle.debug ? this.name : this.toDataURL()) + "'");
await ret.load();
document.fonts.add(ret);
return ret;
}
// 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;
});
// 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;
}
// 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 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 /////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// ZIP Bundler //
///////////////////////////////////////////////////////////////////////////////
// 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();
// Produce a .ZIP header for export
toZipHeader(crc32, offset) {
let central = offset !== undefined;
let ret = [];
if (central) {
ret.writeInt (4, 0x02014B50); // Signature
ret.writeInt (2, 20); // Version created by
Bundle.writeInt (ret, 4, 0x02014B50); // Signature
Bundle.writeInt (ret, 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
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) {
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
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
}
ret.writeString (file.name, true); // Filename
Bundle.writeString (ret, this.name); // Filename
if (!central)
ret.writeBytes(file.data); // File data
return ret.toByteArray();
Bundle.writeBytes(ret, this.data); // File data
return Uint8Array.from(ret);
}
// 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 //
// Boot Program //
///////////////////////////////////////////////////////////////////////////////
/*
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());

444
app/app/App.js Normal file
View File

@ -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 };

106
app/app/CPU.js Normal file
View File

@ -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 };

187
app/app/Debugger.js Normal file
View File

@ -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 };

892
app/app/Disassembler.js Normal file
View File

@ -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 };

467
app/app/Memory.js Normal file
View File

@ -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 };

863
app/app/RegisterList.js Normal file
View File

@ -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 };

44
app/app/Util.js Normal file
View File

@ -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
};

196
app/core/Core.js Normal file
View File

@ -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 };

378
app/core/CoreWorker.js Normal file
View File

@ -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);
}
}();

151
app/core/Sim.js Normal file
View File

@ -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 };

View File

@ -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"
}
}
}

74
app/locale/en-US.json Normal file
View File

@ -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}"
}
}

34
app/main.js Normal file
View File

@ -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)
}
});

View File

@ -3,8 +3,10 @@
<head>
<meta charset="utf-8">
<title>Virtual Boy Emulator</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<script>window.a=async(b,c,d,e)=>{e=document.createElement('canvas');e.width=c;e.height=d;e=e.getContext('2d');e.drawImage(b,0,0);c=e.getImageData(0,0,c,d).data.filter((z,y)=>!(y&3));d=c.indexOf(0);Object.getPrototypeOf(a).constructor(String.fromCharCode(...c.slice(0,d)))(b,c,d)}</script>
</head>
<body>
<img alt="" style="display: none;" onload="((a,b,c,d)=>{d=document.createElement('canvas');d.width=b;d.height=c;d=d.getContext('2d');d.drawImage(a,0,0);b=d.getImageData(0,0,b,c).data;for(c=0,d='';b[c];c+=4)d+=String.fromCodePoint(b[c]);new Function(d)(a,b,c)})(this,width,height)" src="">
<img alt="" style="display: none;" onload="a(this,width,height)" src="">
</body>
</html>

6
app/theme/check.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458 2.6458" version="1.1">
<g>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1.05832,1.653625 1.3229,-1.3229 v 0.66145 l -1.3229,1.3229 -0.79374,-0.79374 v -0.66145 z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 459 B

6
app/theme/close.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
<g>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 0.52916666,0.52916665 0.396875,0 0.52916664,0.52916665 0.5291667,-0.52916665 0.396875,0 0,0.396875 L 1.8520833,1.4552083 2.38125,1.984375 l 0,0.396875 -0.396875,0 L 1.4552083,1.8520834 0.92604166,2.38125 l -0.396875,0 0,-0.396875 L 1.0583333,1.4552083 0.52916666,0.92604165 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 652 B

6
app/theme/collapse.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
<g>
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 367 B

View File

@ -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;
--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;
}

7
app/theme/expand.svg Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
<g>
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="1.3229167" y="0.79375005" />
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 522 B

File diff suppressed because it is too large Load Diff

View File

@ -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;
}

6
app/theme/radio.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458332 2.6458332" version="1.1">
<g>
<circle style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" cx="1.3229166" cy="1.3229166" r="0.66145831" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 361 B

6
app/theme/scroll.svg Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
<g>
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 1.4552083,0.66145833 0.52916666,1.5874999 V 2.2489583 L 1.4552083,1.3229166 2.38125,2.2489583 V 1.5874999 Z" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 484 B

View File

@ -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;
}

View File

@ -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);
}
};

View File

@ -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);
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
this.element.setAttribute("aria-label", name);
this.element.innerText = text;
if (toolTip)
this.element.setAttribute("title", toolTip);
else this.element.removeAttribute("title");
// Regenerate localized display text
translate() {
super.translate();
if (this.contents != null)
this.contents.innerText = this.gui.translate(this.text, this);
}
}
///////////////////////////// Private Methods /////////////////////////////
///////////////////////////////////////////////////////////////////////////////
// CheckBox //
///////////////////////////////////////////////////////////////////////////////
// Key down event handler
onkeydown(e) {
// On/off toggle box
class CheckBox extends Button {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
attribute: "aria-checked",
className: "tk tk-checkbox",
role : "checkbox",
style : {},
type : Button.TOGGLE
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Default styles override
for (let entry of Object.entries({
display : "inline-grid",
gridTemplateColumns: "max-content auto"
})) if (!(entry[0] in uptions.style))
uptions.style[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
this.contents.classList.add("tk-contents");
// Configure icon
this.icon = document.createElement("div");
this.icon.className = "tk tk-icon";
this.icon.setAttribute("aria-hidden", "true");
this.prepend(this.icon);
}
}
///////////////////////////////////////////////////////////////////////////////
// Radio //
///////////////////////////////////////////////////////////////////////////////
// Single selection box
class Radio extends CheckBox {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
className: "tk tk-radio",
role : "radio",
type : Button.RADIO
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
}
}
///////////////////////////////////////////////////////////////////////////////
// Group //
///////////////////////////////////////////////////////////////////////////////
// Radio button or menu item group
class Group extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app) {
super(app, {
tagName: "div",
style : {
height : "0",
position: "absolute",
width : "0"
}
});
// Configure instance fields
this.items = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Add an item
add(item) {
// Error checking
if (!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 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;
}
// Configure event
e.preventDefault();
e.stopPropagation();
// Remove all items
clear() {
this.items.splice();
this.setAttribute("aria-owns", "");
}
// Pointer down event handler
onpointerdown(e) {
// Un-check all items in the group
deselect() {
for (let item of this.items)
if (item.isSelected && "setSelected" in item)
item.setSelected(false);
}
// Configure event
e.stopPropagation();
// 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 };

View File

@ -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;
}
};

View File

@ -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);
}
};

View File

@ -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.children = [];
this.gui = gui || this;
this.label = null;
this.resizeObserver = null;
this.visible = "visible" in options ? !!options.visible : true;
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 };

View File

@ -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;
}
};

View File

@ -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;
}
};

View File

@ -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();
}
};

View File

@ -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 };

View File

@ -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);
}
};

View File

@ -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" );
}
};

View File

@ -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);
}
};

1149
app/toolkit/ScrollBar.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -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;
}
};

View File

@ -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;
}
// Specify the component's display text
setText(text) {
text = !text && text !== 0 ? "" : "" + text;
this.lastCommit = text;
this.element.value = text;
// Error checking
if (typeof length != "number" || isNaN(length))
return;
// Range checking
length = Math.floor(length);
if (length < 0)
return;
// Configure component
this.maxLength = length;
this.setAttribute("maxlength", length);
}
// Specify a regex pattern for valid text characters
setPattern(pattern) {
/*
Disabled because user agents may not prevent invalid input
// Error checking
if (pattern && typeof pattern != "string")
return;
// Configure component
this.pattern = pattern = pattern || null;
this.setAttribute("pattern", pattern);
*/
}
// Specify the widget's display text
setText(text = "") {
this.element.value = text.toString();
}
///////////////////////////// Package Methods /////////////////////////////
// Update 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 };

View File

@ -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 };

File diff suppressed because it is too large Load Diff

View File

@ -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);
}
};

View File

@ -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();

View File

@ -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;
}
};

View File

@ -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();

View File

@ -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);
}
}

1372
core/cpu.c

File diff suppressed because it is too large Load Diff

251
core/vb.c
View File

@ -1,4 +1,4 @@
#define VBAPI
#define VBAPI VB_EXPORT
/* Header includes */
#include <float.h>
@ -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) {
/* Process the simulation until a break condition occurs */
do {
until = sysUntil (emu1, *clocks);
broke = sysEmulate(emu1, until );
until = *clocks;
until = sysUntil (sim, until);
broke = sysEmulate(sim, until);
*clocks -= until;
} while (!broke && *clocks > 0);
return broke;
}
/* Processing two simulations */
else {
/* 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 {
until = sysUntil (emu1, *clocks);
until = sysUntil (emu2, until );
broke = sysEmulate(emu1, until );
broke |= sysEmulate(emu2, until );
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);
}

View File

@ -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);

View File

@ -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

View File

@ -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*

View File

@ -1,19 +1,29 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten/emscripten.h>
#include <vb.h>
//////////////////////////////// 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) {
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);
}
// 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;
else vbEmulate(sim0, &clocks);
vbSetCallback(sim0, VB_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;
}