Newer (final?) web rewrite

This commit is contained in:
Guy Perfect 2023-03-08 10:42:27 -06:00
parent 7d31c55391
commit e20fe119f2
78 changed files with 9571 additions and 9496 deletions

View File

@ -1,214 +0,0 @@
import java.awt.image.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import javax.imageio.*;
public class Bundle {
// File loaded from disk
static class File2 implements Comparable<File2> {
// Instance fields
byte[] data; // Contents
String filename; // Full path relative to root
// Comparator
public int compareTo(File2 o) {
if (filename.equals("app/_boot.js"))
return -1;
if (o.filename.equals("app/_boot.js"))
return 1;
return filename.compareTo(o.filename);
}
}
// Load all files in directory tree into memory
static HashMap<String, File2> readFiles(String bundleTitle) {
var dirs = new ArrayDeque<File>();
var root = new File(".");
var subs = new ArrayDeque<String>();
var files = new HashMap<String, File2>();
// Process all subdirectories
dirs.add(root);
while (!dirs.isEmpty()) {
var dir = dirs.remove();
// Add all subdirectories
for (var sub : dir.listFiles(f->f.isDirectory())) {
// Exclusions
if (dir == root && sub.getName().equals(".git"))
continue;
// Add the directory for bundling
dirs.add(sub);
}
// Add all files
for (var file : dir.listFiles(f->f.isFile())) {
var file2 = new File2();
// Read the file into memory
try {
var stream = new FileInputStream(file);
file2.data = stream.readAllBytes();
stream.close();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
// Determine the file's full pathname
subs.clear();
subs.addFirst(file.getName());
for (;;) {
file = file.getParentFile();
if (file.equals(root))
break;
subs.addFirst(file.getName());
}
file2.filename = String.join("/", subs);
// Exclusions
if (
file2.filename.startsWith(".git" ) ||
file2.filename.startsWith(bundleTitle + "_") &&
file2.filename.endsWith (".html" )
) continue;
// Add the file to the output
files.put(file2.filename, file2);
}
}
return files;
}
// Prepend manifest object to _boot.js
static void manifest(HashMap<String, File2> files, String bundleName) {
// Produce a sorted list of files
var values = files.values().toArray(new File2[files.size()]);
Arrays.sort(values);
// Build a file manifest
var manifest = new StringBuilder();
manifest.append("\"use strict\";\nlet manifest=[");
for (var file : values) {
manifest.append(
"[\"" + file.filename + "\"," + file.data.length + "]");
if (file != values[values.length - 1])
manifest.append(",");
}
manifest.append("],bundleName=\"" + bundleName + "\";");
// Prepend the manifest to _boot.js
var boot = files.get("app/_boot.js");
boot.data = (
manifest.toString() +
new String(boot.data, StandardCharsets.UTF_8) +
"\u0000"
).getBytes(StandardCharsets.UTF_8);
}
// Construct bundled blob
static byte[] blob(HashMap<String, File2> files) {
// Produce a sorted list of files
var values = files.values().toArray(new File2[files.size()]);
Arrays.sort(values);
// Build the blob
var blob = new ByteArrayOutputStream();
for (var file : values) try {
blob.write(file.data);
} catch (Exception e) { }
return blob.toByteArray();
}
// Encode bundled blob as a .png
static byte[] png(byte[] blob) {
// Calculate the dimensions of the image
int width = (int) Math.ceil(Math.sqrt(blob.length));
int height = (int) Math.ceil((double) blob.length / width);
// Prepare the pixel data
var pixels = new int[width * height];
for (int x = 0; x < blob.length; x++) {
int l = blob[x] & 0xFF;
pixels[x] = 0xFF000000 | l << 16 | l << 8 | l;
}
// Produce a BufferedImage containing the pixels
var img = new BufferedImage(width, height,
BufferedImage.TYPE_BYTE_GRAY);
img.getRaster().setPixels(0, 0, width, height, pixels);
// Encode the image as a PNG byte array
var png = new ByteArrayOutputStream();
try { ImageIO.write(img, "png", png); }
catch (Exception e) { }
return png.toByteArray();
}
// Embed bundle .png into template.html as a data URL
static void template(byte[] png, String bundleName) {
// Encode the PNG as a data URL
String url = "data:image/png;base64," +
Base64.getMimeEncoder().encodeToString(png)
.replaceAll("\\r\\n", "");
try {
// Read template.html into memory
var inStream = new FileInputStream("app/template.html");
String template =
new String(inStream.readAllBytes(), StandardCharsets.UTF_8)
.replace("src=\"\"", "src=\"" + url + "\"")
;
inStream.close();
// Write the output HTML file
var outStream = new FileOutputStream(bundleName + ".html");
outStream.write(template.getBytes(StandardCharsets.UTF_8));
outStream.close();
} catch (Exception e) { throw new RuntimeException(e.getMessage()); }
}
// Determine the filename of the bundle
static String bundleName(String name) {
var calendar = Calendar.getInstance();
return String.format("%s_%04d%02d%02d",
name,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH)
);
}
// Program entry point
public static void main(String[] args) {
// Error checking
if (args.length != 1) {
System.err.println("Usage: Bundle <name>");
return;
}
// Program tasks
String bundleName = bundleName(args[0]);
var files = readFiles(args[0]);
manifest(files, bundleName);
var blob = blob(files);
var png = png(blob);
template(png, bundleName);
}
}

View File

@ -1,308 +0,0 @@
/*
The Bundle.java utility prepends a file manifest to this script before
execution is started. This script runs within the context of an async
function.
*/
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// Bundled file manager
let Bundle = globalThis.Bundle = new class Bundle extends Array {
constructor() {
super();
// Configure instance fields
this.debug =
location.protocol != "file:" && location.hash == "#debug";
this.decoder = new TextDecoder();
this.encoder = new TextEncoder();
this.moduleCall = (... a)=>this.module (... a);
this.resourceCall = (... a)=>this.resource(... a);
// Generate the CRC32 lookup table
this.crcLookup = new Uint32Array(256);
for (let x = 0; x <= 255; x++) {
let l = x;
for (let j = 7; j >= 0; j--)
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
this.crcLookup[x] = l;
}
}
///////////////////////////// Public Methods //////////////////////////////
// Add a file to the bundle
add(name, data) {
this.push(this[name] = new BundledFile(name, data));
}
// Export all bundled resources to a .ZIP file
save() {
let centrals = new Array(this.length);
let locals = new Array(this.length);
let offset = 0;
let size = 0;
// Encode file and directory entries
for (let x = 0; x < this.length; x++) {
let file = this[x];
let sum = this.crc32(file.data);
locals [x] = file.toZipHeader(sum);
centrals[x] = file.toZipHeader(sum, offset);
offset += locals [x].length;
size += centrals[x].length;
}
// Encode end of central directory
let end = [];
this.writeInt(end, 4, 0x06054B50); // Signature
this.writeInt(end, 2, 0); // Disk number
this.writeInt(end, 2, 0); // Central dir start disk
this.writeInt(end, 2, this.length); // # central dir this disk
this.writeInt(end, 2, this.length); // # central dir total
this.writeInt(end, 4, size); // Size of central dir
this.writeInt(end, 4, offset); // Offset of central dir
this.writeInt(end, 2, 0); // .ZIP comment length
// Prompt the user to save the resulting file
let a = document.createElement("a");
a.download = bundleName + ".zip";
a.href = URL.createObjectURL(new Blob(
locals.concat(centrals).concat([Uint8Array.from(end)]),
{ type: "application/zip" }
));
a.style.visibility = "hidden";
document.body.appendChild(a);
a.click();
a.remove();
}
///////////////////////////// Package Methods /////////////////////////////
// Write a byte array into an output buffer
writeBytes(data, bytes) {
//data.push(... bytes);
for (let b of bytes)
data.push(b);
}
// Write an integer into an output buffer
writeInt(data, size, value) {
for (; size > 0; size--, value >>= 8)
data.push(value & 0xFF);
}
// Write a string of text as bytes into an output buffer
writeString(data, text) {
data.push(... this.encoder.encode(text));
}
///////////////////////////// Private Methods /////////////////////////////
// Calculate the CRC32 checksum for a byte array
crc32(data) {
let c = 0xFFFFFFFF;
for (let x = 0; x < data.length; x++)
c = ((c >>> 8) ^ this.crcLookup[(c ^ data[x]) & 0xFF]);
return ~c & 0xFFFFFFFF;
}
}();
///////////////////////////////////////////////////////////////////////////////
// BundledFile //
///////////////////////////////////////////////////////////////////////////////
// Individual bundled file
class BundledFile {
constructor(name, data) {
// Configure instance fields
this.data = data;
this.name = name;
// Resolve the MIME type
this.mime =
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
name.endsWith(".json" ) ? "application/json;charset=UTF-8" :
name.endsWith(".png" ) ? "image/png" :
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".wasm" ) ? "application/wasm" :
name.endsWith(".woff2") ? "font/woff2" :
"application/octet-stream"
;
}
///////////////////////////// Public Methods //////////////////////////////
// Install a font from a bundled resource
async installFont(name) {
if (name === undefined) {
name = "/" + this.name;
name = name.substring(name.lastIndexOf("/") + 1);
}
let ret = new FontFace(name, "url('"+
(Bundle.debug ? this.name : this.toDataURL()) + "'");
await ret.load();
document.fonts.add(ret);
return ret;
}
// Install an image as a CSS icon from a bundled resource
installImage(name, filename) {
document.documentElement.style.setProperty("--" +
name || this.name.replaceAll(/\/\./, "_"),
"url('" + (Bundle.debug ?
filename || this.name : this.toDataURL()) + "')");
return name;
}
// Install a stylesheet from a bundled resource
installStylesheet(enabled) {
let ret = document.createElement("link");
ret.href = Bundle.debug ? this.name : this.toDataURL();
ret.rel = "stylesheet";
ret.type = "text/css";
ret.setEnabled = enabled=>{
if (enabled)
ret.removeAttribute("disabled");
else ret.setAttribute("disabled", "");
};
ret.setEnabled(!!enabled);
document.head.appendChild(ret);
return ret;
}
// Encode the file data as a data URL
toDataURL() {
return "data:" + this.mime + ";base64," +
btoa(String.fromCharCode(...this.data));
}
// Interpret the file's contents as bundled script source data URL
toScript() {
// Process all URL strings prefixed with /**/
let parts = this.toString().split("/**/");
let src = parts.shift();
for (let part of parts) {
let quote = part.indexOf("\"", 1);
// Begin with the path of the current file
let path = this.name.split("/");
path.pop();
// Navigate to the path of the target file
let file = part.substring(1, quote).split("/");
while (file.length > 0) {
let sub = file.shift();
switch (sub) {
case "..": path.pop(); // Fallthrough
case "." : break;
default : path.push(sub);
}
}
// Append the file as a data URL
file = Bundle[path.join("/")];
src += "\"" + file[
file.mime.startsWith("text/javascript") ?
"toScript" : "toDataURL"
]() + "\"" + part.substring(quote + 1);
}
// Encode the transformed source as a data URL
return "data:" + this.mime + ";base64," + btoa(src);
}
// Decode the file data as a UTF-8 string
toString() {
return Bundle.decoder.decode(this.data);
}
///////////////////////////// Package Methods /////////////////////////////
// Produce a .ZIP header for export
toZipHeader(crc32, offset) {
let central = offset !== undefined;
let ret = [];
if (central) {
Bundle.writeInt (ret, 4, 0x02014B50); // Signature
Bundle.writeInt (ret, 2, 20); // Version created by
} else
Bundle.writeInt (ret, 4, 0x04034B50); // Signature
Bundle.writeInt (ret, 2, 20); // Version required
Bundle.writeInt (ret, 2, 0); // Bit flags
Bundle.writeInt (ret, 2, 0); // Compression method
Bundle.writeInt (ret, 2, 0); // Modified time
Bundle.writeInt (ret, 2, 0); // Modified date
Bundle.writeInt (ret, 4, crc32); // Checksum
Bundle.writeInt (ret, 4, this.data.length); // Compressed size
Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size
Bundle.writeInt (ret, 2, this.name.length); // Filename length
Bundle.writeInt (ret, 2, 0); // Extra field length
if (central) {
Bundle.writeInt (ret, 2, 0); // File comment length
Bundle.writeInt (ret, 2, 0); // Disk number start
Bundle.writeInt (ret, 2, 0); // Internal attributes
Bundle.writeInt (ret, 4, 0); // External attributes
Bundle.writeInt (ret, 4, offset); // Relative offset
}
Bundle.writeString (ret, this.name); // Filename
if (!central)
Bundle.writeBytes(ret, this.data); // File data
return Uint8Array.from(ret);
}
}
///////////////////////////////////////////////////////////////////////////////
// Boot Program //
///////////////////////////////////////////////////////////////////////////////
// De-register the boot function
delete globalThis.a;
// Remove the bundle image element from the document
Bundle.src = arguments[0].src;
arguments[0].remove();
// Convert the file manifest into BundledFile objects
let buffer = arguments[1];
let offset = arguments[2] - manifest[0][1];
for (let entry of manifest) {
Bundle.add(entry[0], buffer.subarray(offset, offset + entry[1]));
offset += entry[1];
if (Bundle.length == 1)
offset++; // Skip null delimiter
}
// Begin program operations
import(Bundle.debug ? "./app/main.js" : Bundle["app/main.js"].toScript());

View File

@ -1,448 +0,0 @@
import { Debugger } from /**/"./Debugger.js";
///////////////////////////////////////////////////////////////////////////////
// App //
///////////////////////////////////////////////////////////////////////////////
// Web-based emulator application
class App extends Toolkit {
///////////////////////// Initialization Methods //////////////////////////
constructor(options) {
super({
className: "tk tk-app",
label : "app.title",
role : "application",
tagName : "div",
style : {
display : "flex",
flexDirection: "column"
}
});
// Configure instance fields
options = options || {};
this.debugMode = true;
this.dualSims = false;
this.core = options.core;
this.linkSims = true;
this.locales = {};
this.themes = {};
this.Toolkit = Toolkit;
// Configure themes
if ("themes" in options)
for (let theme of Object.entries(options.themes))
this.addTheme(theme[0], theme[1]);
if ("theme" in options)
this.setTheme(options.theme);
// Configure locales
if ("locales" in options)
for (let locale of options.locales)
this.addLocale(locale);
if ("locale" in options)
this.setLocale(options.locale);
// Configure widget
this.localize(this);
// Not presenting a standalone application
if (!options.standalone)
return;
// Set up standalone widgets
this.initMenuBar();
this.desktop = new Toolkit.Desktop(this,
{ style: { flexGrow: 1 } });
this.add(this.desktop);
// Configure document for presentation
document.body.className = "tk tk-body";
window.addEventListener("resize", e=>
this.element.style.height = window.innerHeight + "px");
window.dispatchEvent(new Event("resize"));
document.body.appendChild(this.element);
// Configure debugger components
this[0] = new Debugger(this, 0, this.core[0]);
this[1] = new Debugger(this, 1, this.core[1]);
// Configure subscription handling
this.subscriptions = {
[this.core[0].sim]: this[0],
[this.core[1].sim]: this[1]
};
this.core.onsubscriptions = e=>this.onSubscriptions(e);
// Temporary config debugging
console.log("Memory keyboard commands:");
console.log(" Ctrl+G: Goto");
console.log("Disassembler keyboard commands:");
console.log(" Ctrl+B: Toggle bytes column");
console.log(" Ctrl+F: Fit columns");
console.log(" Ctrl+G: Goto");
console.log(" F10: Run to next");
console.log(" F11: Single step");
console.log("Call dasm(\"key\", value) in the console " +
"to configure the disassembler:");
console.log(this[0].getDasmConfig());
window.dasm = (key, value)=>{
let config = this[0].getDasmConfig();
if (!key in config || typeof value != typeof config[key])
return;
if (typeof value == "number" && value != 1 && value != 0)
return;
config[key] = value;
this[0].setDasmConfig(config);
this[1].setDasmConfig(config);
return this[0].getDasmConfig();
};
}
// Configure file menu
initFileMenu(menuBar) {
let menu, item;
// Menu
menuBar.add(menu = menuBar.file = new Toolkit.MenuItem(this,
{ text: "app.menu.file._" }));
// Load ROM
menu.add(item = menu.loadROM0 = new Toolkit.MenuItem(this, {
text: "app.menu.file.loadROM"
}));
item.setSubstitution("sim", "");
item.addEventListener("action",
()=>this.promptFile(f=>this.loadROM(0, f)));
menu.add(item = menu.loadROM1 = new Toolkit.MenuItem(this, {
text : "app.menu.file.loadROM",
visible: false
}));
item.setSubstitution("sim", " 2");
item.addEventListener("action",
()=>this.promptFile(f=>this.loadROM(1, f)));
// Debug Mode
menu.add(item = menu.debugMode = new Toolkit.MenuItem(this, {
checked: this.debugMode,
enabled: false,
text : "app.menu.file.debugMode",
type : "check"
}));
item.addEventListener("action", e=>e.component.setChecked(true));
}
// Configure Emulation menu
initEmulationMenu(menuBar) {
let menu, item;
menuBar.add(menu = menuBar.emulation = new Toolkit.MenuItem(this,
{ text: "app.menu.emulation._" }));
menu.add(item = menu.runPause = new Toolkit.MenuItem(this, {
enabled: false,
text : "app.menu.emulation.run"
}));
menu.add(item = menu.reset = new Toolkit.MenuItem(this, {
enabled: false,
text : "app.menu.emulation.reset"
}));
menu.add(item = menu.dualSims = new Toolkit.MenuItem(this, {
checked: this.dualSims,
text : "app.menu.emulation.dualSims",
type : "check"
}));
item.addEventListener("action",
e=>this.setDualSims(e.component.isChecked));
menu.add(item = menu.linkSims = new Toolkit.MenuItem(this, {
checked: this.linkSims,
text : "app.menu.emulation.linkSims",
type : "check",
visible: this.dualSims
}));
item.addEventListener("action",
e=>this.setLinkSims(e.component.isChecked));
}
// Configure Debug menus
initDebugMenu(menuBar, sim) {
let menu, item;
menuBar.add(menu = menuBar["debug" + sim] =
new Toolkit.MenuItem(this, {
text : "app.menu.debug._",
visible: sim == 0 || this.dualSims
}));
menu.setSubstitution("sim",
sim == 1 || this.dualSims ? " " + (sim + 1) : "");
menu.add(item = menu.console = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.console", enabled: false }));
menu.add(item = menu.memory = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.memory" }));
item.addEventListener("action",
()=>this.showWindow(this[sim].memoryWindow));
menu.add(item = menu.cpu = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.cpu" }));
item.addEventListener("action",
()=>this.showWindow(this[sim].cpuWindow));
menu.add(item = menu.breakpoints = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.breakpoints", enabled: false }));
menu.addSeparator();
menu.add(item = menu.palettes = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.palettes", enabled: false }));
menu.add(item = menu.characters = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.characters", enabled: false }));
menu.add(item = menu.bgMaps = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.bgMaps", enabled: false }));
menu.add(item = menu.objects = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.objects", enabled: false }));
menu.add(item = menu.worlds = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.worlds", enabled: false }));
menu.add(item = menu.frameBuffers = new Toolkit.MenuItem(this,
{ text: "app.menu.debug.frameBuffers", enabled: false }));
}
// Configure Theme menu
initThemeMenu(menuBar) {
let menu, item;
menuBar.add(menu = menuBar.theme = new Toolkit.MenuItem(this,
{ text: "app.menu.theme._" }));
menu.add(item = menu.light = new Toolkit.MenuItem(this,
{ text: "app.menu.theme.light" }));
item.addEventListener("action", e=>this.setTheme("light"));
menu.add(item = menu.dark = new Toolkit.MenuItem(this,
{ text: "app.menu.theme.dark" }));
item.addEventListener("action", e=>this.setTheme("dark"));
menu.add(item = menu.virtual = new Toolkit.MenuItem(this,
{ text: "app.menu.theme.virtual" }));
item.addEventListener("action", e=>this.setTheme("virtual"));
}
// Set up the menu bar
initMenuBar() {
let menuBar = this.menuBar = new Toolkit.MenuBar(this,
{ label: "app.menu._" });
this.initFileMenu (menuBar);
this.initEmulationMenu(menuBar);
this.initDebugMenu (menuBar, 0);
this.initDebugMenu (menuBar, 1);
this.initThemeMenu (menuBar);
this.add(menuBar);
}
///////////////////////////// Event Handlers //////////////////////////////
// Subscriptions arrived from the core thread
onSubscriptions(subscriptions) {
for (let sim of Object.entries(subscriptions)) {
let dbg = this.subscriptions[sim[0]];
for (let sub of Object.entries(sim[1])) switch (sub[0]) {
case "proregs": dbg.programRegisters.refresh(sub[1]); break;
case "sysregs": dbg.systemRegisters .refresh(sub[1]); break;
case "dasm" : dbg.disassembler .refresh(sub[1]); break;
case "memory" : dbg.memory .refresh(sub[1]); break;
}
}
}
///////////////////////////// Public Methods //////////////////////////////
// Register a locale JSON
addLocale(locale) {
if (!("id" in locale))
throw "No id field in locale";
this.locales[locale.id] = Toolkit.flatten(locale);
}
// Register a theme stylesheet
addTheme(id, stylesheet) {
this.themes[id] = stylesheet;
}
///////////////////////////// Package Methods /////////////////////////////
// Specify the language for localization management
setLocale(id) {
if (!(id in this.locales)) {
let lang = id.substring(0, 2);
id = "en-US";
for (let key of Object.keys(this.locales)) {
if (key.substring(0, 2) == lang) {
id = key;
break;
}
}
}
super.setLocale(this.locales[id]);
}
// Specify the active color theme
setTheme(key) {
if (!(key in this.themes))
return;
for (let tkey of Object.keys(this.themes))
this.themes[tkey].setEnabled(tkey == key);
}
// Regenerate localized display text
translate() {
if (arguments.length != 0)
return super.translate.apply(this, arguments);
document.title = super.translate("app.title", this);
}
///////////////////////////// Private Methods /////////////////////////////
// Load a ROM for a simulation
async loadROM(index, file) {
// No file was given
if (!file)
return;
// Load the file into memory
try { file = new Uint8Array(await file.arrayBuffer()); }
catch {
alert(this.translate("error.fileRead"));
return;
}
// Validate file size
if (
file.length < 1024 ||
file.length > 0x1000000 ||
(file.length - 1 & file.length) != 0
) {
alert(this.translate("error.romNotVB"));
return;
}
// Load the ROM into the simulation
if (!(await this[index].sim.setROM(file, { refresh: true }))) {
alert(this.translate("error.romNotVB"));
return;
}
// Seek the disassembler to PC
this[index].disassembler.seek(0xFFFFFFF0, true);
}
// Prompt the user to select a file
promptFile(then) {
let file = document.createElement("input");
file.type = "file";
file.addEventListener("input",
e=>file.files[0] && then(file.files[0]));
file.click();
}
// Attempt to run until the next instruction
async runNext(index) {
let two = this.dualSims && this.linkSims;
// Perform the operation
let data = await this.core.runNext(
this[index].sim.sim,
two ? this[index ^ 1].sim.sim : 0, {
refresh: true
});
// Update the disassemblers
this[index].disassembler.pc = data.pc[0];
this[index].disassembler.seek(data.pc[0]);
if (two) {
this[index ^ 1].disassembler.pc = data.pc[1];
this[index ^ 1].disassembler.seek(data.pc[1]);
}
}
// Specify whether dual sims mode is active
setDualSims(dualSims) {
let sub = dualSims ? " 1" : "";
// Configure instance fields
this.dualSims = dualSims = !!dualSims;
// Configure menus
this.menuBar.emulation.dualSims.setChecked(dualSims);
this.menuBar.emulation.linkSims.setVisible(dualSims);
this.menuBar.file.loadROM0.setSubstitution("sim", sub);
this.menuBar.file.loadROM1.setVisible(dualSims);
this.menuBar.debug0.setSubstitution("sim", sub);
this.menuBar.debug1.setVisible(dualSims);
// Configure debuggers
this[0].setDualSims(dualSims);
this[1].setDualSims(dualSims);
this.core.connect(this[0].sim.sim,
dualSims && this.linkSims ? this[1].sim.sim : 0);
}
// Specify whether the sims are connected for communicatinos
setLinkSims(linked) {
linked = !!linked;
// State is not changing
if (linked == this.linkSims)
return;
// Link or un-link the sims
if (this.dualSims)
this.core.connect(this[0].sim.sim, linked ? this[1].sim.sim : 0);
}
// Display a window
showWindow(wnd) {
wnd.setVisible(true);
wnd.focus()
}
// Execute one instruction
async singleStep(index) {
let two = this.dualSims && this.linkSims;
// Perform the operation
let data = await this.core.singleStep(
this[index].sim.sim,
two ? this[index ^ 1].sim.sim : 0, {
refresh: true
});
// Update the disassemblers
this[index].disassembler.pc = data.pc[0];
this[index].disassembler.seek(data.pc[0]);
if (two) {
this[index ^ 1].disassembler.pc = data.pc[1];
this[index ^ 1].disassembler.seek(data.pc[1]);
}
}
}
export { App };

View File

@ -1,106 +0,0 @@
import { Disassembler } from /**/"./Disassembler.js";
///////////////////////////////////////////////////////////////////////////////
// CPU //
///////////////////////////////////////////////////////////////////////////////
// CPU register editor and disassembler
class CPU extends Toolkit.SplitPane {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, sim) {
super(app, {
className: "tk tk-splitpane tk-cpu",
edge : Toolkit.SplitPane.RIGHT,
style : {
position: "relative"
}
});
this.app = app;
this.sim = sim;
this.initDasm();
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
let text = "";
for (let x = 0; x < 16; x++) {
if (x) text += "\n";
let digit = x.toString(16);
text += digit + "\n" + digit.toUpperCase();
}
this.metrics.element.innerText = text;
this.splitter.append(this.metrics.element);
this.setView(1, this.regs = new Toolkit.SplitPane(app, {
className: "tk tk-splitpane",
edge : Toolkit.SplitPane.TOP
}));
this.regs.setView(0, this.sysregs = new RegisterList(this, true ));
this.regs.setView(1, this.proregs = new RegisterList(this, false));
// Adjust split panes to the initial size of the System Registers pane
let resize;
let preshow = e=>this.onPreshow(resize);
resize = new ResizeObserver(preshow);
resize.observe(this.sysregs.viewport);
resize.observe(this.metrics.element);
this.metrics.addEventListener("resize", e=>this.metricsResize());
}
initDasm() {
this.dasm = new Disassembler(this.app, this.sim);
}
///////////////////////////// Event Handlers //////////////////////////////
// Resize handler prior to first visibility
onPreshow(resize) {
this.metricsResize();
// Once the list of registers is visible, stop listening
if (this.isVisible()) {
resize.disconnect();
this.sysregs.view.element.style.display = "grid";
return;
}
// Update the split panes
let sys = this.sysregs.view.element;
let pro = this.proregs.view.element;
this.setValue(
Math.max(sys.scrollWidth, pro.scrollWidth) +
this.sysregs.vertical.getBounds().width
);
this.regs.setValue(
this.sysregs[PSW].expansion.getBounds().bottom -
sys.getBoundingClientRect().top
);
}
}
export { CPU };

View File

@ -1,187 +0,0 @@
import { Disassembler } from /**/"./Disassembler.js";
import { Memory } from /**/"./Memory.js";
import { RegisterList } from /**/"./RegisterList.js";
class Debugger {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, index, sim) {
// Configure instance fields
this.app = app;
this.index = index;
this.sim = sim;
// Configure components
this.disassembler = new Disassembler(this);
this.memory = new Memory (this);
this.programRegisters = new RegisterList(this, false);
this.systemRegisters = new RegisterList(this, true );
// Configure windows
this.initCPUWindow ();
this.initMemoryWindow();
}
// Set up the CPU window
initCPUWindow() {
let app = this.app;
// Produce the window
let wnd = this.cpuWindow = new app.Toolkit.Window(app, {
width : 400,
height : 300,
className : "tk tk-window tk-cpu" + (this.index==0?"":" two"),
closeToolTip: "common.close",
title : "cpu._",
visible : false
});
wnd.setSubstitution("sim", this.index == 1 ? " 2" : "");
wnd.addEventListener("close", ()=>wnd.setVisible(false));
app.desktop.add(wnd);
// Visibility override
let that = this;
let setVisible = wnd.setVisible;
wnd.setVisible = function(visible) {
that.disassembler .setSubscribed(visible);
that.systemRegisters .setSubscribed(visible);
that.programRegisters.setSubscribed(visible);
setVisible.apply(wnd, arguments);
};
// Auto-seek the initial view
let onSeek = ()=>this.disassembler.seek(this.disassembler.pc, true);
Toolkit.addResizeListener(this.disassembler.element, onSeek);
wnd.addEventListener("firstshow", ()=>{
app.desktop.center(wnd);
Toolkit.removeResizeListener(this.disassembler.element, onSeek);
});
// Window splitters
let sptMain = new Toolkit.SplitPane(this.app, {
className: "tk tk-splitpane tk-main",
edge : Toolkit.SplitPane.RIGHT
});
let sptRegs = new Toolkit.SplitPane(this.app, {
className: "tk tk-splitpane tk-registers",
edge : Toolkit.SplitPane.TOP
});
// Configure window splitter initial size
let resize = new ResizeObserver(()=>{
// Measure register lists
let sys = this.systemRegisters .getPreferredSize();
let pro = this.programRegisters.getPreferredSize();
let height = Math.ceil(Math.max(sys.height, pro.height));
let width = Math.ceil(Math.max(sys.width , pro.width )) +
this.systemRegisters.vertical.getBounds().width;
// Configure splitters
if (sptMain.getValue() != width)
sptMain.setValue(width);
if (sptRegs.getValue() != height)
sptRegs.setValue(height);
});
resize.observe(this.programRegisters.view.element);
resize.observe(this.systemRegisters .view.element);
// Stop monitoring splitter size when something receives focus
let onFocus = e=>{
resize.disconnect();
wnd.removeEventListener("focus", onFocus, true);
};
sptMain.addEventListener("focus", onFocus, true);
// Configure window layout
sptMain.setView(0, this.disassembler);
sptMain.setView(1, sptRegs);
sptRegs.setView(0, this.systemRegisters);
sptRegs.setView(1, this.programRegisters);
wnd.append(sptMain);
}
// Set up the Memory window
initMemoryWindow() {
let app = this.app;
// Produce the window
let wnd = this.memoryWindow = new app.Toolkit.Window(app, {
width : 400,
height : 300,
className : "tk tk-window" + (this.index == 0 ? "" : " two"),
closeToolTip: "common.close",
title : "memory._",
visible : false
});
wnd.setSubstitution("sim", this.index == 1 ? " 2" : "");
wnd.addEventListener("close" , ()=>wnd.setVisible(false));
wnd.addEventListener("firstshow", ()=>app.desktop.center(wnd));
app.desktop.add(wnd);
// Visibility override
let that = this;
let setVisible = wnd.setVisible;
wnd.setVisible = function(visible) {
that.memory.setSubscribed(visible);
setVisible.apply(wnd, arguments);
};
// Configure window layout
wnd.append(this.memory);
}
///////////////////////////// Package Methods /////////////////////////////
// Retrieve the disassembler configuraiton
getDasmConfig() {
return this.disassembler.getConfig();
}
// Attempt to run until the next instruction
runNext() {
this.app.runNext(this.index);
}
// Update the disassembler configuration
setDasmConfig(config) {
this.disassembler .setConfig(config);
this.memory .dasmChanged();
this.programRegisters.dasmChanged();
this.systemRegisters .dasmChanged();
}
// Specify whether dual sims mode is active
setDualSims(dualSims) {
// Update substitutions for sim 1
if (this.index == 0) {
let sub = dualSims ? " 1" : "";
this.cpuWindow .setSubstitution("sim", sub);
this.memoryWindow.setSubstitution("sim", sub);
}
// Hide windows for sim 2
else if (!dualSims) {
this.cpuWindow .close(false);
this.memoryWindow.close(false);
}
}
// Execute one instruction
singleStep() {
this.app.singleStep(this.index);
}
}
export { Debugger };

View File

@ -1,958 +0,0 @@
import { Util } from /**/"../app/Util.js";
// Opcode definition
class Opdef {
constructor(format, mnemonic, signExtend) {
this.format = format;
this.mnemonic = mnemonic;
this.signExtend = !!signExtend;
}
}
// Top-level opcode definition lookup table by opcode
let OPDEFS = [
new Opdef(1, "MOV" ), new Opdef(1, "ADD" ), new Opdef(1, "SUB" ),
new Opdef(1, "CMP" ), new Opdef(1, "SHL" ), new Opdef(1, "SHR" ),
new Opdef(1, "JMP" ), new Opdef(1, "SAR" ), new Opdef(1, "MUL" ),
new Opdef(1, "DIV" ), new Opdef(1, "MULU" ), new Opdef(1, "DIVU" ),
new Opdef(1, "OR" ), new Opdef(1, "AND" ), new Opdef(1, "XOR" ),
new Opdef(1, "NOT" ), new Opdef(2, "MOV" ,1), new Opdef(2, "ADD",1),
new Opdef(2, "SETF" ), new Opdef(2, "CMP" ,1), new Opdef(2, "SHL" ),
new Opdef(2, "SHR" ), new Opdef(2, "CLI" ), new Opdef(2, "SAR" ),
new Opdef(2, "TRAP" ), new Opdef(2, "RETI" ), new Opdef(2, "HALT" ),
new Opdef(0, null ), new Opdef(2, "LDSR" ), new Opdef(2, "STSR" ),
new Opdef(2, "SEI" ), new Opdef(2, null ), new Opdef(3, "Bcond"),
new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"),
new Opdef(3, "Bcond"), new Opdef(3, "Bcond" ), new Opdef(3, "Bcond"),
new Opdef(3, "Bcond"), new Opdef(5,"MOVEA",1), new Opdef(5,"ADDI",1),
new Opdef(4, "JR" ), new Opdef(4, "JAL" ), new Opdef(5, "ORI" ),
new Opdef(5, "ANDI" ), new Opdef(5, "XORI" ), new Opdef(5, "MOVHI"),
new Opdef(6, "LD.B" ), new Opdef(6, "LD.H" ), new Opdef(0, null ),
new Opdef(6, "LD.W" ), new Opdef(6, "ST.B" ), new Opdef(6, "ST.H" ),
new Opdef(0, null ), new Opdef(6, "ST.W" ), new Opdef(6, "IN.B" ),
new Opdef(6, "IN.H" ), new Opdef(6, "CAXI" ), new Opdef(6, "IN.W" ),
new Opdef(6, "OUT.B"), new Opdef(6, "OUT.H" ), new Opdef(7, null ),
new Opdef(6, "OUT.W")
];
// Bit string mnemonic lookup table by sub-opcode
let BITSTRINGS = [
"SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD",
null , null , null , null ,
"ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" ,
"ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU"
];
// Floating-point/Nintendo mnemonic lookup table by sub-opcode
let FLOATENDOS = [
"CMPF.S", null , "CVT.WS", "CVT.SW" ,
"ADDF.S", "SUBF.S", "MULF.S", "DIVF.S" ,
"XB" , "XH" , "REV" , "TRNC.SW",
"MPYHW"
];
// Program register names
let PROREGS = { 2: "hp", 3: "sp", 4: "gp", 5: "tp", 31: "lp" };
// System register names
let SYSREGS = [
"EIPC", "EIPSW", "FEPC", "FEPSW",
"ECR" , "PSW" , "PIR" , "TKCW" ,
null , null , null , null ,
null , null , null , null ,
null , null , null , null ,
null , null , null , null ,
"CHCW", "ADTRE", null , null ,
null , null , null , null
];
// Condition mnemonics
let CONDS = [
"V" , ["C" , "L" ], ["E" , "Z" ], "NH",
"N" , "T" , "LT" , "LE",
"NV", ["NC", "NL"], ["NE", "NZ"], "H" ,
"P" , "F" , "GE" , "GT"
];
// Output setting keys
const SETTINGS = [
"bcondMerged", "branchAddress", "condCase", "condCL", "condEZ",
"condNames", "hexCaps", "hexDollar", "hexSuffix", "imm5OtherHex",
"imm5ShiftHex", "imm5TrapHex", "imm16AddiLargeHex", "imm16AddiSmallHex",
"imm16MoveHex", "imm16OtherHex", "jmpBrackets", "memoryLargeHex",
"memorySmallHex", "memoryInside", "mnemonicCaps", "operandReverse",
"proregCaps", "proregNames", "setfMerged", "sysregCaps", "sysregNames"
];
///////////////////////////////////////////////////////////////////////////////
// Line //
///////////////////////////////////////////////////////////////////////////////
// One line of output
class Line {
///////////////////////// Initialization Methods //////////////////////////
constructor(parent, first) {
// Configure instance fields
this.first = first;
this.parent = parent;
// Configure labels
this.lblAddress = this.label("tk-address" , first);
this.lblBytes = this.label("tk-bytes" , first);
this.lblMnemonic = this.label("tk-mnemonic", first);
this.lblOperands = this.label("tk-operands", false);
}
///////////////////////////// Package Methods /////////////////////////////
// Update the elements' display
refresh(row, isPC) {
// The row is not available
if (!row) {
this.lblAddress .innerText = "--------";
this.lblBytes .innerText = "";
this.lblMnemonic.innerText = "---";
this.lblOperands.innerText = "";
}
// Update labels with the disassembled row's contents
else {
this.lblAddress .innerText = row.address;
this.lblBytes .innerText = row.bytes;
this.lblMnemonic.innerText = row.mnemonic;
this.lblOperands.innerText = row.operands;
}
// Update style according to selection
let method = row && isPC ? "add" : "remove";
this.lblAddress .classList[method]("tk-selected");
this.lblBytes .classList[method]("tk-selected");
this.lblMnemonic.classList[method]("tk-selected");
this.lblOperands.classList[method]("tk-selected");
}
// Specify whether the elements on this line are visible
setVisible(visible) {
// Column elements
let columns = [
this.lblAddress,
this.lblBytes,
this.lblMnemonic,
this.lblOperands
];
// Column elements on the first row
if (this.first) {
columns[0] = columns[0].parentNode; // Address
columns[1] = columns[1].parentNode; // Bytes
columns[2] = columns[2].parentNode; // Mnemonic
}
// Column visibility
visible = [
visible, // Address
visible && this.parent.hasBytes, // Bytes
visible, // Mnemonic
visible // Operands
];
// Configure elements
for (let x = 0; x < 4; x++)
columns[x].style.display = visible[x] ? "block" : "none";
}
///////////////////////////// Private Methods /////////////////////////////
// Create a display label
label(className, first) {
// Create the label element
let label = document.createElement("div");
label.className = "tk " + className;
// The label is part of the first row of output
let element = label;
if (first) {
// Create a container element
element = document.createElement("div");
element.append(label);
element.max = 0;
// Ensure the container can always fit the column contents
Toolkit.addResizeListener(element, ()=>{
let width = Math.ceil(label.getBoundingClientRect().width);
if (width <= element.max)
return;
element.max = width;
element.style.minWidth = width + "px";
});
}
// Configure elements
this.parent.view.append(element);
return label;
}
}
///////////////////////////////////////////////////////////////////////////////
// Disassembler //
///////////////////////////////////////////////////////////////////////////////
// Text disassembler for NVC
class Disassembler extends Toolkit.ScrollPane {
///////////////////////// Initialization Methods //////////////////////////
constructor(debug) {
super(debug.app, {
className : "tk tk-scrollpane tk-disassembler",
horizontal: Toolkit.ScrollPane.AS_NEEDED,
focusable : true,
tabStop : true,
tagName : "div",
vertical : Toolkit.ScrollPane.NEVER
});
// Configure instance fields
this.address = Util.u32(0xFFFFFFF0);
this.app = debug.app;
this.columns = [ 0, 0, 0, 0 ];
this.data = [];
this.debug = debug;
this.hasBytes = true;
this.isSubscribed = false;
this.lines = null;
this.pc = this.address;
this.pending = [];
this.rows = [];
this.scroll = 0;
this.sim = debug.sim;
// Default output settings
this.setConfig({
bcondMerged : true,
branchAddress : true,
condCase : false,
condCL : 1,
condEZ : 1,
condNames : true,
hexCaps : true,
hexDollar : false,
hexSuffix : false,
imm5OtherHex : false,
imm5ShiftHex : false,
imm5TrapHex : false,
imm16AddiLargeHex: true,
imm16AddiSmallHex: false,
imm16MoveHex : true,
imm16OtherHex : true,
jmpBrackets : true,
memoryLargeHex : true,
memorySmallHex : false,
memoryInside : false,
mnemonicCaps : true,
operandReverse : false,
proregCaps : false,
proregNames : true,
setfMerged : false,
sysregCaps : true,
sysregNames : true
});
// Configure viewport
this.viewport.classList.add("tk-mono");
// Configure view
let view = document.createElement("div");
view.className = "tk tk-view";
Object.assign(view.style, {
display : "grid",
gridTemplateColumns: "repeat(3, max-content) auto"
});
this.setView(view);
// Font-measuring element
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-metrics tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
this.metrics.element.innerText = "X";
this.append(this.metrics.element);
// First row always exists
this.lines = [ new Line(this, true) ];
// Configure event handlers
Toolkit.addResizeListener(this.viewport, e=>this.onResize(e));
this.addEventListener("keydown", e=>this.onKeyDown (e));
this.addEventListener("wheel" , e=>this.onMouseWheel(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
let tall = this.tall(false);
// Ctrl key is pressed
if (e.ctrlKey) switch (e.key) {
// Toggle bytes column
case "b": case "B":
this.showBytes(!this.hasBytes);
break;
// Fit columns
case "f": case "F":
this.fitColumns();
break;
// Goto
case "g": case "G":
this.promptGoto();
break;
default: return;
}
// Ctrl key is not pressed
else switch (e.key) {
// Navigation
case "ArrowDown" : this.fetch(+1 , true); break;
case "ArrowUp" : this.fetch(-1 , true); break;
case "PageDown" : this.fetch(+tall, true); break;
case "PageUp" : this.fetch(-tall, true); break;
// View control
case "ArrowLeft" : this.horizontal.setValue(
this.horizontal.value - this.horizontal.increment); break;
case "ArrowRight": this.horizontal.setValue(
this.horizontal.value + this.horizontal.increment); break;
// Single step
case "F10":
this.debug.runNext();
break;
// Single step
case "F11":
this.debug.singleStep();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Mouse wheel
onMouseWheel(e) {
// User agent scaling action
if (e.ctrlKey)
return;
// No rotation has occurred
let offset = Math.sign(e.deltaY) * 3;
if (offset == 0)
return;
// Update the display address
this.fetch(offset, true);
}
// Viewport resized
onResize(e) {
let fetch = false;
let tall = this.tall(true);
// Add additional lines to the output
for (let x = 0; x < tall; x++) {
if (x >= this.lines.length) {
fetch = true;
this.lines.push(new Line(this));
}
this.lines[x].setVisible(true);
}
// Remove extra lines from the output
for (let x = tall; x < this.lines.length; x++)
this.lines[x].setVisible(false);
// Configure horizontal scroll bar
if (this.metrics)
this.horizontal.setIncrement(this.metrics.getBounds().width);
// Update the display
if (fetch)
this.fetch(0, true);
else this.refresh();
}
///////////////////////////// Public Methods //////////////////////////////
// Produce disassembly text
disassemble(rows) {
// Produce a deep copy of the input list
let copy = new Array(rows.length);
for (let x = 0; x < rows.length; x++) {
copy[x] = {};
Object.assign(copy[x], rows[x]);
}
rows = copy;
// Process all rows
for (let row of rows) {
row.operands = [];
// Read instruction bits from the bus
let bits0 = row.bytes[1] << 8 | row.bytes[0];
let bits1;
if (row.bytes.length == 4)
bits1 = row.bytes[3] << 8 | row.bytes[2];
// Working variables
let opcode = bits0 >> 10;
let opdef = OPDEFS[opcode];
// Sub-opcode mnemonics
if (row.opcode == 0b011111)
row.mnemonic = BITSTRINGS[bits0 & 31] || "---";
else if (row.opcode == 0b111110)
row.mnemonic = FLOATENDOS[bits1 >> 10 & 63] || "---";
else row.mnemonic = opdef.mnemonic;
// Processing by format
switch (opdef.format) {
case 1: this.formatI (row, bits0 ); break;
case 3: this.formatIII(row, bits0 ); break;
case 4: this.formatIV (row, bits0, bits1); break;
case 6: this.formatVI (row, bits0, bits1); break;
case 7: this.formatVII(row, bits0 ); break;
case 2:
this.formatII(row, bits0, opdef.signExtend); break;
case 5:
this.formatV (row, bits0, bits1, opdef.signExtend);
}
// Format bytes
let text = [];
for (let x = 0; x < row.bytes.length; x++)
text.push(row.bytes[x].toString(16).padStart(2, "0"));
row.bytes = text.join(" ");
// Post-processing
row.address = row.address.toString(16).padStart(8, "0");
if (this.hexCaps) {
row.address = row.address.toUpperCase();
row.bytes = row.bytes .toUpperCase();
}
if (!this.mnemonicCaps)
row.mnemonic = row.mnemonic.toLowerCase();
if (this.operandReverse)
row.operands.reverse();
row.operands = row.operands.join(", ");
}
return rows;
}
// Retrieve all output settings in an object
getConfig() {
let ret = {};
for (let key of SETTINGS)
ret[key] = this[key];
return ret;
}
// Update with disassembly state from the core
refresh(data = 0) {
let bias;
// Scrolling prefresh
if (typeof data == "number")
bias = 16 + data;
// Received data from the core thread
else {
this.data = data.rows;
this.pc = data.pc;
if (this.data.length == 0)
return;
this.address = this.data[0].address;
this.rows = this.disassemble(this.data);
bias = 16 +
(data.scroll === null ? 0 : this.scroll - data.scroll);
}
// Update elements
let count = Math.min(this.tall(true), this.data.length);
for (let y = 0; y < count; y++) {
let index = bias + y;
let line = this.data[index];
let row = this.rows[index];
this.lines[y].refresh(row, line && line.address == this.pc);
}
// Refesh scroll pane
this.update();
}
// Bring an address into view
seek(address, force) {
// Check if the address is already in the view
if (!force) {
let bias = 16;
let tall = this.tall(false);
let count = Math.min(tall, this.data.length);
// The address is currently visible in the output
for (let y = 0; y < count; y++) {
let row = this.data[bias + y];
if (!row || Util.u32(address - row.address) >= row.size)
continue;
// The address is on this row
this.refresh();
return;
}
}
// Place the address at a particular position in the view
this.address = address;
this.fetch(null);
}
// Update output settings
setConfig(config) {
// Update settings
for (let key of SETTINGS)
if (key in config)
this[key] = config[key];
// Regenerate output
this.refresh({
pc : this.pc,
rows : this.data,
scroll: null
});
}
// Subscribe to or unsubscribe from core updates
setSubscribed(subscribed) {
subscribed = !!subscribed;
// Nothing to change
if (subscribed == this.isSubscribed)
return;
// Configure instance fields
this.isSubscribed = subscribed;
// Subscribe to core updates
if (subscribed)
this.fetch(0);
// Unsubscribe from core updates
else this.sim.unsubscribe("dasm");
}
///////////////////////////// Private Methods /////////////////////////////
// Select a condition's name
cond(cond) {
let ret = CONDS[cond];
switch (cond) {
case 1: case 9: return CONDS[cond][this.condCL];
case 2: case 10: return CONDS[cond][this.condEZ];
}
return CONDS[cond];
}
// Retrieve disassembly data from the core
async fetch(scroll, prefresh) {
let row;
// Scrolling relative to the current view
if (scroll) {
if (prefresh)
this.refresh(scroll);
this.scroll = Util.s32(this.scroll + scroll);
row = -scroll;
}
// Jumping to an address directly
else row = scroll === null ? Math.floor(this.tall(false) / 3) + 16 : 0;
// Retrieve data from the core
this.refresh(
await this.sim.disassemble(
this.address,
row,
this.tall(true) + 32,
scroll === null ? null : this.scroll, {
subscribe: this.isSubscribed && "dasm"
})
);
}
// Shrink all columns to their minimum size
fitColumns() {
let line = this.lines[0];
for (let column of [ "lblAddress", "lblBytes", "lblMnemonic" ] ) {
let element = line[column].parentNode;
element.max = 0;
element.style.removeProperty("min-width");
}
}
// Represent a hexadecimal value
hex(value, digits) {
let sign = Util.s32(value) < 0 ? "-" : "";
let ret = Math.abs(Util.u32(value)).toString(16).padStart(digits,"0");
if (this.hexCaps)
ret = ret.toUpperCase();
if (this.hexSuffix)
ret = ("abcdefABCDEF".indexOf(ret[0]) == -1 ? "" : "0") +
ret + "h";
else ret = (this.hexDollar ? "$" : "0x") + ret;
return sign + ret;
}
// Prompt the user to specify a new address
promptGoto() {
// Receive input from the user
let address = prompt(this.app.translate("common.gotoPrompt"));
if (address == null)
return;
// Process the input as an address in hexadecimal
address = parseInt(address, 16);
if (isNaN(address))
return;
// Move the selection and refresh the display
this.seek(Util.u32(address));
}
// Select a program register name
proreg(index) {
let ret = this.proregNames && PROREGS[index] || "r" + index;
return this.proregCaps ? ret.toUpperCase() : ret;
}
// Specify whether or not to show the bytes column
showBytes(show) {
let tall = this.tall(true);
// Configure instance fields
this.hasBytes = show;
// Configure elements
this.view.style.gridTemplateColumns =
"repeat(" + (show ? 3 : 2) + ", max-content) auto";
for (let x = 0; x < tall; x++)
this.lines[x].setVisible(true);
// Measure scroll pane
this.update();
}
// Measure how many rows of output are visible
tall(partial) {
let lineHeight = !this.metrics ? 0 :
Math.ceil(this.metrics.getBounds().height);
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
this.getBounds().height / lineHeight));
}
//////////////////////////// Decoding Methods /////////////////////////////
// Disassemble a Format I instruction
formatI(row, bits0) {
let reg1 = this.proreg(bits0 & 31);
// JMP
if (row.mnemonic == "JMP") {
if (this.jmpBrackets)
reg1 = "[" + reg1 + "]";
row.operands.push(reg1);
}
// Other instructions
else {
let reg2 = this.proreg(bits0 >> 5 & 31);
row.operands.push(reg1, reg2);
}
}
// Disassemble a Format II instruction
formatII(row, bits0, signExtend) {
// Bit-string instructions are zero-operand
if (bits0 >> 10 == 0b011111)
return;
// Processing by mnemonic
switch (row.mnemonic) {
// Zero-operand
case "---" : // Fallthrough
case "CLI" : // Fallthrough
case "HALT": // Fallthrough
case "RETI": // Fallthrough
case "SEI" : return;
// Distinct notation
case "LDSR": return this.ldstsr(row, bits0, true );
case "SETF": return this.setf (row, bits0 );
case "STSR": return this.ldstsr(row, bits0, false);
}
// Retrieve immediate operand
let imm = bits0 & 31;
if (signExtend)
imm = Util.signExtend(bits0, 5);
// TRAP instruction is one-operand
if (row.mnemonic == "TRAP") {
row.operands.push(this.trapHex ?
this.hex(imm, 1) : imm.toString());
return;
}
// Processing by mnemonic
let hex = this.imm5OtherHex;
switch (row.mnemonic) {
case "SAR": // Fallthrough
case "SHL": // Fallthrough
case "SHR": hex = this.imm5ShiftHex;
}
imm = hex ? this.hex(imm, 1) : imm.toString();
// Two-operand instruction
let reg2 = this.proreg(bits0 >> 5 & 31);
row.operands.push(imm, reg2);
}
// Disassemble a Format III instruction
formatIII(row, bits0) {
let cond = this.cond(bits0 >> 9 & 15);
let disp = Util.signExtend(bits0 & 0x1FF, 9);
// Condition merged with mnemonic
if (this.bcondMerged) {
switch (cond) {
case "F": row.mnemonic = "NOP"; return;
case "T": row.mnemonic = "BR" ; break;
default : row.mnemonic = "B" + cond;
}
}
// Condition as operand
else {
if (!this.condCaps)
cond = cond.toLowerCase();
row.operands.push(cond);
}
// Operand as destination address
if (this.branchAddress) {
disp = Util.u32(row.address + disp & 0xFFFFFFFE)
.toString(16).padStart(8, "0");
if (this.hexCaps)
disp = disp.toUpperCase();
row.operands.push(disp);
}
// Operand as displacement
else {
let sign = disp < 0 ? "-" : disp > 0 ? "+" : "";
let rel = this.hex(Math.abs(disp), 1);
row.operands.push(sign + rel);
}
}
// Disassemble a Format IV instruction
formatIV(row, bits0, bits1) {
let disp = Util.signExtend(bits0 << 16 | bits1, 26);
// Operand as destination address
if (this.branchAddress) {
disp = Util.u32(row.address + disp & 0xFFFFFFFE)
.toString(16).padStart(8, "0");
if (this.hexCaps)
disp = disp.toUpperCase();
row.operands.push(disp);
}
// Operand as displacement
else {
let sign = disp < 0 ? "-" : disp > 0 ? "+" : "";
let rel = this.hex(Math.abs(disp), 1);
row.operands.push(sign + rel);
}
}
// Disassemble a Format V instruction
formatV(row, bits0, bits1, signExtend) {
let imm = signExtend ? Util.signExtend(bits1) : bits1;
let reg1 = this.proreg(bits0 & 31);
let reg2 = this.proreg(bits0 >> 5 & 31);
if (
row.mnemonic == "ADDI" ?
Math.abs(imm) <= 256 ?
this.imm16AddiSmallHex :
this.imm16AddiLargeHex
: row.mnemonic == "MOVEA" || row.mnemonic == "MOVHI" ?
this.imm16MoveHex
:
this.imm16OtherHex
) imm = this.hex(imm, 4);
row.operands.push(imm, reg1, reg2);
}
// Disassemble a Format VI instruction
formatVI(row, bits0, bits1) {
let disp = Util.signExtend(bits1);
let reg1 = this.proreg(bits0 & 31);
let reg2 = this.proreg(bits0 >> 5 & 31);
let sign =
disp < 0 ? "-" :
disp == 0 || !this.memoryInside ? "" :
"+";
// Displacement is hexadecimal
disp = Math.abs(disp);
if (disp == 0)
disp = ""
else if (disp <= 256 ? this.memorySmallHex : this.memoryLargeHex)
disp = this.hex(disp, 1);
// Format the displacement figure according to its presentation
disp = this.memoryInside ?
sign == "" ? "" : " " + sign + " " + disp :
sign + disp
;
// Apply operands
row.operands.push(this.memoryInside ?
"[" + reg1 + disp + "]" :
disp + "[" + reg1 + "]",
reg2);
// Swap operands for output and store instructions
switch (row.mnemonic) {
case "OUT.B": case "OUT.H": case "OUT.W":
case "ST.B" : case "ST.H" : case "ST.W" :
row.operands.reverse();
}
}
// Disassemble a Format VII instruction
formatVII(row, bits0) {
let reg1 = this.proreg(bits0 & 31);
let reg2 = this.proreg(bits0 >> 5 & 31);
// Invalid sub-opcode is zero-operand
if (row.mnemonic == "---")
return;
// Processing by mnemonic
switch (row.mnemonic) {
case "XB": // Fallthrough
case "XH": break;
default : row.operands.push(reg1);
}
row.operands.push(reg2);
}
// Format an LDSR or STSR instruction
ldstsr(row, bits0, reverse) {
// System register
let sysreg = bits0 & 31;
sysreg = this.sysregNames && SYSREGS[sysreg] || sysreg.toString();
if (!this.sysregCaps)
sysreg = sysreg.toLowerCase();
// Program register
let reg2 = this.proreg(bits0 >> 5 & 31);
// Operands
row.operands.push(sysreg, reg2);
if (reverse)
row.operands.reverse();
}
// Format a SETF instruction
setf(row, bits0) {
let cond = this.cond (bits0 & 15);
let reg2 = this.proreg(bits0 >> 5 & 31);
// Condition merged with mnemonic
if (!this.bcondMerged) {
row.mnemonic += cond;
}
// Condition as operand
else {
if (!this.condCaps)
cond = cond.toLowerCase();
row.operands.push(cond);
}
row.operands.push(reg2);
}
}
export { Disassembler };

View File

@ -1,517 +0,0 @@
import { Util } from /**/"./Util.js";
// Bus indexes
const MEMORY = 0;
// Text to hex digit conversion
const DIGITS = {
"0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7,
"8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15
};
///////////////////////////////////////////////////////////////////////////////
// Line //
///////////////////////////////////////////////////////////////////////////////
// One line of output
class Line {
///////////////////////// Initialization Methods //////////////////////////
constructor(parent, index) {
// Configure instance fields
this.index = index;
this.parent = parent;
// Address label
this.lblAddress = document.createElement("div");
this.lblAddress.className = "tk tk-address";
parent.view.appendChild(this.lblAddress);
// Byte labels
this.lblBytes = new Array(16);
for (let x = 0; x < 16; x++) {
let lbl = this.lblBytes[x] = document.createElement("div");
lbl.className = "tk tk-byte tk-" + x.toString(16);
parent.view.appendChild(lbl);
}
}
///////////////////////////// Package Methods /////////////////////////////
// Update the elements' display
refresh() {
let bus = this.parent[this.parent.bus];
let address = this.parent.mask(bus.address + this.index * 16);
let data = bus.data;
let dataAddress = bus.dataAddress;
let hexCaps = this.parent.dasm.hexCaps;
let offset =
(this.parent.row(address) - this.parent.row(dataAddress)) * 16;
// Format the line's address
let text = address.toString(16).padStart(8, "0");
if (hexCaps)
text = text.toUpperCase();
this.lblAddress.innerText = text;
// The line's data is not available
if (offset < 0 || offset >= data.length) {
for (let lbl of this.lblBytes) {
lbl.innerText = "--";
lbl.classList.remove("tk-selected");
}
}
// The line's data is available
else for (let x = 0; x < 16; x++, offset++) {
let lbl = this.lblBytes[x];
text = data[offset].toString(16).padStart(2, "0");
// The byte is the current selection
if (Util.u32(address + x) == bus.selection) {
lbl.classList.add("tk-selected");
if (this.parent.digit !== null)
text = this.parent.digit.toString(16);
}
// The byte is not the current selection
else lbl.classList.remove("tk-selected");
// Update the label's text
if (hexCaps)
text = text.toUpperCase();
lbl.innerText = text;
}
}
// Specify whether the elements on this line are visible
setVisible(visible) {
visible = visible ? "block" : "none";
this.lblAddress.style.display = visible;
for (let lbl of this.lblBytes)
lbl.style.display = visible;
}
}
///////////////////////////////////////////////////////////////////////////////
// Memory //
///////////////////////////////////////////////////////////////////////////////
// Memory hex editor
class Memory extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(debug) {
super(debug.app, {
className : "tk tk-memory",
tagName : "div",
style : {
alignItems : "stretch",
display : "grid",
gridTemplateRows: "auto",
position : "relative"
}
});
// Configure instance fields
this.app = debug.app;
this.bus = MEMORY;
this.dasm = debug.disassembler;
this.debug = debug;
this.digit = null;
this.isSubscribed = false;
this.lines = [];
this.sim = debug.sim;
// Initialize bus
this[MEMORY] = {
address : 0x05000000,
data : [],
dataAddress: 0x05000000,
selection : 0x05000000
};
// Configure editor pane
this.editor = new Toolkit.ScrollPane(this.app, {
className : "tk tk-scrollpane tk-editor",
horizontal: Toolkit.ScrollPane.AS_NEEDED,
focusable : true,
tabStop : true,
tagName : "div",
vertical : Toolkit.ScrollPane.NEVER
});
this.append(this.editor);
// Configure view
this.view = document.createElement("div");
this.view.className = "tk tk-view";
Object.assign(this.view.style, {
display : "grid",
gridTemplateColumns: "repeat(17, max-content)"
});
this.editor.setView(this.view);
// Font-measuring element
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-metrics tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
this.metrics.element.innerText = "X";
this.append(this.metrics.element);
// Configure event handlers
Toolkit.addResizeListener(this.editor.viewport, e=>this.onResize(e));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("wheel" , e=>this.onMouseWheel (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Typed a digit
onDigit(digit) {
let bus = this[this.bus];
// Begin an edit
if (this.digit === null) {
this.digit = digit;
this.setSelection(bus.selection, true);
}
// Complete an edit
else {
this.digit = this.digit << 4 | digit;
this.setSelection(bus.selection + 1);
}
}
// Key press
onKeyDown(e) {
let bus = this[this.bus];
let key = e.key;
// A hex digit was entered
if (key.toUpperCase() in DIGITS) {
this.onDigit(DIGITS[key.toUpperCase()]);
key = "digit";
}
// Ctrl key is pressed
if (e.ctrlKey) switch (key) {
// Goto
case "g": case "G":
this.promptGoto();
break;
default: return;
}
// Ctrl key is not pressed
else switch (key) {
// Arrow key navigation
case "ArrowDown" : this.setSelection(bus.selection + 16); break;
case "ArrowLeft" : this.setSelection(bus.selection - 1); break;
case "ArrowRight": this.setSelection(bus.selection + 1); break;
case "ArrowUp" : this.setSelection(bus.selection - 16); break;
// Commit current edit
case "Enter":
case " ":
if (this.digit !== null)
this.setSelection(bus.selection);
break;
// Page key navigation
case "PageDown":
this.setSelection(bus.selection + this.tall(false) * 16);
break;
case "PageUp":
this.setSelection(bus.selection - this.tall(false) * 16);
break;
// Hex digit: already processed
case "digit": break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Mouse wheel
onMouseWheel(e) {
// User agent scaling action
if (e.ctrlKey)
return;
// No rotation has occurred
let offset = Math.sign(e.deltaY) * 48;
if (offset == 0)
return;
// Update the display address
this.fetch(this[this.bus].address + offset, true);
}
// Pointer down
onPointerDown(e) {
// Common handling
this.editor.focus();
e.stopPropagation();
e.preventDefault();
// Not a click action
if (e.button != 0)
return;
// Determine the row that was clicked on
let lineHeight = !this.metrics ? 0 :
Math.max(0, Math.ceil(this.metrics.getBounds().height));
if (lineHeight == 0)
return;
let y = Math.floor(
(e.y - this.view.getBoundingClientRect().top) / lineHeight);
// Determine the column that was clicked on
let columns = this.lines[0].lblBytes;
let bndCur = columns[0].getBoundingClientRect();
if (e.x >= bndCur.left) for (let x = 0; x < 16; x++) {
let bndNext = x == 15 ? null :
columns[x + 1].getBoundingClientRect();
// The current column was clicked: update the selection
if (e.x < (x == 15 ? bndCur.right :
bndCur.right + (bndNext.left - bndCur.right) / 2)) {
this.setSelection(this[this.bus].address + y * 16 + x);
return;
}
// Advance to the next column
bndCur = bndNext;
}
}
// Viewport resized
onResize(e) {
let fetch = false;
let tall = this.tall(true);
// Add additional lines to the output
for (let x = 0; x < tall; x++) {
if (x >= this.lines.length) {
fetch = true;
this.lines.push(new Line(this, x));
}
this.lines[x].setVisible(true);
}
// Remove extra lines from the output
for (let x = tall; x < this.lines.length; x++)
this.lines[x].setVisible(false);
// Configure horizontal scroll bar
if (this.metrics) this.editor.horizontal
.setIncrement(this.metrics.getBounds().width);
// Update the display
if (fetch)
this.fetch(this[this.bus].address, true);
else this.refresh();
}
///////////////////////////// Public Methods //////////////////////////////
// Update with memory state from the core
refresh(data) {
let bus = this[this.bus];
// Update with data from the core thread
if (data) {
bus.data = data.bytes;
bus.dataAddress = data.address;
}
// Update elements
for (let y = 0, tall = this.tall(true); y < tall; y++)
this.lines[y].refresh();
}
// Subscribe to or unsubscribe from core updates
setSubscribed(subscribed) {
subscribed = !!subscribed;
// Nothing to change
if (subscribed == this.isSubscribed)
return;
// Configure instance fields
this.isSubscribed = subscribed;
// Subscribe to core updates
if (subscribed)
this.fetch(this[this.bus].address);
// Unsubscribe from core updates
else this.sim.unsubscribe("memory");
}
///////////////////////////// Package Methods /////////////////////////////
// The disassembler configuration has changed
dasmChanged() {
this.refresh();
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve memory data from the core
async fetch(address, prefresh) {
// Configure instance fields
this[this.bus].address = address = this.mask(address);
// Update the view immediately
if (prefresh)
this.refresh();
// Retrieve data from the core
this.refresh(
await this.sim.read(
address - 16 * 16,
(this.tall(true) + 32) * 16, {
subscribe: this.isSubscribed && "memory"
})
);
}
// Mask an address according to the current bus
mask(address) {
return Util.u32(address);
}
// Prompt the user to specify a new address
promptGoto() {
// Receive input from the user
let address = prompt(this.app.translate("common.gotoPrompt"));
if (address == null)
return;
// Process the input as an address in hexadecimal
address = parseInt(address, 16);
if (isNaN(address))
return;
// The address is not currently visible in the output
let tall = this.tall(false);
if (Util.u32(address - this.address) >= tall * 16)
this.fetch((address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16);
// Move the selection and refresh the display
this.setSelection(address);
}
// Determine which row relative to top the selection is on
row(address) {
let row = address - this[this.bus].address & 0xFFFFFFF0;
row = Util.s32(row);
return row / 16;
}
// Specify which byte is selected
setSelection(address, noCommit) {
let bus = this[this.bus];
let fetch = false;
// Commit a pending data entry
if (!noCommit && this.digit !== null) {
this.write(this.digit);
this.digit = null;
fetch = true;
}
// Configure instance fields
bus.selection = address = this.mask(address);
// Working variables
let row = this.row(address);
// The new address is above the top line of output
if (row < 0) {
this.fetch(bus.address + row * 16 & 0xFFFFFFF0, true);
return;
}
// The new address is below the bottom line of output
let tall = this.tall(false);
if (row >= tall) {
this.fetch(address - tall * 16 + 16 & 0xFFFFFFF0, true);
return;
}
// Update the display
if (fetch)
this.fetch(bus.address, true);
else this.refresh();
}
// Measure how many rows of output are visible
tall(partial) {
let lineHeight = !this.metrics ? 0 :
Math.ceil(this.metrics.getBounds().height);
return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"](
this.editor.getBounds().height / lineHeight));
}
// Write a value to the core thread
write(value) {
let bus = this[this.bus];
let offset = (this.row(bus.selection) + 16) * 16;
if (offset < bus.data.length)
bus.data[offset | bus.selection & 15] = value;
this.sim.write(
bus.selection,
Uint8Array.from([ value ]), {
refresh: true
});
}
}
export { Memory };

View File

@ -1,889 +0,0 @@
import { Util } from /**/"./Util.js";
// Value types
const HEX = 0;
const SIGNED = 1;
const UNSIGNED = 2;
const FLOAT = 3;
// System register indexes
const ADTRE = 25;
const CHCW = 24;
const ECR = 4;
const EIPC = 0;
const EIPSW = 1;
const FEPC = 2;
const FEPSW = 3;
const PC = -1;
const PIR = 6;
const PSW = 5;
const TKCW = 7;
// Program register names
const PROREGS = {
[ 2]: "hp",
[ 3]: "sp",
[ 4]: "gp",
[ 5]: "tp",
[31]: "lp"
};
// System register names
const SYSREGS = {
[ADTRE]: "ADTRE",
[CHCW ]: "CHCW",
[ECR ]: "ECR",
[EIPC ]: "EIPC",
[EIPSW]: "EIPSW",
[FEPC ]: "FEPC",
[FEPSW]: "FEPSW",
[PC ]: "PC",
[PIR ]: "PIR",
[PSW ]: "PSW",
[TKCW ]: "TKCW",
[29 ]: "29",
[30 ]: "30",
[31 ]: "31"
};
// Expansion control types
const BIT = 0;
const INT = 1;
// Produce a template object for register expansion controls
function ctrl(name, shift, size, disabled) {
return {
disabled: !!disabled,
name : name,
shift : shift,
size : size
};
}
// Program register epansion controls
const EXP_PROGRAM = [
ctrl("cpu.hex" , true , HEX ),
ctrl("cpu.signed" , false, SIGNED ),
ctrl("cpu.unsigned", false, UNSIGNED),
ctrl("cpu.float" , false, FLOAT )
];
// CHCW expansion controls
const EXP_CHCW = [
ctrl("ICE", 1, 1)
];
// ECR expansion controls
const EXP_ECR = [
ctrl("FECC", 16, 16),
ctrl("EICC", 0, 16)
];
// PIR expansion controls
const EXP_PIR = [
ctrl("PT", 0, 16, true)
];
// PSW expansion controls
const EXP_PSW = [
ctrl("CY", 3, 1), ctrl("FRO", 9, 1),
ctrl("OV", 2, 1), ctrl("FIV", 8, 1),
ctrl("S" , 1, 1), ctrl("FZD", 7, 1),
ctrl("Z" , 0, 1), ctrl("FOV", 6, 1),
ctrl("NP", 15, 1), ctrl("FUD", 5, 1),
ctrl("EP", 14, 1), ctrl("FPR", 4, 1),
ctrl("ID", 12, 1), ctrl("I" , 16, 4),
ctrl("AE", 13, 1)
];
// TKCW expansion controls
const EXP_TKCW = [
ctrl("FIT", 7, 1, true), ctrl("FUT", 4, 1, true),
ctrl("FZT", 6, 1, true), ctrl("FPT", 3, 1, true),
ctrl("FVT", 5, 1, true), ctrl("OTM", 8, 1, true),
ctrl("RDI", 2, 1, true), ctrl("RD" , 0, 2, true)
];
///////////////////////////////////////////////////////////////////////////////
// Register //
///////////////////////////////////////////////////////////////////////////////
// One register within a register list
class Register {
///////////////////////// Initialization Methods //////////////////////////
constructor(list, index, andMask, orMask) {
// Configure instance fields
this.andMask = andMask;
this.app = list.app;
this.controls = [];
this.dasm = list.dasm;
this.format = HEX;
this.index = index;
this.isExpanded = null;
this.list = list;
this.metrics = { width: 0, height: 0 };
this.orMask = orMask;
this.sim = list.sim;
this.system = list.system;
this.value = 0x00000000;
// Establish elements
let row = document.createElement("tr");
let cell;
list.view.append(row);
// Processing by type
this[this.system ? "initSystem" : "initProgram"]();
// Expansion button
this.btnExpand = new Toolkit.Component(this.app, {
className: "tk tk-expand tk-mono",
tagName : "div"
});
row .append(cell = document.createElement("td"));
cell.className = "tk";
cell.style.width = "1px";
cell.append(this.btnExpand.element);
// Name label
this.lblName = document.createElement("div");
Object.assign(this.lblName, {
className: "tk tk-name",
id : Toolkit.id(),
innerText: this.dasm.sysregCaps?this.name:this.name.toLowerCase()
});
this.lblName.style.userSelect = "none";
row .append(cell = document.createElement("td"));
cell.className = "tk";
cell.append(this.lblName);
// Value text box
this.txtValue = new Toolkit.TextBox(this.app, {
className: "tk tk-textbox tk-mono",
maxLength: 8
});
this.txtValue.setAttribute("aria-labelledby", this.lblName.id);
this.txtValue.setAttribute("digits", "8");
this.txtValue.addEventListener("action", e=>this.onValue());
row .append(cell = document.createElement("td"));
Object.assign(cell.style, {
textAlign: "right",
width : "1px"
});
cell.className = "tk";
cell.append(this.txtValue.element);
// Expansion area
if (this.expansion != null)
this.list.view.append(this.expansion);
// Enable expansion function
if (this.expansion != null) {
let key = e=>this.expandKeyDown (e);
let pointer = e=>this.expandPointerDown(e);
this.btnExpand.setAttribute("aria-controls", this.expansion.id);
this.btnExpand.setAttribute("aria-labelledby", this.lblName.id);
this.btnExpand.setAttribute("role", "button");
this.btnExpand.setAttribute("tabindex", "0");
this.btnExpand.addEventListener("keydown" , key );
this.btnExpand.addEventListener("pointerdown", pointer);
this.lblName .addEventListener("pointerdown", pointer);
this.setExpanded(this.system && this.index == PSW);
}
// Expansion function is unavailable
else this.btnExpand.setAttribute("aria-hidden", "true");
}
// Set up a program register
initProgram() {
this.name = PROREGS[this.index] || "r" + this.index.toString();
this.initExpansion(EXP_PROGRAM);
}
// Set up a system register
initSystem() {
this.name = SYSREGS[this.index] || this.index.toString();
switch (this.index) {
case CHCW :
this.initExpansion(EXP_CHCW); break;
case ECR :
this.initExpansion(EXP_ECR ); break;
case EIPSW: case FEPSW: case PSW:
this.initExpansion(EXP_PSW ); break;
case PIR :
this.initExpansion(EXP_PIR ); break;
case TKCW :
this.initExpansion(EXP_TKCW); break;
}
}
// Initialize expansion controls
initExpansion(controls) {
let two = this.index == ECR || this.index == PIR;
// Establish expansion element
let exp = this.expansion = document.createElement("tr");
exp.contents = new Toolkit.Component(this.app, {
className: "tk tk-expansion",
id : Toolkit.id(),
tagName : "div",
style : {
display : "grid",
gridTemplateColumns:
this.system ? "repeat(2, max-content)" : "max-content"
}
});
let cell = document.createElement("td");
cell.className = "tk";
cell.colSpan = "3";
cell.append(exp.contents.element);
exp.append(cell);
exp = exp.contents;
// Produce program register controls
if (!this.system) {
let group = new Toolkit.Group();
exp.append(group);
// Process all controls
for (let template of controls) {
// Create control
let ctrl = new Toolkit.Radio(this.app, {
group : group,
selected: template.shift,
text : template.name
});
ctrl.format = template.size;
// Configure event handler
ctrl.addEventListener("action",
e=>this.setFormat(e.component.format));
// Add the control to the element
let box = document.createElement("div");
box.append(ctrl.element);
exp.append(box);
}
return;
}
// Process all control templates
for (let template of controls) {
let box, ctrl;
// Not using an inner two-column layout
if (!two)
exp.append(box = document.createElement("div"));
// Bit check box
if (template.size == 1) {
box.classList.add("tk-bit");
// Create control
ctrl = new Toolkit.CheckBox(this.app, {
text : "name",
substitutions: { name: template.name }
});
ctrl.mask = 1 << template.shift;
box.append(ctrl.element);
// Disable control
if (template.disabled)
ctrl.setEnabled(false);
// Configure event handler
ctrl.addEventListener("action", e=>this.onBit(e.component));
}
// Number text box
else {
if (!two)
box.classList.add("tk-number");
// Create label
let label = document.createElement("label");
Object.assign(label, {
className: "tk tk-label",
innerText: template.name,
});
if (!two) Object.assign(box.style, {
columnGap : "2px",
display : "grid",
gridTemplateColumns: "max-content auto"
});
(two ? exp : box).append(label);
// Create control
ctrl = new Toolkit.TextBox(this.app, {
id : Toolkit.id(),
style: { height: "1em" }
});
label.htmlFor = ctrl.id;
(two ? exp : box).append(ctrl.element);
// Control is a hex field
if (template.size == 16) {
ctrl.element.classList.add("tk-mono");
ctrl.setAttribute("digits", 4);
ctrl.setMaxLength(4);
}
// Disable control
if (template.disabled) {
ctrl.setEnabled(false);
(two ? label : box).setAttribute("disabled", "true");
}
// Configure event handler
ctrl.addEventListener("action", e=>this.onNumber(e.component));
}
Object.assign(ctrl, template);
this.controls.push(ctrl);
}
}
///////////////////////////// Event Handlers //////////////////////////////
// Expand button key press
expandKeyDown(e) {
// Processing by key
switch (e.key) {
case "Enter":
case " ":
this.setExpanded(!this.isExpanded);
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Expand button pointer down
expandPointerDown(e) {
// Focus management
this.btnExpand.focus();
// Error checking
if (e.button != 0)
return;
// Configure event
e.stopPropagation();
e.preventDefault();
// Configure expansion area
this.setExpanded(!this.isExpanded);
}
// Expansion bit check box
onBit(ctrl) {
this.setValue(ctrl.isSelected ?
this.value | ctrl.mask :
this.value & Util.u32(~ctrl.mask)
);
}
// Expansion number text box
onNumber(ctrl) {
let mask = (1 << ctrl.size) - 1 << ctrl.shift;
let value = parseInt(ctrl.getText(), ctrl.size == 16 ? 16 : 10);
this.setValue(isNaN(value) ? this.value :
this.value & Util.u32(~mask) | value << ctrl.shift & mask);
}
// Register value
onValue() {
let text = this.txtValue.getText();
let value;
// Processing by type
switch (this.format) {
// Unsigned hexadecimal
case HEX:
value = parseInt(text, 16);
break;
// Decimal
case SIGNED:
case UNSIGNED:
value = parseInt(text);
break;
// Float
case FLOAT:
value = parseFloat(text);
if (isNaN(value))
break;
value = Util.fromF32(value);
break;
}
// Assign the new value
this.setValue(isNaN(value) ? this.value : value);
}
///////////////////////////// Package Methods /////////////////////////////
// Disassembler settings have been updated
dasmChanged() {
let dasm = this.list.dasm;
let name = this.name;
// Program register name
if (!this.system) {
if (!dasm.proregNames)
name = "r" + this.index.toString();
if (dasm.proregCaps)
name = name.toUpperCase();
}
// System register name
else {
if (!dasm.sysregCaps)
name = name.toLowerCase();
}
// Common processing
this.lblName.innerText = name;
this.refresh(this.value);
}
// Update the value returned from the core
refresh(value) {
let text;
// Configure instance fields
this.value = value = Util.u32(value);
// Value text box
switch (this.format) {
// Unsigned hexadecimal
case HEX:
text = value.toString(16).padStart(8, "0");
if (this.dasm.hexCaps)
text = text.toUpperCase();
break;
// Signed decimal
case SIGNED:
text = Util.s32(value).toString();
break;
// Unsigned decial
case UNSIGNED:
text = Util.u32(value).toString();
break;
// Float
case FLOAT:
if ((value & 0x7F800000) != 0x7F800000) {
text = Util.toF32(value).toFixed(5).replace(/0+$/, "");
if (text.endsWith("."))
text += "0";
} else text = "NaN";
break;
}
this.txtValue.setText(text);
// No further processing for program registers
if (!this.system)
return;
// Process all expansion controls
for (let ctrl of this.controls) {
// Bit check box
if (ctrl.size == 1) {
ctrl.setSelected(value & ctrl.mask);
continue;
}
// Integer text box
text = value >> ctrl.shift & (1 << ctrl.size) - 1;
text = ctrl.size != 16 ? text.toString() :
text.toString(16).padStart(4, "0");
if (this.dasm.hexCaps)
text = text.toUpperCase();
ctrl.setText(text);
}
}
// Specify whether the expansion area is visible
setExpanded(expanded) {
expanded = !!expanded;
// Error checking
if (this.expansion == null || expanded === this.isExpanded)
return;
// Configure instance fields
this.isExpanded = expanded;
// Configure elements
let key = expanded ? "common.collapse" : "common.expand";
this.btnExpand.setAttribute("aria-expanded", expanded);
this.btnExpand.setToolTip(key);
this.expansion.style.display =
expanded ? "table-row" : "none";
}
// Specify the font metrics
setMetrics(width, height) {
// Configure instance fields
this.metrics = { width: width, height: height };
// Height
height += "px";
this.txtValue.element.style.height = height;
for (let ctrl of this.controls.filter(c=>c.size > 1))
ctrl.element.style.height = height;
// Hexadecimal formatting
if (this.format == HEX) {
this.txtValue.element.style.width = (width * 8) + "px";
this.txtValue.setMaxLength(8);
}
// Decimal formatting
else {
this.txtValue.element.style.removeProperty("width");
this.txtValue.setMaxLength(null);
}
// Expansion text boxes
for (let box of this.controls.filter(c=>c.size > 1)) {
box.element.style.height = height;
if (box.size == 16)
box.element.style.width = (width * 4) + "px";
}
}
///////////////////////////// Private Methods /////////////////////////////
// Specify the formatting type of the register value
setFormat(format) {
if (format == this.format)
return;
this.format = format;
this.txtValue.element
.classList[format == HEX ? "add" : "remove"]("tk-mono");
this.setMetrics(this.metrics.width, this.metrics.height);
this.refresh(this.value);
}
// Specify a new value for the register
async setValue(value) {
// Update the display with the new value immediately
value = Util.u32(value & this.andMask | this.orMask);
let matched = value == this.value;
this.refresh(value);
if (matched)
return;
// Update the new value in the core
let options = { refresh: true };
this.refresh(await (
!this.system ?
this.sim.setProgramRegister(this.index, value, options) :
this.index == PC ?
this.sim.setProgramCounter ( value, options) :
this.sim.setSystemRegister (this.index, value, options)
));
}
}
///////////////////////////////////////////////////////////////////////////////
// RegisterList //
///////////////////////////////////////////////////////////////////////////////
// Scrolling list of registers
class RegisterList extends Toolkit.ScrollPane {
///////////////////////// Initialization Methods //////////////////////////
constructor(debug, system) {
super(debug.app, {
className: "tk tk-scrollpane tk-reglist " +
(system ? "tk-system" : "tk-program"),
vertical : Toolkit.ScrollPane.ALWAYS
});
// Configure instance fields
this.app = debug.app;
this.dasm = debug.disassembler;
this.method = system?"getSystemRegisters":"getProgramRegisters";
this.registers = [];
this.sim = debug.sim;
this.subscription = system ? "sysregs" : "proregs";
this.system = system;
// Configure view element
this.setView(new Toolkit.Component(debug.app, {
className: "tk tk-list",
tagName : "table",
style : {
width: "100%"
}
}));
// Font-measuring element
let text = "";
for (let x = 0; x < 16; x++) {
if (x != 0) text += "\n";
let digit = x.toString(16);
text += digit + "\n" + digit.toUpperCase();
}
this.metrics = new Toolkit.Component(this.app, {
className: "tk tk-mono",
tagName : "div",
style : {
position : "absolute",
visibility: "hidden"
}
});
this.metrics.element.innerText = text;
this.metrics.addEventListener("resize", e=>this.onMetrics());
this.viewport.append(this.metrics.element);
// Processing by type
this[system ? "initSystem" : "initProgram"]();
// Configure component
this.addEventListener("keydown", e=>this.onKeyDown (e));
this.addEventListener("wheel" , e=>this.onMouseWheel(e));
}
// Initialize a list of program registers
initProgram() {
this.add(new Register(this, 0, 0x00000000, 0x00000000));
for (let x = 1; x < 32; x++)
this.add(new Register(this, x, 0xFFFFFFFF, 0x00000000));
}
// Initialie a list of system registers
initSystem() {
this.add(new Register(this, PC , 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, PSW , 0x000FF3FF, 0x00000000));
this.add(new Register(this, ADTRE, 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, CHCW , 0x00000002, 0x00000000));
this.add(new Register(this, ECR , 0xFFFFFFFF, 0x00000000));
this.add(new Register(this, EIPC , 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, EIPSW, 0x000FF3FF, 0x00000000));
this.add(new Register(this, FEPC , 0xFFFFFFFE, 0x00000000));
this.add(new Register(this, FEPSW, 0x000FF3FF, 0x00000000));
this.add(new Register(this, PIR , 0x00000000, 0x00005346));
this.add(new Register(this, TKCW , 0x00000000, 0x000000E0));
this.add(new Register(this, 29 , 0xFFFFFFFF, 0x00000000));
this.add(new Register(this, 30 , 0x00000000, 0x00000004));
this.add(new Register(this, 31 , 0xFFFFFFFF, 0x00000000));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "ArrowDown":
this.vertical.setValue(this.vertical.value +
this.vertical.increment);
break;
case "ArrowLeft":
this.horizontal.setValue(this.horizontal.value -
this.horizontal.increment);
break;
case "ArrowRight":
this.horizontal.setValue(this.horizontal.value +
this.horizontal.increment);
break;
case "ArrowUp":
this.vertical.setValue(this.vertical.value -
this.vertical.increment);
break;
case "PageDown":
this.vertical.setValue(this.vertical.value +
this.vertical.extent);
break;
case "PageUp":
this.vertical.setValue(this.vertical.value -
this.vertical.extent);
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Metrics element resized
onMetrics() {
// Error checking
if (!this.metrics)
return;
// Measure the dimensions of one hex character
let bounds = this.metrics.getBounds();
if (bounds.height <= 0)
return;
let width = Math.ceil(bounds.width);
let height = Math.ceil(bounds.height / 32);
// Resize all text boxes
for (let reg of this.registers)
reg.setMetrics(width, height);
// Update scroll bars
this.horizontal.setIncrement(height);
this.vertical .setIncrement(height);
}
// Mouse wheel
onMouseWheel(e) {
// User agent scaling action
if (e.ctrlKey)
return;
// No rotation has occurred
let offset = Math.sign(e.deltaY) * 3;
if (offset == 0)
return;
// Update the display address
this.vertical.setValue(this.vertical.value +
this.vertical.increment * offset);
}
///////////////////////////// Public Methods //////////////////////////////
// Update with CPU state from the core
refresh(registers) {
// System registers
if (this.system) {
for (let reg of Object.entries(SYSREGS))
this[reg[0]].refresh(registers[reg[1].toLowerCase()]);
}
// Program registers
else for (let x = 0; x < 32; x++)
this[x].refresh(registers[x]);
}
// Subscribe to or unsubscribe from core updates
setSubscribed(subscribed) {
subscribed = !!subscribed;
// Nothing to change
if (subscribed == this.isSubscribed)
return;
// Configure instance fields
this.isSubscribed = subscribed;
// Subscribe to core updates
if (subscribed)
this.fetch();
// Unsubscribe from core updates
else this.sim.unsubscribe(this.subscription);
}
///////////////////////////// Package Methods /////////////////////////////
// Disassembler settings have been updated
dasmChanged() {
for (let reg of this.registers)
reg.dasmChanged();
}
// Determine the initial size of the register list
getPreferredSize() {
let ret = {
height: 0,
width : 0
};
// Error checking
if (!this.view)
return ret;
// Measure the view element
ret.width = this.view.element.scrollWidth;
// Locate the bottom of PSW
if (this.system && this[PSW].expansion) {
ret.height = this[PSW].expansion.getBoundingClientRect().bottom -
this.view.getBounds().top;
}
return ret;
}
///////////////////////////// Private Methods /////////////////////////////
// Add a register to the list
add(reg) {
this[reg.index] = reg;
this.registers.push(reg);
}
// Retrieve CPU state from the core
async fetch() {
this.refresh(
await this.sim[this.method]({
subscribe: this.isSubscribed && this.subscription
})
);
}
}
export { RegisterList };

View File

@ -1,44 +0,0 @@
let F32 = new Float32Array( 1);
let S32 = new Int32Array (F32.buffer, 0, 1);
let U32 = new Uint32Array (F32.buffer, 0, 1);
// Interpret a floating short as a 32-bit integer
function fromF32(x) {
F32[0] = x;
return S32[0];
}
// Interpret a 32-bit integer as a floating short
function toF32(x) {
S32[0] = x;
return F32[0];
}
// Represent a value as a signed 32-bit integer
function s32(x) {
S32[0] = x;
return S32[0];
}
// Sign-extend a value with a given number of bits
function signExtend(value, bits) {
bits = 32 - bits;
S32[0] = value << bits;
return S32[0] >> bits;
}
// Represent a value as an unsigned 32-bit integer
function u32(x) {
U32[0] = x;
return U32[0];
}
export let Util = {
fromF32 : fromF32,
toF32 : toF32,
s32 : s32,
signExtend: signExtend,
u32 : u32
};

View File

@ -1,196 +0,0 @@
import { Sim } from /**/"./Sim.js";
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
let RESTRICT = {};
let WASM_URL = url(/**/"./core.wasm" );
let WORKER_URL = url(/**/"./CoreWorker.js");
///////////////////////////////////////////////////////////////////////////////
// Core //
///////////////////////////////////////////////////////////////////////////////
// Environment manager for simulated Virtual Boys
class Core {
//////////////////////////////// Constants ////////////////////////////////
// States
static IDLE = 0;
static RUNNING = 1;
///////////////////////////// Static Methods //////////////////////////////
// Create a new instance of Core
static create(options) {
return new Core(RESTRICT).init(options);
}
///////////////////////// Initialization Methods //////////////////////////
// Stub constructor
constructor(restrict) {
if (restrict != RESTRICT) {
throw "Cannot instantiate Core directly. " +
"Use Core.create() instead.";
}
}
// Substitute constructor
async init(options = {}) {
// Configure instance fields
this.length = 0;
this.onsubscriptions = null;
this.resolutions = [];
this.state = Core.IDLE;
this.worker = new Worker(WORKER_URL);
this.worker.onmessage = e=>this.onMessage(e.data);
// Issue a create command
if ("sims" in options)
await this.create(options.sims, WASM_URL);
// Only initialize the WebAssembly module
else this.send("init", false, { wasm: WASM_URL });
return this;
}
///////////////////////////// Event Handlers //////////////////////////////
// Worker message received
onMessage(data) {
// Process a promised response
if ("response" in data)
this.resolutions.shift()(data.response);
// Process subscriptions
if (this.onsubscriptions && data.subscriptions)
this.onsubscriptions(data.subscriptions);
}
///////////////////////////// Public Methods //////////////////////////////
// Associate two simulations as peers, or remove an association
connect(a, b, options = {}) {
return this.send({
command: "connect",
respond: !("respond" in options) || !!options.respond,
sims : [ a, b ]
});
}
// Create and initialize new simulations
async create(sims, wasm) {
let numSims = sims===undefined ? 1 : Math.max(0, parseInt(sims) || 0);
// Execute the command in the core thread
let response = await this.send({
command: "create",
sims : numSims,
wasm : wasm
});
// Process the core thread's response
let ret = [];
for (let x = 0; x < numSims; x++, this.length++)
ret.push(this[this.length] =
new Sim(this, response[x], this.length));
return sims === undefined ? ret[0] : ret;
}
// Delete a simulation
destroy(sim, options = {}) {
// Configure simulation
sim = this[sim] || sim;
if (sim.core != this)
return;
let ptr = sim.destroy();
// State management
for (let x = sim.index + 1; x < this.length; x++)
(this[x - 1] = this[x]).index--;
delete this[--this.length];
// Execute the command on the core thread
return this.send({
command: "destroy",
respond: !("respond" in options) || !!options.respond,
sim : ptr
});
}
// Attempt to run until the next instruction
runNext(a, b, options = {}) {
return this.send({
command: "runNext",
refresh: !!options.refresh,
respond: !("respond" in options) || !!options.respond,
sims : [ a, b ]
});
}
// Execute one instruction
singleStep(a, b, options = {}) {
return this.send({
command: "singleStep",
refresh: !!options.refresh,
respond: !("respond" in options) || !!options.respond,
sims : [ a, b ]
});
}
// Unsubscribe from frame data
unsubscribe(key, sim = 0) {
this.send({
command: "unsubscribe",
key : key,
respond: false,
sim : sim
});
}
///////////////////////////// Private Methods /////////////////////////////
// Send a message to the Worker
send(data = {}, transfers = []) {
// Create the message object
Object.assign(data, {
respond: !("respond" in data) || !!data.respond,
run : !("run" in data) || !!data.run
});
// Do not wait on a response
if (!data.respond)
this.worker.postMessage(data, transfers);
// Wait for the response to come back
else return new Promise((resolve, reject)=>{
this.resolutions.push(response=>resolve(response));
this.worker.postMessage(data, transfers);
});
}
}
///////////////////////////////////////////////////////////////////////////////
export { Core };

View File

@ -1,377 +0,0 @@
"use strict";
// Un-sign a 32-bit integer
// Emscripten is sign-extending uint32_t and Firefox can't import in Workers
let u32 = (()=>{
let U32 = new Uint32Array(1);
return x=>{ U32[0] = x; return U32[0]; };
})();
///////////////////////////////////////////////////////////////////////////////
// CoreWorker //
///////////////////////////////////////////////////////////////////////////////
// Thread manager for Core commands
new class CoreWorker {
///////////////////////// Initialization Methods //////////////////////////
// Stub constructor
constructor() {
onmessage = async e=>{
await this.init(e.data.wasm);
onmessage = e=>this.onCommand(e.data, false);
onmessage(e);
};
}
// Substitute constructor
async init(wasm) {
// Load the WebAssembly module
let imports = {
env: { emscripten_notify_memory_growth: ()=>this.onMemory() }
};
this.wasm = await (typeof wasm == "string" ?
WebAssembly.instantiateStreaming(fetch(wasm), imports) :
WebAssembly.instantiate ( wasm , imports)
);
// Configure instance fields
this.api = this.wasm.instance.exports;
this.frameData = null;
this.isRunning = false;
this.memory = this.api.memory.buffer;
this.ptrSize = this.api.PointerSize();
this.ptrType = this.ptrSize == 4 ? Uint32Array : Uint64Array;
this.subscriptions = {};
}
///////////////////////////// Event Handlers //////////////////////////////
// Message from audio thread
onAudio(frames) {
}
// Message from main thread
onCommand(data) {
// Subscribe to the command
if (data.subscribe) {
let sub = data.sim || 0;
sub = this.subscriptions[sub] || (this.subscriptions[sub] = {});
sub = sub[data.subscribe] = {};
Object.assign(sub, data);
delete sub.promised;
delete sub.run;
delete sub.subscribe;
}
// Execute the command
if (data.run)
this[data.command](data);
// Process all subscriptions to refresh any debugging interfaces
if (data.refresh)
this.doSubscriptions(data.sim ? [ data.sim ] : data.sims);
// Reply to the main thread
if (data.respond) {
postMessage({
response: data.response
}, data.transfers);
}
}
// Memory growth
onMemory() {
this.memory = this.api.memory.buffer;
}
//////////////////////////////// Commands /////////////////////////////////
// Associate two simulations as peers, or remove an association
connect(data) {
this.api.vbConnect(data.sims[0], data.sims[1]);
}
// Allocate and initialize a new simulation
create(data) {
let ptr = this.api.Create(data.sims);
data.response = new this.ptrType(this.memory, ptr, data.sims).slice();
data.transfers = [ data.response.buffer ];
this.api.Free(ptr);
}
// Delete a simulation
destroy(data) {
this.api.Destroy(data.sim);
}
// Locate instructions for disassembly
disassemble(data) {
let decode; // Address of next row
let index; // Index in list of next row
let rows = new Array(data.rows);
let pc = u32(this.api.vbGetProgramCounter(data.sim));
let row; // Located output row
// The target address is before or on the first row of output
if (data.row <= 0) {
decode = u32(data.target - 4 * Math.max(0, data.row + 10));
// Locate the target row
for (;;) {
row = this.dasmRow(data.sim, decode, pc);
if (u32(data.target - decode) < row.size)
break;
decode = u32(decode + row.size);
}
// Locate the first row of output
for (index = data.row; index < 0; index++) {
decode = u32(decode + row.size);
row = this.dasmRow(data.sim, decode, pc);
}
// Prepare to process remaining rows
decode = u32(decode + row.size);
rows[0] = row;
index = 1;
}
// The target address is after the first row of output
else {
let circle = new Array(data.row + 1);
let count = Math.min(data.row + 1, data.rows);
let src = 0;
decode = u32(data.target - 4 * (data.row + 10));
// Locate the target row
for (;;) {
row = circle[src] = this.dasmRow(data.sim, decode, pc);
decode = u32(decode + row.size);
if (u32(data.target - row.address) < row.size)
break;
src = (src + 1) % circle.length;
}
// Copy entries from the circular buffer to the output list
for (index = 0; index < count; index++) {
src = (src + 1) % circle.length;
rows[index] = circle[src];
}
}
// Locate any remaining rows
for (; index < data.rows; index++) {
let row = rows[index] = this.dasmRow(data.sim, decode, pc);
decode = u32(decode + row.size);
}
// Respond to main thread
data.response = {
pc : pc,
rows : rows,
scroll: data.scroll
};
}
// Retrieve all CPU program registers
getProgramRegisters(data) {
let ret = data.response = new Uint32Array(32);
for (let x = 0; x < 32; x++)
ret[x] = this.api.vbGetProgramRegister(data.sim, x);
data.transfers = [ ret.buffer ];
}
// Retrieve the value of a system register
getSystemRegister(data) {
data.response = u32(this.api.vbGetSystemRegister(data.sim, data.id));
}
// Retrieve all CPU system registers (including PC)
getSystemRegisters(data) {
data.response = {
adtre: u32(this.api.vbGetSystemRegister(data.sim, 25)),
chcw : u32(this.api.vbGetSystemRegister(data.sim, 24)),
ecr : u32(this.api.vbGetSystemRegister(data.sim, 4)),
eipc : u32(this.api.vbGetSystemRegister(data.sim, 0)),
eipsw: u32(this.api.vbGetSystemRegister(data.sim, 1)),
fepc : u32(this.api.vbGetSystemRegister(data.sim, 2)),
fepsw: u32(this.api.vbGetSystemRegister(data.sim, 3)),
pc : u32(this.api.vbGetProgramCounter(data.sim )),
pir : u32(this.api.vbGetSystemRegister(data.sim, 6)),
psw : u32(this.api.vbGetSystemRegister(data.sim, 5)),
tkcw : u32(this.api.vbGetSystemRegister(data.sim, 7)),
[29] : u32(this.api.vbGetSystemRegister(data.sim, 29)),
[30] : u32(this.api.vbGetSystemRegister(data.sim, 30)),
[31] : u32(this.api.vbGetSystemRegister(data.sim, 31))
};
}
// Read bytes from the simulation
read(data) {
let ptr = this.api.Malloc(data.length);
this.api.ReadBuffer(data.sim, ptr, data.address, data.length);
let buffer = new Uint8Array(this.memory, ptr, data.length).slice();
this.api.Free(ptr);
data.response = {
address: data.address,
bytes : buffer
};
data.transfers = [ buffer.buffer ];
}
// Attempt to execute until the following instruction
runNext(data) {
this.api.RunNext(data.sims[0], data.sims[1]);
let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ];
if (data.sims[1])
pc.push(u32(this.api.vbGetProgramCounter(data.sims[1])));
data.response = {
pc: pc
}
}
// Specify a new value for PC
setProgramCounter(data) {
data.response =
u32(this.api.vbSetProgramCounter(data.sim, data.value));
}
// Specify a new value for a program register
setProgramRegister(data) {
data.response = this.api.vbSetProgramRegister
(data.sim, data.id, data.value);
}
// Specify a ROM buffer
setROM(data) {
let ptr = this.api.Malloc(data.rom.length);
let buffer = new Uint8Array(this.memory, ptr, data.rom.length);
for (let x = 0; x < data.rom.length; x++)
buffer[x] = data.rom[x];
data.response = !!this.api.SetROM(data.sim, ptr, data.rom.length);
}
// Specify a new value for a system register
setSystemRegister(data) {
data.response = u32(this.api.vbSetSystemRegister
(data.sim, data.id, data.value));
}
// Execute one instruction
singleStep(data) {
this.api.SingleStep(data.sims[0], data.sims[1]);
let pc = [ u32(this.api.vbGetProgramCounter(data.sims[0])) ];
if (data.sims[1])
pc.push(u32(this.api.vbGetProgramCounter(data.sims[1])));
data.response = {
pc: pc
}
}
// Unsubscribe from frame data
unsubscribe(data) {
let sim = data.sim || 0;
if (sim in this.subscriptions) {
let subs = this.subscriptions[sim];
delete subs[data.key];
if (Object.keys(subs).length == 0)
delete this.subscriptions[sim];
}
}
// Write bytes to the simulation
write(data) {
let ptr = this.api.Malloc(data.bytes.length);
let buffer = new Uint8Array(this.memory, ptr, data.bytes.length);
for (let x = 0; x < data.bytes.length; x++)
buffer[x] = data.bytes[x];
this.api.WriteBuffer(data.sim, ptr, data.address, data.bytes.length);
this.api.Free(ptr);
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve basic information for a row of disassembly
dasmRow(sim, address, pc) {
let bits = this.api.vbRead(sim, address, 3 /* VB_U16 */);
let opcode = bits >> 10 & 63;
let size = (
opcode < 0b101000 || // Formats I through III
opcode == 0b110010 || // Illegal
opcode == 0b110110 // Illegal
) ? 2 : 4;
// Establish row information
let row = {
address: address,
bytes : [ bits & 0xFF, bits >> 8 ],
size : u32(address + 2) == pc ? 2 : size
};
// Read additional bytes
if (size == 4) {
bits = this.api.vbRead(sim, address + 2, 3 /* VB_U16 */);
row.bytes.push(bits & 0xFF, bits >> 8);
}
return row;
}
// Process subscriptions and send a message to the main thread
doSubscriptions(sims) {
let message = { subscriptions: {} };
let transfers = [];
// Process all simulations
for (let sim of sims) {
// There are no subscriptions for this sim
if (!(sim in this.subscriptions))
continue;
// Working variables
let subs = message.subscriptions[sim] = {};
// Process all subscriptions
for (let sub of Object.entries(this.subscriptions[sim])) {
// Run the command
this[sub[1].command](sub[1]);
// Add the response to the message
if (sub[1].response) {
subs[sub[0]] = sub[1].response;
delete sub[1].response;
}
// Add the transferable objects to the message
if (sub[1].transfers) {
transfers.push(... sub[1].transfers);
delete sub[1].transfers;
}
}
}
// Send the message to the main thread
if (Object.keys(message).length != 0)
postMessage(message, transfers);
}
}();

View File

@ -1,151 +0,0 @@
///////////////////////////////////////////////////////////////////////////////
// Sim //
///////////////////////////////////////////////////////////////////////////////
// One simulated Virtual Boy
class Sim {
///////////////////////// Initialization Methods //////////////////////////
constructor(core, sim, index) {
this.core = core;
this.index = index;
this.sim = sim;
}
///////////////////////////// Public Methods //////////////////////////////
// Locate CPU instructions
disassemble(target, row, rows, scroll, options = {}) {
return this.core.send({
command : "disassemble",
row : row,
rows : rows,
scroll : scroll,
sim : this.sim,
subscribe: options.subscribe,
target : target
});
}
// Retrieve all CPU program registers
getProgramRegisters(options = {}) {
return this.core.send({
command : "getProgramRegisters",
sim : this.sim,
subscribe: options.subscribe
});
}
// Retrieve the value of a system register
getSystemRegister(id, options = {}) {
return this.core.send({
command : "getSystemRegister",
id : id,
sim : this.sim,
subscribe: options.subscribe
});
}
// Retrieve all CPU system registers (including PC)
getSystemRegisters(options = {}) {
return this.core.send({
command : "getSystemRegisters",
sim : this.sim,
subscribe: options.subscribe
});
}
// Read multiple bytes from the bus
read(address, length, options = {}) {
return this.core.send({
address : address,
command : "read",
debug : !("debug" in options) || !!options.debug,
length : length,
sim : this.sim,
subscribe: options.subscribe
});
}
// Specify a new value for PC
setProgramCounter(value, options = {}) {
return this.core.send({
command: "setProgramCounter",
refresh: !!options.refresh,
sim : this.sim,
value : value
});
}
// Specify a new value for a program register
setProgramRegister(id, value, options = {}) {
return this.core.send({
command: "setProgramRegister",
id : id,
refresh: !!options.refresh,
sim : this.sim,
value : value
});
}
// Specify the current ROM buffer
setROM(rom, options = {}) {
return this.core.send({
command: "setROM",
rom : rom,
refresh: !!options.refresh,
sim : this.sim
}, [ rom.buffer ]);
}
// Specify a new value for a system register
setSystemRegister(id, value, options = {}) {
return this.core.send({
command: "setSystemRegister",
id : id,
refresh: !!options.refresh,
sim : this.sim,
value : value
});
}
// Ubsubscribe from frame data
unsubscribe(key) {
return this.core.unsubscribe(key, this.sim);
}
// Write multiple bytes to the bus
write(address, bytes, options = {}) {
return this.core.send({
address : address,
command : "write",
bytes : bytes,
debug : !("debug" in options) || !!options.debug,
refresh : !!options.refresh,
sim : this.sim,
subscribe: options.subscribe
},
bytes instanceof ArrayBuffer ? [ bytes ] :
bytes.buffer instanceof ArrayBuffer ? [ bytes.buffer ] :
undefined
);
}
///////////////////////////// Package Methods /////////////////////////////
// The simulation has been destroyed
destroy() {
let sim = this.sim;
this.core = null;
this.sim = 0;
return sim;
}
}
export { Sim };

View File

@ -1,74 +0,0 @@
{
"id": "en-US",
"common": {
"close" : "Close",
"collapse" : "Collapse",
"expand" : "Expand",
"gotoPrompt": "Enter the address to go to:"
},
"error": {
"fileRead": "An error occurred when reading the file.",
"romNotVB": "The selected file is not a Virtual Boy ROM."
},
"app": {
"title": "Virtual Boy Emulator",
"menu": {
"_": "Application menu bar",
"file": {
"_" : "File",
"loadROM" : "Load ROM{sim}...",
"debugMode": "Debug mode"
},
"emulation": {
"_" : "Emulation",
"run" : "Run",
"reset" : "Reset",
"dualSims": "Dual sims",
"linkSims": "Link sims"
},
"debug": {
"_" : "Debug{sim}",
"console" : "Console",
"memory" : "Memory",
"cpu" : "CPU",
"breakpoints" : "Breakpoints",
"palettes" : "Palettes",
"characters" : "Characters",
"bgMaps" : "BG maps",
"objects" : "Objects",
"worlds" : "Worlds",
"frameBuffers": "Frame buffers"
},
"theme": {
"_" : "Theme",
"light" : "Light",
"dark" : "Dark",
"virtual": "Virtual"
}
}
},
"cpu": {
"_" : "CPU{sim}",
"float" : "Float",
"hex" : "Hex",
"signed" : "Signed",
"splitHorizontal": "Program and system registers splitter",
"splitVertical" : "Disassembler and registers splitter",
"unsigned" : "Unsigned"
},
"memory": {
"_": "Memory{sim}"
}
}

View File

@ -1,34 +0,0 @@
// Global theme assets
Bundle["app/theme/kiosk.css"].installStylesheet(true);
await Bundle["app/theme/inconsolata.woff2"].installFont(
"Inconsolata SemiExpanded Medium");
await Bundle["app/theme/roboto.woff2"].installFont("Roboto");
Bundle["app/theme/check.svg" ].installImage("tk-check" , "check.svg" );
Bundle["app/theme/close.svg" ].installImage("tk-close" , "close.svg" );
Bundle["app/theme/collapse.svg"].installImage("tk-collapse", "collapse.svg");
Bundle["app/theme/expand.svg" ].installImage("tk-expand" , "expand.svg" );
Bundle["app/theme/radio.svg" ].installImage("tk-radio" , "radio.svg" );
Bundle["app/theme/scroll.svg" ].installImage("tk-scroll" , "scroll.svg" );
// Module imports
import { Core } from /**/"./core/Core.js";
import { Toolkit } from /**/"./toolkit/Toolkit.js";
import { App } from /**/"./app/App.js";
// Begin application
let dark = matchMedia("(prefers-color-scheme: dark)").matches;
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
new App({
core : await Core.create({ sims: 2 }),
locale : navigator.language,
standalone: true,
theme : dark ? "dark" : "light",
locales : [
await (await fetch(url(/**/"./locale/en-US.json"))).json()
],
themes : {
dark : Bundle["app/theme/dark.css" ].installStylesheet( dark),
light : Bundle["app/theme/light.css" ].installStylesheet(!dark),
virtual: Bundle["app/theme/virtual.css"].installStylesheet(false)
}
});

View File

@ -1,12 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<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(this,width,height)" src="">
</body>
</html>

View File

@ -1,26 +0,0 @@
:root {
--tk-control : #333333;
--tk-control-active : #555555;
--tk-control-border : #cccccc;
--tk-control-highlight : #444444;
--tk-control-shadow : #9b9b9b;
--tk-control-text : #cccccc;
--tk-desktop : #111111;
--tk-selected : #008542;
--tk-selected-blur : #325342;
--tk-selected-blur-text : #ffffff;
--tk-selected-text : #ffffff;
--tk-splitter-focus : #ffffff99;
--tk-window : #222222;
--tk-window-blur-close : #d9aeae;
--tk-window-blur-close-text: #eeeeee;
--tk-window-blur-title : #9fafb9;
--tk-window-blur-title2 : #c0b2ab;
--tk-window-blur-title-text: #444444;
--tk-window-close : #ee9999;
--tk-window-close-text : #ffffff;
--tk-window-text : #cccccc;
--tk-window-title : #80ccff;
--tk-window-title2 : #ffb894;
--tk-window-title-text : #000000;
}

View File

@ -1,761 +0,0 @@
:root {
--tk-font-dialog: "Roboto", sans-serif;
--tk-font-mono : "Inconsolata SemiExpanded Medium", monospace;
--tk-font-size : 12px;
}
.tk {
font-family: var(--tk-font-dialog);
font-size : var(--tk-font-size);
line-height: 1em;
margin : 0;
outline : none;
padding : 0;
}
table.tk {
border : none;
border-spacing: 0;
}
.tk-body {
overflow: hidden;
}
.tk-app {
/* Height managed through resize listener */
width: 100%;
}
.tk-mono {
font-family: var(--tk-font-mono);
}
/********************************** Button ***********************************/
.tk-button > * {
background: var(--tk-control);
border : 1px solid var(--tk-control-shadow);
box-shadow: 1px 1px 0 0 var(--tk-control-shadow);
color : var(--tk-control-text);
margin : 0 1px 1px 0;
padding : 3px;
text-align: center;
}
.tk-button:focus > * {
background: var(--tk-control-active);
}
.tk-button.active > * {
box-shadow: none;
margin : 1px 0 0 1px;
}
.tk-button[aria-disabled="true"] > * {
color: var(--tk-control-shadow);
}
/********************************* Check Box *********************************/
.tk-checkbox {
align-items: center;
column-gap : 2px;
}
.tk-checkbox .tk-icon {
align-items: center;
background : var(--tk-window);
border : 1px solid var(--tk-control-shadow);
box-sizing : border-box;
display : flex;
height : 12px;
width : 12px;
}
.tk-checkbox .tk-icon:before {
background : transparent;
content : "";
height : 100%;
display : block;
mask-image : var(--tk-check);
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-image : var(--tk-check);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-checkbox[aria-checked="true"] .tk-icon:before {
background: var(--tk-window-text);
}
.tk-checkbox:focus .tk-icon {
background: var(--tk-control-active);
}
.tk-checkbox[aria-checked="true"]:focus .tk-icon:before {
background: var(--tk-control-text);
}
.tk-checkbox.active:focus .tk-icon:before {
background: var(--tk-control-shadow);
}
/****************************** Drop-Down List *******************************/
.tk-dropdown {
background : var(--tk-window);
border : 1px solid var(--tk-control-shadow);
border-radius: 0;
color : var(--tk-window-text);
margin : 0;
padding : 1px;
}
/********************************* Menu Bar **********************************/
.tk-menu-bar {
background : var(--tk-control);
border-bottom: 1px solid var(--tk-control-border);
color : var(--tk-control-text);
column-gap : 1px;
display : flex;
flex-wrap : wrap;
padding : 2px;
user-select : none;
white-space : nowrap;
}
.tk-menu {
background : var(--tk-control);
border : 1px solid var(--tk-control-border);
box-shadow : 1px 1px 0 0 var(--tk-control-border);
display : flex;
flex-direction: column;
padding : 3px;
row-gap : 1px;
}
.tk-menu-item > * {
background: var(--tk-control);
border : 1px solid transparent;
column-gap: 4px;
display : flex;
margin : 0 1px 1px 0;
padding : 3px;
}
.tk-menu-item > * > .tk-icon {
box-sizing: border-box;
display : none;
height : 1em;
width : 1em;
}
.tk-menu-item > * > .tk-text {
flex-grow: 1;
}
.tk-menu-item[aria-disabled="true"] > * > .tk-text {
color: var(--tk-control-shadow);
}
.tk-menu-item:not(.active, [aria-disabled="true"]):hover > *,
.tk-menu-item:not(.active):focus > * {
border-color: var(--tk-control-shadow);
box-shadow : 1px 1px 0 0 var(--tk-control-shadow);
}
.tk-menu.icons > .tk-menu-item > * > .tk-icon {
display: block;
}
.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon {
border: 1px solid var(--tk-control-border);
}
.tk-menu-item[role="menuitemcheckbox"] > * > .tk-icon:before {
background : transparent;
content : "";
height : 100%;
display : block;
mask-image : var(--tk-check);
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-image : var(--tk-check);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-menu-item[role="menuitemcheckbox"][aria-checked="true"]
> * > .tk-icon:before {
background: var(--tk-control-text);
}
.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"]
> * > .tk-icon {
border: 1px solid var(--tk-control-shadow);
}
.tk-menu-item[role="menuitemcheckbox"][aria-disabled="true"]
> * > .tk-icon:before {
background: var(--tk-control-shadow);
}
.tk-menu-item:not(.active):focus > * {
background: var(--tk-control-active);
}
.tk-menu-item.active > * {
background : var(--tk-control-active);
border-color: var(--tk-control-shadow);
margin : 1px 0 0 1px;
}
.tk-menu-separator {
border : 0 solid var(--tk-control-shadow);
border-width : 1px 0 0 0;
height : 0;
margin-bottom: 1px;
}
/*********************************** Radio ***********************************/
.tk-radio {
align-items: center;
column-gap : 2px;
}
.tk-radio .tk-icon {
align-items : center;
background : var(--tk-window);
border : 1px solid var(--tk-control-shadow);
border-radius: 50%;
box-sizing : border-box;
display : flex;
height : 10px;
width : 10px;
}
.tk-radio .tk-icon:before {
background : transparent;
content : "";
height : 100%;
display : block;
mask-image : var(--tk-radio);
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-image : var(--tk-radio);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-radio[aria-checked="true"] .tk-icon:before {
background: var(--tk-window-text);
}
.tk-radio:focus .tk-icon {
background: var(--tk-control-active);
}
.tk-radio[aria-checked="true"]:focus .tk-icon:before {
background: var(--tk-control-text);
}
.tk-radio.active[aria-checked="false"]:focus .tk-icon:before {
background: var(--tk-control-shadow);
}
/******************************** Scroll Bar *********************************/
.tk-scrollbar {
background: var(--tk-control-highlight);
box-shadow: 0 0 0 1px var(--tk-control-shadow) inset;
box-sizing: border-box;
}
.tk-scrollbar .tk-thumb,
.tk-scrollbar .tk-unit-down,
.tk-scrollbar .tk-unit-up {
background: var(--tk-control);
border : 1px solid var(--tk-control-border);
box-sizing: border-box;
color : var(--tk-control-text);
}
.tk-scrollbar:focus .tk-thumb,
.tk-scrollbar:focus .tk-unit-down,
.tk-scrollbar:focus .tk-unit-up {
background: var(--tk-control-active);
}
.tk-scrollbar .tk-unit-down,
.tk-scrollbar .tk-unit-up {
height: 13px;
width : 13px;
}
.tk-scrollbar .tk-unit-down:before,
.tk-scrollbar .tk-unit-up:before {
background : currentColor;
content : "";
display : block;
height : 100%;
mask-image : var(--tk-scroll);
mask-position: center;
mask-repeat : no-repeat;
mask-size : 100%;
width : 100%;
-webkit-mask-image : var(--tk-scroll);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : 100%;
}
.tk-scrollbar[aria-orientation="horizontal"] .tk-unit-down:before {
transform: rotate(-90deg);
}
.tk-scrollbar[aria-orientation="horizontal"] .tk-unit-up:before {
transform: rotate(90deg);
}
.tk-scrollbar[aria-orientation="vertical"] .tk-unit-down:before {
}
.tk-scrollbar[aria-orientation="vertical"] .tk-unit-up:before {
transform: rotate(180deg);
}
.tk-scrollbar .tk-unit-down.tk-active:before,
.tk-scrollbar .tk-unit-up.tk-active:before {
mask-size: calc(100% - 2px);
-webkit-mask-size: calc(100% - 2px);
}
.tk-scrollbar[aria-disabled="true"] .tk-unit-down,
.tk-scrollbar[aria-disabled="true"] .tk-unit-up,
.tk-scrollbar.tk-full .tk-unit-down,
.tk-scrollbar.tk-full .tk-unit-up {
background: var(--tk-control);
border-color: var(--tk-control-shadow);
color : var(--tk-control-shadow);
}
.tk-scrollbar .tk-block-down,
.tk-scrollbar .tk-block-up {
background : var(--tk-control-highlight);
border-color: var(--tk-control-shadow);
border-style: solid;
border-width: 0 1px;
}
.tk-scrollbar[aria-orientation="horizontal"] .tk-block-down,
.tk-scrollbar[aria-orientation="horizontal"] .tk-block-up {
border-width: 1px 0;
}
.tk-scrollbar .tk-block-down.tk-active,
.tk-scrollbar .tk-block-up.tk-active {
background: var(--tk-control-shadow);
}
.tk-scrollbar[aria-disabled="true"] .tk-thumb,
.tk-scrollbar[aria-disabled="true"] .tk-block-down,
.tk-scrollbar[aria-disabled="true"] .tk-block-up,
.tk-scrollbar.tk-full .tk-thumb,
.tk-scrollbar.tk-full .tk-block-down,
.tk-scrollbar.tk-full .tk-block-up {
visibility: hidden;
}
/******************************** Scroll Pane ********************************/
.tk-scrollpane {
background: var(--tk-control);
}
.tk-scrollpane > .tk-scrollbar {
border: 0 solid var(--tk-control);
}
.tk-scrollpane > .tk-scrollbar[aria-orientation="horizontal"] {
border-width: 1px 0 0 0;
}
.tk-scrollpane > .tk-scrollbar[aria-orientation="vertical"] {
border-width: 0 0 0 1px;
}
/******************************** Split Pane *********************************/
.tk-splitpane > [role="separator"][aria-orientation="horizontal"] {
cursor: ns-resize;
height: 3px;
}
.tk-splitpane > [role="separator"][aria-orientation="vertical"] {
cursor: ew-resize;
width : 3px;
}
.tk-splitpane > [role="separator"]:focus {
background: var(--tk-splitter-focus);
z-index : 1;
}
/********************************* Text Box **********************************/
.tk-textbox {
background: var(--tk-window);
border : 1px solid var(--tk-control-shadow);
color : var(--tk-window-text);
margin : 0;
padding : 2px;
}
/********************************** Windows **********************************/
.tk-desktop {
background: var(--tk-desktop);
}
.tk-window > * {
border : 1px solid var(--tk-control-border);
box-shadow: 1px 1px 0 0 var(--tk-control-border);
margin : 0 1px 1px 0;
}
.tk-window > * > .tk-nw {left : -1px; top : -1px; height: 8px; width : 8px; }
.tk-window > * > .tk-n {left : 7px; top : -1px; right : 8px; height: 3px; }
.tk-window > * > .tk-ne {right: 0px; top : -1px; height: 8px; width : 8px; }
.tk-window > * > .tk-w {left : -1px; top : 7px; width : 3px; bottom: 8px; }
.tk-window > * > .tk-e {right: 0px; top : 7px; width : 3px; bottom: 8px; }
.tk-window > * > .tk-sw {left : -1px; bottom: 0px; height: 8px; width : 8px; }
.tk-window > * > .tk-s {left : 7px; bottom: 0px; right : 8px; height: 3px; }
.tk-window > * > .tk-se {right: 0px; bottom: 0px; height: 8px; width : 8px; }
.tk-window > * > .tk-title {
align-items : center;
background : var(--tk-window-blur-title);
border-bottom: 1px solid var(--tk-control-shadow);
box-sizing : border-box;
color : var(--tk-window-blur-title-text);
overflow : hidden;
padding : 1px;
position : relative;
}
.tk-window.two > * > .tk-title {
background: var(--tk-window-blur-title2);
}
.tk-window > * > .tk-title .tk-text {
cursor : default;
flex-basis : 0;
font-weight : bold;
min-width : 0;
overflow : hidden;
padding : 1px 1px 1px calc(1em + 3px);
text-align : center;
text-overflow: ellipsis;
user-select : none;
white-space : nowrap;
}
.tk-window > * > .tk-title .tk-close {
background: var(--tk-window-blur-close);
border : 1px solid var(--tk-control-shadow);
color : var(--tk-window-blur-close-text);
height : calc(1em - 1px);
margin : 1px 1px 1px 0;
overflow : none;
width : calc(1em - 1px);
}
.tk-window > * > .tk-title .tk-close:before {
background : currentColor;
content : "";
display : block;
height : 100%;
width : 100%;
mask-image : var(--tk-close);
mask-position: center;
mask-repeat : no-repeat;
mask-size : 100%;
-webkit-mask-image : var(--tk-close);
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : 100%;
}
.tk-window > * > .tk-title .tk-close.active:before {
mask-size: calc(100% - 2px);
-webkit-mask-size: calc(100% - 2px);
}
.tk-window:focus-within > * > .tk-title {
background: var(--tk-window-title);
color : var(--tk-window-title-text);
}
.tk-window.two:focus-within > * > .tk-title {
background: var(--tk-window-title2);
}
.tk-window:focus-within > * > .tk-title .tk-close {
background: var(--tk-window-close);
color : var(--tk-window-close-text);
}
.tk-window > * > .tk-client {
background: var(--tk-control);
}
/************************************ CPU ************************************/
.tk-cpu .tk-main {
height: 100%;
width : 100%;
}
.tk-cpu .tk-main > .tk-a,
.tk-cpu .tk-registers > .tk-a,
.tk-cpu .tk-registers > .tk-b {
box-shadow: 0 0 0 1px var(--tk-control),0 0 0 2px var(--tk-control-shadow);
}
.tk-cpu .tk-main > .tk-a { margin : 3px; }
.tk-cpu .tk-main > [role="separator"] { margin : 1px -2px; }
.tk-cpu .tk-main > .tk-b { margin : 3px; }
.tk-cpu .tk-registers > .tk-a { margin-bottom: 3px; }
.tk-cpu .tk-registers > [role="separator"] { margin : -2px; }
.tk-cpu .tk-registers > .tk-b { margin-top : 3px; }
.tk-disassembler .tk-viewport {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk-disassembler .tk-view {
height: 100%;
}
.tk-disassembler .tk-metrics {
padding-bottom: 1px;
}
.tk-disassembler .tk {
cursor : default;
font-family: var(--tk-font-mono);
user-select: none;
white-space: nowrap;
}
.tk-disassembler .tk-bytes,
.tk-disassembler .tk-mnemonic,
.tk-disassembler .tk-operands {
padding: 0 0 1px calc(1.2em - 1px);
}
.tk-disassembler .tk-address {
padding-left: 1px;
}
.tk-disassembler .tk-operands {
padding-right: 1px;
}
.tk-disassembler .tk-selected {
background: var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
}
.tk-disassembler:focus-within .tk-selected {
background: var(--tk-selected);
color : var(--tk-selected-text);
}
.tk-reglist .tk-viewport {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk-reglist .tk-list {
align-items: center;
}
.tk-reglist .tk-expand {
align-items : center;
border-radius : 2px;
display : flex;
height : 11px;
justify-content: center;
width : 11px;
}
.tk-reglist .tk-expand:before {
content : "";
height : 100%;
display : block;
mask-position: center;
mask-repeat : no-repeat;
mask-size : contain;
width : 100%;
-webkit-mask-position: center;
-webkit-mask-repeat : no-repeat;
-webkit-mask-size : contain;
}
.tk-reglist .tk-expand:focus {
background: var(--tk-control-active);
}
.tk-reglist .tk-expand[aria-expanded]:before {
background: var(--tk-window-text);
}
.tk-reglist .tk-expand[aria-expanded]:focus:before {
background: var(--tk-control-text);
}
.tk-reglist .tk-expand[aria-expanded="false"]:before {
mask-image: var(--tk-expand);
-webkit-mask-image: var(--tk-expand);
}
.tk-reglist .tk-expand[aria-expanded="true"]:before {
mask-image: var(--tk-collapse);
-webkit-mask-image: var(--tk-collapse);
}
.tk-reglist .tk-name {
padding: 0 0.5em 0 1px;
}
.tk-reglist .tk-textbox {
background: transparent;
border : none;
padding : 0;
width : 1.5em;
}
.tk-reglist.tk-program .tk-textbox:not(.tk-mono) {
text-align: right;
width : 6em;
}
.tk-reglist .tk-expansion {
align-items : center;
column-gap : 0.8em;
margin-bottom: 2px;
padding : 2px 0 0 1.5em;
}
.tk-reglist .tk-expansion .tk-number .tk-label {
align-items : center;
display : flex;
justify-content: center;
min-width : 12px;
}
.tk-reglist .tk-expansion .tk-checkbox[aria-disabled="true"][aria-checked="true"]
.tk-icon:before {
background: var(--tk-control-shadow);
}
.tk-reglist .tk-expansion .tk-checkbox[aria-disabled="true"] .tk-contents,
.tk-reglist .tk-expansion .tk-number[disabled] *,
.tk-reglist .tk-expansion .tk-label[disabled],
.tk-reglist .tk-expansion .tk-textbox[disabled] {
color: var(--tk-control-shadow);
}
/********************************** Memory ***********************************/
.tk-window .tk-memory {
height: 100%;
width : 100%;
}
.tk-memory .tk-editor {
box-shadow: 0 0 0 1px var(--tk-control),0 0 0 2px var(--tk-control-shadow);
height : calc(100% - 6px);
margin : 3px;
width : calc(100% - 6px);
}
.tk-memory .tk-viewport {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk-memory .tk-metrics,
.tk-memory .tk-view * {
padding-bottom: 1px;
cursor : default;
font-family : var(--tk-font-mono);
user-select : none;
}
.tk-memory .tk-byte {
border : 0 solid transparent;
padding : 0 1px;
text-align: center;
}
.tk-memory .tk-byte:not(.tk-15) {
margin-right: calc(0.6em - 1px);
}
.tk-memory .tk-address,
.tk-memory .tk-byte.tk-7 {
margin-right: calc(1.2em - 1px);
}
.tk-memory .tk-byte.tk-selected {
background: var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
}
.tk-memory .tk-editor:focus-within .tk-byte.tk-selected {
background: var(--tk-selected);
color : var(--tk-selected-text);
}

View File

@ -1,26 +0,0 @@
:root {
--tk-control : #eeeeee;
--tk-control-active : #cccccc;
--tk-control-border : #000000;
--tk-control-highlight : #f8f8f8;
--tk-control-shadow : #6c6c6c;
--tk-control-text : #000000;
--tk-desktop : #cccccc;
--tk-selected : #008542;
--tk-selected-blur : #325342;
--tk-selected-blur-text : #ffffff;
--tk-selected-text : #ffffff;
--tk-splitter-focus : #00000080;
--tk-window : #ffffff;
--tk-window-blur-close : #d9aeae;
--tk-window-blur-close-text: #eeeeee;
--tk-window-blur-title : #aac4d5;
--tk-window-blur-title2 : #dbc4b8;
--tk-window-blur-title-text: #444444;
--tk-window-close : #ee9999;
--tk-window-close-text : #ffffff;
--tk-window-text : #000000;
--tk-window-title : #80ccff;
--tk-window-title2 : #ffb894;
--tk-window-title-text : #000000;
}

View File

@ -1,70 +0,0 @@
:root {
--tk-control : #000000;
--tk-control-active : #550000;
--tk-control-border : #ff0000;
--tk-control-highlight : #550000;
--tk-control-shadow : #aa0000;
--tk-control-text : #ff0000;
--tk-desktop : #000000;
--tk-selected : #550000;
--tk-selected-blur : #550000;
--tk-selected-blur-text : #ff0000;
--tk-selected-text : #ff0000;
--tk-splitter-focus : #ff000099;
--tk-window : #000000;
--tk-window-blur-close : #000000;
--tk-window-blur-close-text: #aa0000;
--tk-window-blur-title : #000000;
--tk-window-blur-title2 : #000000;
--tk-window-blur-title-text: #aa0000;
--tk-window-close : #550000;
--tk-window-close-text : #ff0000;
--tk-window-text : #ff0000;
--tk-window-title : #550000;
--tk-window-title2 : #550000;
--tk-window-title-text : #ff0000;
}
input {
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
}
/******************************** Scroll Bar *********************************/
.tk-scrollbar .tk-thumb,
.tk-scrollbar .tk-unit-down,
.tk-scrollbar .tk-unit-up {
background : #aa0000;
border-color: #550000;
color : #000000;
}
.tk-scrollbar:focus .tk-thumb,
.tk-scrollbar:focus .tk-unit-down,
.tk-scrollbar:focus .tk-unit-up {
background : #ff0000;
border-color: #aa0000;
}
.tk-scrollbar[aria-disabled="true"] .tk-thumb,
.tk-scrollbar[aria-disabled="true"] .tk-unit-down,
.tk-scrollbar[aria-disabled="true"] .tk-unit-up,
.tk-scrollbar.tk-full .tk-thumb,
.tk-scrollbar.tk-full .tk-unit-down,
.tk-scrollbar.tk-full .tk-unit-up {
background : #550000;
border-color: #aa0000;
color : #aa0000;
}
.tk-scrollbar .tk-block-down,
.tk-scrollbar .tk-block-up {
background : #550000;
border-color: #aa0000;
}
.tk-window > * > .tk-client > .tk-memory {
box-shadow: 0 0 0 1px #000000, 0 0 0 2px #ff0000;
}

View File

@ -1,387 +0,0 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Button //
///////////////////////////////////////////////////////////////////////////////
// Push, toggle or radio button
class Button extends Component {
static Component = Component;
//////////////////////////////// Constants ////////////////////////////////
// Types
static BUTTON = 0;
static RADIO = 1;
static TOGGLE = 2;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-button",
focusable: true,
role : "button",
tagName : "div",
style : {
display : "inline-block",
userSelect: "none"
}
});
// Configure instance fields
options = options || {};
this.attribute = options.attribute || "aria-pressed";
this.group = null;
this.isEnabled = null;
this.isSelected = false;
this.text = null;
this.type = Button.BUTTON;
// Configure contents
this.contents = document.createElement("div");
this.append(this.contents);
// Configure component
this.setEnabled(!("enabled" in options) || options.enabled);
if ("group" in options)
options.group.add(this);
this.setText (options.text);
this.setType (options.type);
if ("selected" in options)
this.setSelected(options.selected);
// Configure event handlers
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "Enter": // Fallthrough
case " " :
this.click();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
// Pointer down
onPointerDown(e) {
this.focus();
// Error checking
if (
!this.isEnabled ||
this.element.hasPointerCapture(e.pointerId) ||
e.button != 0
) return;
// Configure event
this.element.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList.add("active");
}
// Pointer move
onPointerMove(e) {
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure event
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList[
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
}
// Pointer up
onPointerUp(e) {
// Error checking
if (
!this.isEnabled ||
e.button != 0 ||
!this.element.hasPointerCapture(e.pointerId)
) return;
// Configure event
this.element.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
this.element.classList.remove("active");
// Item is an action
let bounds = this.getBounds();
if (this.menu == null && Toolkit.isInside(this.element, e))
this.click();
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically activate the button
click() {
if (this instanceof Toolkit.CheckBox)
this.setSelected(this instanceof Toolkit.Radio||!this.isSelected);
this.event("action");
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("aria-disabled", enabled ? null : "true");
}
// Specify whether the toggle or radio button is selected
setSelected(selected) {
selected = !!selected;
// Take no action
if (selected == this.isSelected)
return;
// Processing by button type
switch (this.type) {
case Button.RADIO :
if (selected && this.group != null)
this.group.deselect();
// Fallthrough
case Button.TOGGLE:
this.isSelected = selected;
this.setAttribute(this.attribute, selected);
}
}
// Specify the widget's display text
setText(text) {
this.text = (text || "").toString().trim();
this.translate();
}
// Specify what kind of button this is
setType(type) {
switch (type) {
case Button.BUTTON:
this.type = type;
this.setAttribute(this.attribute, null);
this.setSelected(false);
break;
case Button.RADIO : // Fallthrough
case Button.TOGGLE:
this.type = type;
this.setAttribute(this.attribute, this.isSelected);
this.setSelected(this.isSelected);
break;
default: return;
}
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
super.translate();
if (this.contents != null)
this.contents.innerText = this.gui.translate(this.text, this);
}
}
///////////////////////////////////////////////////////////////////////////////
// CheckBox //
///////////////////////////////////////////////////////////////////////////////
// On/off toggle box
class CheckBox extends Button {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
attribute: "aria-checked",
className: "tk tk-checkbox",
role : "checkbox",
style : {},
type : Button.TOGGLE
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Default styles override
for (let entry of Object.entries({
display : "inline-grid",
gridTemplateColumns: "max-content auto"
})) if (!(entry[0] in uptions.style))
uptions.style[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
this.contents.classList.add("tk-contents");
// Configure icon
this.icon = document.createElement("div");
this.icon.className = "tk tk-icon";
this.icon.setAttribute("aria-hidden", "true");
this.prepend(this.icon);
}
}
///////////////////////////////////////////////////////////////////////////////
// Radio //
///////////////////////////////////////////////////////////////////////////////
// Single selection box
class Radio extends CheckBox {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
// Default options override
let uptions = {};
Object.assign(uptions, options || {});
for (let entry of Object.entries({
className: "tk tk-radio",
role : "radio",
type : Button.RADIO
})) if (!(entry[0] in uptions))
uptions[entry[0]] = entry[1];
// Component overrides
super(app, uptions);
}
}
///////////////////////////////////////////////////////////////////////////////
// Group //
///////////////////////////////////////////////////////////////////////////////
// Radio button or menu item group
class Group extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app) {
super(app, {
tagName: "div",
style : {
height : "0",
position: "absolute",
width : "0"
}
});
// Configure instance fields
this.items = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Add an item
add(item) {
// Error checking
if (!Toolkit.isComponent(item) || this.items.indexOf(item) != -1)
return item;
// Configure component
this.setAttribute("role",
item instanceof Toolkit.Radio ? "radiogroup" : "group");
// Configure item
if (item.group != null)
item.group.remove(item);
item.group = this;
// Add the item to the collection
item.id = item.id || Toolkit.id();
this.items.push(item);
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
return item;
}
// Remove all items
clear() {
this.items.splice();
this.setAttribute("aria-owns", "");
}
// Un-check all items in the group
deselect() {
for (let item of this.items)
if (item.isSelected && "setSelected" in item)
item.setSelected(false);
}
// Remove an item
remove(item) {
// Error checking
let index = this.items.indexOf(item);
if (index == -1)
return;
// Remove the item from the collection
this.items.splice(index, 1);
this.setAttribute("aria-owns", this.items.map(i=>i.id).join(" "));
item.group = null;
return item;
}
}
export { Button, CheckBox, Group, Radio };

View File

@ -1,312 +0,0 @@
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Component //
///////////////////////////////////////////////////////////////////////////////
// Abstract class representing a distinct UI element
class Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options, defaults) {
// Configure instance fields
this.children = [];
this.gui = gui || this;
this.label = null;
this.resizeObserver = null;
this.substitutions = {};
this.toolTip = null;
// Configure default options
let uptions = options || {};
options = {};
Object.assign(options, uptions);
options.style = options.style || {};
defaults = defaults || {};
defaults.style = defaults.style || {};
for (let key of Object.keys(defaults))
if (!(key in options))
options[key] = defaults[key];
for (let key of Object.keys(defaults.style))
if (!(key in options.style))
options.style[key] = defaults.style[key];
this.visibility = !!options.visibility;
// Configure element
this.element = document.createElement(
("tagName" in options ? options.tagName : null) || "div");
if (Object.keys(options.style).length != 0)
Object.assign(this.element.style, options.style);
if ("className" in options && options.className)
this.element.className = options.className;
if ("focusable" in options)
this.setFocusable(options.focusable, options.tabStop);
if ("id" in options)
this.setId(options.id);
if ("role" in options && options.role )
this.element.setAttribute("role", options.role);
if ("visible" in options)
this.setVisible(options.visible);
// Configure component
this.setAttribute("name", options.name || "");
this.setLabel (options.label || "");
this.setToolTip (options.toolTip || "");
// Configure substitutions
if ("substitutions" in options) {
for (let sub of Object.entries(options.substitutions))
this.setSubstitution(sub[0], sub[1], true);
this.translate();
}
}
///////////////////////////// Public Methods //////////////////////////////
// Add a child component
add(component) {
// The component is already a child of this component
let index = this.children.indexOf(component);
if (index != -1)
return index;
// The component has a different parent already
if (component.parent != null)
component.parent.remove(component);
// Add the child component to this component
component.parent = this;
this.children.push(component);
if ("addHook" in this)
this.addHook(component);
else this.append(component);
if ("addedHook" in component)
component.addedHook(this);
return this.children.length - 1;
}
// Listen for events
addEventListener(type, listener, useCapture) {
let callback = e=>{
e.component = this;
return listener(e);
};
// Register the listener for the event type
this.element.addEventListener(type, callback, useCapture);
// Listen for resize events on the element
if (type == "resize" && this.resizeObserver == null) {
this.resizeObserver = new ResizeObserver(
()=>this.event("resize"));
this.resizeObserver.observe(this.element);
}
return callback;
}
// Add a DOM element as a sibling after this component
after(child) {
let element = child instanceof Element ? child : child.element;
this.element.after(element);
}
// Add a DOM element to the end of this component's children
append(child) {
let element = child instanceof Element ? child : child.element;
this.element.append(element);
}
// Add a DOM element as a sibling before this component
before(child) {
let element = child instanceof Element ? child : child.element;
this.element.before(element);
}
// Request non-focus on this component
blur() {
this.element.blur();
}
// Determine whether this component contains another or an element
contains(child) {
// Child is an element
if (child instanceof Element)
return this.element.contains(child);
// Child is a component
for (let component = child; component; component = component.parent)
if (component == this)
return true;
return false;
}
// Request focus on the component
focus() {
this.element.focus();
}
// Retrieve the current DOM position of the element
getBounds() {
return this.element.getBoundingClientRect();
}
// Determine whether this component currently has focus
hasFocus() {
return document.activeElement == this.element;
}
// Determine whether the component is visible
isVisible() {
// Common visibility test
if (
!document.contains(this.element) ||
this.parent && !this.parent.isVisible()
) return false;
// Overridden visibility test
if ("visibleHook" in this) {
if (!this.visibleHook())
return false;
}
// Default visibility test
else {
let style = getComputedStyle(this.element);
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
}
// Add a DOM element to the beginning of this component's children
prepend(child) {
let element = child instanceof Element ? child : child.element;
this.element.prepend(element);
}
// Remove a child component
remove(component) {
let index = this.children.indexOf(component);
// The component does not belong to this component
if (index == -1)
return -1;
// Remove the child component from this component
this.children.splice(index, 1);
if ("removeHook" in this)
this.removeHook(component);
else component.element.remove();
if ("removedHook" in component)
component.removedHook(this);
return index;
}
// Remove an event listener
removeEventListener(type, listener, useCapture) {
this.element.removeEventListener(type, listener, useCapture);
}
// Specify an HTML attribute's value
setAttribute(name, value) {
value =
value === false ? false :
value === null || value === undefined ? "" :
value.toString().trim()
;
if (value === "")
this.element.removeAttribute(name);
else this.element.setAttribute(name, value);
}
// Specify whether or not the element is focusable
setFocusable(focusable, tabStop) {
if (!focusable)
this.element.removeAttribute("tabindex");
else this.element.setAttribute("tabindex",
tabStop || tabStop === undefined ? "0" : "-1");
}
// Specify a localization key for the accessible name label
setLabel(key) {
this.label = key;
this.translate();
}
// Specify the DOM Id for this element
setId(id) {
this.id = id = id || null;
this.setAttribute("id", id);
}
// Specify text to substitute within localized contexts
setSubstitution(key, text, noTranslate) {
let ret = this.substitutions[key] || null;
// Providing new text
if (text !== null)
this.substitutions[key] = text.toString();
// Removing an association
else if (key in this.substitutions)
delete this.substitutions[key];
// Update display text
if (!noTranslate)
this.translate();
return ret;
}
// Specify a localization key for the tool tip text
setToolTip(key) {
this.toolTip = key;
this.translate();
}
// Specify whether the component is visible
setVisible(visible) {
let prop = this.visibility ? "visibility" : "display";
if (!!visible)
this.element.style.removeProperty(prop);
else this.element.style[prop] = this.visibility ? "hidden" : "none";
}
///////////////////////////// Package Methods /////////////////////////////
// Dispatch an event
event(type, fields) {
this.element.dispatchEvent(Toolkit.event(type, this, fields));
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
if (this.label)
this.setAttribute("aria-label", this.gui.translate(this.label, this));
if (this.toolTip)
this.setAttribute("title", this.gui.translate(this.toolTip, this));
}
};
export { Component };

View File

@ -1,125 +0,0 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// DropDown //
///////////////////////////////////////////////////////////////////////////////
// Text entry field
class DropDown extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-dropdown",
tagName : "select"
});
// Configure instance fields
this.isEnabled = null;
this.options = [];
// Configure component
options = options || {};
this.setEnabled(!("enabled" in options) || options.enabled);
if ("options" in options)
this.setOptions(options.options);
this.setSelectedIndex(
("selectedIndex" in options ? options : this).selectedIndex);
// Configure event handlers
this.addEventListener("keydown" , e=>e.stopPropagation());
this.addEventListener("pointerdown", e=>e.stopPropagation());
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically change the selection
change() {
this.element.dispatchEvent(this.event("input"));
}
// Retrieve the current selection index
getSelectedIndex() {
return this.element.selectedIndex;
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("disabled", enabled ? null : "true");
}
// Specify the list contents
setOptions(options) {
// Error checking
if (!Array.isArray(options))
return;
// Erase the list of options
this.options.splice(0);
this.element.replaceChildren();
// Add options from the input
for (let option of options) {
if (typeof option != "string")
continue;
this.options.push(option);
this.element.add(document.createElement("option"));
}
// Update the display text
this.translate();
}
// Specify the current selection
setSelectedIndex(index) {
// Error checking
if (typeof index != "number" || isNaN(index))
return this.element.selectedIndex;
index = Math.round(index);
if (index < -1 || index >= this.options.length)
return this.element.selectedIndex;
// Configure element and instance fields
return this.element.selectedIndex = index;
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
super.translate();
// Error checking
if (!this.options)
return;
// Update the list items
for (let x = 0; x < this.options.length; x++) {
this.element.item(x).innerText =
this.gui.translate(this.options[x], this);
}
}
}
export { DropDown };

View File

@ -1,748 +0,0 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Menu //
///////////////////////////////////////////////////////////////////////////////
// Pop-up menu container, child of MenuItem
class Menu extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className : "tk tk-menu",
role : "menu",
tagName : "div",
visibility: true,
visible : false,
style : {
position: "absolute",
}
});
// Trap pointer events
this.addEventListener("pointerdown", e=>{
e.stopPropagation();
e.preventDefault();
});
}
///////////////////////////// Package Methods /////////////////////////////
// Replacement behavior for parent.add()
addedHook(parent) {
this.setAttribute("aria-labelledby", parent.id);
}
};
///////////////////////////////////////////////////////////////////////////////
// MenuSeparator //
///////////////////////////////////////////////////////////////////////////////
// Separator between groups of menu items
class MenuSeparator extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-menu-separator",
role : "separator",
tagName : "div"
});
}
};
///////////////////////////////////////////////////////////////////////////////
// MenuItem //
///////////////////////////////////////////////////////////////////////////////
// Individual menu selection
class MenuItem extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-menu-item",
focusable: true,
tabStop : false,
tagName : "div"
});
options = options || {};
// Configure instance fields
this.isEnabled = null;
this.isExpanded = false;
this.menu = null;
this.menuBar = null;
this.text = null;
this.type = null;
// Configure element
this.contents = document.createElement("div");
this.append(this.contents);
this.eicon = document.createElement("div");
this.eicon.className = "tk tk-icon";
this.contents.append(this.eicon);
this.etext = document.createElement("div");
this.etext.className = "tk tk-text";
this.contents.append(this.etext);
// Configure event handlers
this.addEventListener("blur" , e=>this.onBlur (e));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
// Configure widget
this.gui.localize(this);
this.setEnabled("enabled" in options ? !!options.enabled : true);
this.setId (Toolkit.id());
this.setText (options.text);
this.setType (options.type, options.checked);
}
///////////////////////////// Event Handlers //////////////////////////////
// Focus lost
onBlur(e) {
// An item in a different menu is receiving focus
if (this.menu != null) {
if (
!this .contains(e.relatedTarget) &&
!this.menu.contains(e.relatedTarget)
) this.setExpanded(false);
}
// Item is an action
else if (e.component == this)
this.element.classList.remove("active");
// Simulate a bubbling event sequence
if (this.parent)
this.parent.onBlur(e);
}
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "ArrowDown":
// Error checking
if (!this.parent)
break;
// Top-level: open the menu and focus its first item
if (this.parent == this.menuBar) {
if (this.menu == null)
return;
this.setExpanded(true);
this.listItems()[0].focus();
}
// Sub-menu: cycle to the next sibling
else {
let items = this.parent.listItems();
items[(items.indexOf(this) + 1) % items.length].focus();
}
break;
case "ArrowLeft":
// Error checking
if (!this.parent)
break;
// Sub-menu: close and focus parent
if (
this.parent != this.menuBar &&
this.parent.parent != this.menuBar
) {
this.parent.setExpanded(false);
this.parent.focus();
}
// Top-level: cycle to previous sibling
else {
let menu = this.parent == this.menuBar ?
this : this.parent;
let items = this.menuBar.listItems();
let prev = items[(items.indexOf(menu) +
items.length - 1) % items.length];
if (menu.isExpanded)
prev.setExpanded(true);
prev.focus();
}
break;
case "ArrowRight":
// Error checking
if (!this.parent)
break;
// Sub-menu: open the menu and focus its first item
if (this.menu != null && this.parent != this.menuBar) {
this.setExpanded(true);
(this.listItems()[0] || this).focus();
}
// Top level: cycle to next sibling
else {
let menu = this;
while (menu.parent != this.menuBar)
menu = menu.parent;
let expanded = this.menuBar.expandedMenu() != null;
let items = this.menuBar.listItems();
let next = items[(items.indexOf(menu) + 1) % items.length];
next.focus();
if (expanded)
next.setExpanded(true);
}
break;
case "ArrowUp":
// Error checking
if (!this.parent)
break;
// Top-level: open the menu and focus its last item
if (this.parent == this.menuBar) {
if (this.menu == null)
return;
this.setExpanded(true);
let items = this.listItems();
(items[items.length - 1] || this).focus();
}
// Sub-menu: cycle to previous sibling
else {
let items = this.parent.listItems();
items[(items.indexOf(this) +
items.length - 1) % items.length].focus();
}
break;
case "End":
{
// Error checking
if (!this.parent)
break;
// Focus last sibling
let expanded = this.isExpanded &&
this.parent == this.menuBar;
let items = this.parent.listItems();
let last = items[items.length - 1] || this;
last.focus();
if (expanded)
last.setExpanded(true);
}
break;
case "Enter":
case " ":
// Do nothing
if (!this.isEnabled)
break;
// Action item: activate the menu item
if (this.menu == null)
this.activate(this.type == "check" && e.key == " ");
// Sub-menu: open the menu and focus its first item
else {
this.setExpanded(true);
let items = this.listItems();
if (items[0])
items[0].focus();
}
break;
case "Escape":
// Error checking
if (!this.parent)
break;
// Top-level (not specified by WAI-ARIA)
if (this.parent == this.menuBar) {
if (this.isExpanded)
this.setExpanded(false);
else this.menuBar.exit();
}
// Sub-menu: close and focus parent
else {
this.parent.setExpanded(false);
this.parent.focus();
}
break;
case "Home":
{
// Error checking
if (!this.parent)
break;
// Focus first sibling
let expanded = this.isExpanded &&
this.parent == this.menuBar;
let first = this.parent.listItems()[0] || this;
first.focus();
if (expanded)
first.setExpanded(true);
}
break;
// Do not handle the event
default: return;
}
// The event was handled
e.stopPropagation();
e.preventDefault();
}
// Pointer press
onPointerDown(e) {
this.focus();
// Error checking
if (
!this.isEnabled ||
this.element.hasPointerCapture(e.pointerId) ||
e.button != 0
) return;
// Configure event
if (this.menu == null)
this.element.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Configure component
if (this.menu != null)
this.setExpanded(!this.isExpanded);
else this.element.classList.add("active");
}
// Pointer move
onPointerMove(e) {
// Hovering over a menu when a sibling menu is already open
let expanded = this.parent && this.parent.expandedMenu();
if (this.menu != null && expanded != null && expanded != this) {
// Configure component
this.setExpanded(true);
this.focus();
// Configure event
e.stopPropagation();
e.preventDefault();
return;
}
// Not dragging
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure event
e.stopPropagation();
e.preventDefault();
// Not an action item
if (this.menu != null)
return;
// Check if the cursor is within the bounds of the component
this.element.classList[
Toolkit.isInside(this.element, e) ? "add" : "remove"]("active");
}
// Pointer release
onPointerUp(e) {
// Error checking
if (
!this.isEnabled ||
e.button != 0 ||
(this.parent && this.parent.hasFocus() ?
this.menu != null :
!this.element.hasPointerCapture(e.pointerId)
)
) return;
// Configure event
this.element.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
// Item is an action
let bounds = this.getBounds();
if (this.menu == null && Toolkit.isInside(this.element, e))
this.activate();
}
///////////////////////////// Public Methods //////////////////////////////
// Invoke an action command
activate(noExit) {
if (this.menu != null)
return;
if (this.type == "check")
this.setChecked(!this.isChecked);
if (!noExit)
this.menuBar.exit();
this.element.dispatchEvent(Toolkit.event("action", this));
}
// Add a separator between groups of menu items
addSeparator(options) {
let sep = new Toolkit.MenuSeparator(this, options);
this.add(sep);
return sep;
}
// Produce a list of child items
listItems(invisible) {
return this.children.filter(c=>
c instanceof Toolkit.MenuItem &&
(invisible || c.isVisible())
);
}
// Specify whether the menu item is checked
setChecked(checked) {
if (this.type != "check")
return;
this.isChecked = !!checked;
this.setAttribute("aria-checked", this.isChecked);
}
// Specify whether the menu item can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("aria-disabled", enabled ? null : "true");
if (!enabled)
this.setExpanded(false);
}
// Specify whether the sub-menu is open
setExpanded(expanded) {
// State is not changing
expanded = !!expanded;
if (this.menu == null || expanded === this.isExpanded)
return;
// Position the sub-menu
if (expanded) {
let bndGUI = this.gui .getBounds();
let bndMenu = this.menu.getBounds();
let bndThis = this .getBounds();
let bndParent = !this.parent ? bndThis : (
this.parent == this.menuBar ? this.parent : this.parent.menu
).getBounds();
this.menu.element.style.left = Math.max(0,
Math.min(
(this.parent && this.parent == this.menuBar ?
bndThis.left : bndThis.right) - bndParent.left,
bndGUI.right - bndMenu.width
)
) + "px";
this.menu.element.style.top = Math.max(0,
Math.min(
(this.parent && this.parent == this.menuBar ?
bndThis.bottom : bndThis.top) - bndParent.top,
bndGUI.bottom - bndMenu.height
)
) + "px";
}
// Close all open sub-menus
else for (let child of this.listItems())
child.setExpanded(false);
// Configure component
this.isExpanded = expanded;
this.setAttribute("aria-expanded", expanded);
this.menu.setVisible(expanded);
if (expanded)
this.element.classList.add("active");
else this.element.classList.remove("active");
}
// Specify the widget's display text
setText(text) {
this.text = (text || "").toString().trim();
this.translate();
}
// Specify what kind of menu item this is
setType(type, arg) {
this.type = type = (type || "").toString().trim() || "normal";
switch (type) {
case "check":
this.setAttribute("role", "menuitemcheckbox");
this.setChecked(arg);
break;
default: // normal
this.setAttribute("role", "menuitem");
this.setAttribute("aria-checked", null);
break;
}
if (this.parent && "checkIcons" in this.parent)
this.parent.checkIcons();
}
///////////////////////////// Package Methods /////////////////////////////
// Replacement behavior for add()
addHook(component) {
// Convert to sub-menu
if (this.menu == null) {
this.menu = new Toolkit.Menu(this);
this.after(this.menu);
this.setAttribute("aria-haspopup", "menu");
this.setAttribute("aria-expanded", "false");
if (this.parent && "checkIcons" in this.parent)
this.parent.checkIcons();
}
// Add the child component
component.menuBar = this.menuBar;
this.menu.append(component);
if (component instanceof Toolkit.MenuItem && component.menu != null)
this.menu.append(component.menu);
// Configure icon mode
this.checkIcons();
}
// Check whether any child menu items contain icons
checkIcons() {
if (this.menu == null)
return;
if (this.children.filter(c=>
c instanceof Toolkit.MenuItem &&
c.menu == null &&
c.type != "normal"
).length != 0)
this.menu.element.classList.add("icons");
else this.menu.element.classList.remove("icons");
}
// Replacement behavior for remove()
removeHook(component) {
// Remove the child component
component.element.remove();
if (component instanceof Toolkit.MenuItem && component.menu != null)
component.menu.element.remove();
// Convert to action item
if (this.children.length == 0) {
this.menu.element.remove();
this.menu = null;
this.setAttribute("aria-haspopup", null);
this.setAttribute("aria-expanded", "false");
if (this.parent && "checkIcons" in this.parent)
this.parent.checkIcons();
}
}
// Regenerate localized display text
translate() {
super.translate();
if (!("contents" in this))
return;
this.etext.innerText = this.gui.translate(this.text, this);
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve the currently expanded sub-menu, if any
expandedMenu() {
return this.children.filter(c=>c.isExpanded)[0] || null;
}
};
///////////////////////////////////////////////////////////////////////////////
// MenuBar //
///////////////////////////////////////////////////////////////////////////////
// Application menu bar
class MenuBar extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-menu-bar",
focusable: false,
tagName : "div",
tabStop : true,
role : "menubar",
style : {
position: "relative",
zIndex : "1"
}
});
// Configure instance fields
this.focusTarget = null;
this.menuBar = this;
// Configure event handlers
this.addEventListener("blur" , e=>this.onBlur (e), true);
this.addEventListener("focus" , e=>this.onFocus (e), true);
this.addEventListener("keydown", e=>this.onKeyDown(e), true);
// Configure widget
this.gui.localize(this);
}
///////////////////////////// Event Handlers //////////////////////////////
// Focus lost
onBlur(e) {
if (this.contains(e.relatedTarget))
return;
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, true);
let menu = this.expandedMenu();
if (menu != null)
menu.setExpanded(false);
}
// Focus gained
onFocus(e) {
if (this.contains(e.relatedTarget))
return;
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, false);
this.focusTarget = e.relatedTarget;
}
// Key pressed
onKeyDown(e) {
if (e.key != "Tab")
return;
e.stopPropagation();
e.preventDefault();
this.exit();
}
///////////////////////////// Public Methods //////////////////////////////
// Produce a list of child items
listItems(invisible) {
return this.children.filter(c=>
c instanceof Toolkit.MenuItem &&
(invisible || c.isVisible())
);
}
///////////////////////////// Package Methods /////////////////////////////
// Replacement behavior for add()
addHook(component) {
component.menuBar = this.menuBar;
this.append(component);
if (component instanceof Toolkit.MenuItem && component.menu != null)
this.append(component.menu);
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, true);
}
// Return control to the application
exit() {
this.onBlur({ relatedTarget: null });
if (this.focusTarget)
this.focusTarget.focus();
else document.activeElement.blur();
}
// Replacement behavior for remove()
removeHook(component) {
component.element.remove();
if (component instanceof Toolkit.MenuItem && component.menu != null)
component.menu.element.remove();
let items = this.listItems();
if (items[0])
items[0].setFocusable(true, true);
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
///////////////////////////// Private Methods /////////////////////////////
// Retrieve the currently expanded menu, if any
expandedMenu() {
return this.children.filter(c=>c.isExpanded)[0] || null;
}
};
export { Menu, MenuBar, MenuItem, MenuSeparator };

File diff suppressed because it is too large Load Diff

View File

@ -1,145 +0,0 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// TextBox //
///////////////////////////////////////////////////////////////////////////////
// Text entry field
class TextBox extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-textbox",
tagName : "input"
});
this.element.type = "text";
this.setAttribute("spellcheck", "false");
// Configure instance fields
this.isEnabled = null;
this.maxLength = null;
this.pattern = null;
// Configure component
options = options || {};
this.setEnabled(!("enabled" in options) || options.enabled);
if ("maxLength" in options)
this.setMaxLength(options.maxLength);
if ("pattern" in options)
this.setPattern(options.pattern);
this.setText (options.text);
// Configure event handlers
this.addEventListener("blur" , e=>this.commit ( ));
this.addEventListener("pointerdown", e=>e.stopPropagation( ));
this.addEventListener("keydown" , e=>this.onKeyDown (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Processing by key
switch (e.key) {
case "ArrowLeft":
case "ArrowRight":
e.stopPropagation();
return;
case "Enter":
this.commit();
break;
default: return;
}
// Configure event
e.stopPropagation();
e.preventDefault();
}
///////////////////////////// Public Methods //////////////////////////////
// Programmatically commit the text box
commit() {
this.event("action");
}
// Retrieve the control's value
getText() {
return this.element.value;
}
// Specify whether the button can be activated
setEnabled(enabled) {
this.isEnabled = enabled = !!enabled;
this.setAttribute("disabled", enabled ? null : "true");
}
// Specify the maximum length of the text
setMaxLength(length) {
// Remove limitation
if (length === null) {
this.maxLength = null;
this.setAttribute("maxlength", null);
return;
}
// Error checking
if (typeof length != "number" || isNaN(length))
return;
// Range checking
length = Math.floor(length);
if (length < 0)
return;
// Configure component
this.maxLength = length;
this.setAttribute("maxlength", length);
}
// Specify a regex pattern for valid text characters
setPattern(pattern) {
/*
Disabled because user agents may not prevent invalid input
// Error checking
if (pattern && typeof pattern != "string")
return;
// Configure component
this.pattern = pattern = pattern || null;
this.setAttribute("pattern", pattern);
*/
}
// Specify the widget's display text
setText(text = "") {
this.element.value = text.toString();
}
///////////////////////////// Package Methods /////////////////////////////
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
}
export { TextBox };

View File

@ -1,337 +0,0 @@
import { Component } from /**/"./Component.js";
import { Button, CheckBox, Group, Radio } from /**/"./Button.js" ;
import { DropDown } from /**/"./DropDown.js" ;
import { Menu, MenuBar, MenuItem, MenuSeparator } from /**/"./MenuBar.js" ;
import { ScrollBar, ScrollPane, SplitPane } from /**/"./ScrollBar.js";
import { TextBox } from /**/"./TextBox.js" ;
import { Desktop, Window } from /**/"./Window.js" ;
///////////////////////////////////////////////////////////////////////////////
// Toolkit //
///////////////////////////////////////////////////////////////////////////////
// Top-level user interface manager
let Toolkit = globalThis.Toolkit = (class GUI extends Component {
static initializer() {
// Static state
this.nextId = 0;
// Locale presets
this.NO_LOCALE = { id: "(Null)" };
// Component classes
this.components = [];
Button .setToolkit(this); this.components.push(Button .Component);
Component.setToolkit(this); this.components.push( Component);
DropDown .setToolkit(this); this.components.push(DropDown .Component);
MenuBar .setToolkit(this); this.components.push(MenuBar .Component);
ScrollBar.setToolkit(this); this.components.push(ScrollBar.Component);
TextBox .setToolkit(this); this.components.push(TextBox .Component);
Window .setToolkit(this); this.components.push(Window .Component);
this.Button = Button;
this.CheckBox = CheckBox;
this.Component = Component;
this.Desktop = Desktop;
this.DropDown = DropDown;
this.Group = Group;
this.Menu = Menu;
this.MenuBar = MenuBar;
this.MenuItem = MenuItem;
this.MenuSeparator = MenuSeparator;
this.Radio = Radio;
this.ScrollBar = ScrollBar;
this.ScrollPane = ScrollPane;
this.SplitPane = SplitPane;
this.TextBox = TextBox;
this.Window = Window;
return this;
}
///////////////////////////// Static Methods //////////////////////////////
// Monitor resize events on an element
static addResizeListener(element, listener) {
// Establish a ResizeObserver
if (!("resizeListeners" in element)) {
element.resizeListeners = [];
element.resizeObserver = new ResizeObserver(
(e,o)=>element.dispatchEvent(this.event("resize")));
element.resizeObserver.observe(element);
}
// Associate the listener
if (element.resizeListeners.indexOf(listener) == -1) {
element.resizeListeners.push(listener);
element.addEventListener("resize", listener);
}
}
// Stop monitoring resize events on an element
static clearResizeListeners(element) {
while ("resizeListeners" in element)
this.removeResizeListener(element, element.resizeListeners[0]);
}
// Produce a custom event object
static event(type, component, fields) {
let event = new Event(type, {
bubbles : true,
cancelable: true
});
if (component)
event.component = component;
if (fields)
Object.assign(event, fields);
return event;
}
// Produce a unique element ID
static id() {
return "tk" + (this.nextId++);
}
// Determine whether an object is a component
// The user agent may not resolve imports to the same classes
static isComponent(o) {
return !!this.components.find(c=>o instanceof c);
}
// Determine whether a pointer event is inside an element
static isInside(element, e) {
let bounds = element.getBoundingClientRect();
return (
e.offsetX >= 0 && e.offsetX < bounds.width &&
e.offsetY >= 0 && e.offsetY < bounds.height
);
}
// Generate a list of focusable child elements
static listFocusables(element) {
return Array.from(element.querySelectorAll(
"*:not(*:not(a[href], area, button, details, input, " +
"textarea, select, [tabindex='0'])):not([disabled])"
)).filter(e=>{
for (; e instanceof Element; e = e.parentNode) {
let style = getComputedStyle(e);
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
});
}
// Stop monitoring resize events on an element
static removeResizeListener(element, listener) {
// Error checking
if (!("resizeListeners" in element))
return;
let index = element.resizeListeners.indexOf(listener);
if (index == -1)
return;
// Remove the listener
element.removeEventListener("resize", element.resizeListeners[index]);
element.resizeListeners.splice(index, 1);
// No more listeners: delete the ResizeObserver
if (element.resizeListeners.length == 0) {
element.resizeObserver.unobserve(element);
delete element.resizeListeners;
delete element.resizeObserver;
}
}
// Compute pointer event screen coordinates
static screenCoords(e) {
return {
x: e.screenX / window.devicePixelRatio,
y: e.screenY / window.devicePixelRatio
};
}
///////////////////////// Initialization Methods //////////////////////////
constructor(options) {
super(null, options);
// Configure instance fields
this.locale = Toolkit.NO_LOCALE;
this.localized = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Specify the locale to use for translated strings
setLocale(locale) {
this.locale = locale || Toolkit.NO_LOCALE;
for (let component of this.localized)
component.translate();
}
// Translate a string in the selected locale
translate(key, component) {
// Front-end method
if (key === undefined) {
super.translate();
return;
}
// Working variables
let subs = component ? component.substitutions : {};
key = (key || "").toString().trim();
// Error checking
if (this.locale == null || key == "")
return key;
// Resolve the key first in the substitutions then in the locale
let text = key;
key = key.toLowerCase();
if (key in subs)
text = subs[key];
else if (key in this.locale)
text = this.locale[key];
else return "!" + text.toUpperCase();
// Process all substitutions
for (;;) {
// Working variables
let sIndex = 0;
let rIndex = -1;
let lIndex = -1;
let zIndex = -1;
// Locate the inner-most {} or [] pair
for (;;) {
let match = Toolkit.subCtrl(text, sIndex);
// No control characters found
if (match == -1)
break;
sIndex = match + 1;
// Processing by control character
switch (text.charAt(match)) {
// Opening a substitution group
case "{": rIndex = match; continue;
case "[": lIndex = match; continue;
// Closing a recursion group
case "}":
if (rIndex != -1) {
lIndex = -1;
zIndex = match;
}
break;
// Closing a literal group
case "]":
if (lIndex != -1) {
rIndex = -1;
zIndex = match;
}
break;
}
break;
}
// Process a recursion substitution
if (rIndex != -1) {
text =
text.substring(0, rIndex) +
this.translate(
text.substring(rIndex + 1, zIndex),
component
) +
text.substring(zIndex + 1)
;
}
// Process a literal substitution
else if (lIndex != -1) {
text =
text.substring(0, lIndex) +
text.substring(lIndex + 1, zIndex)
.replaceAll("{", "{{")
.replaceAll("}", "}}")
.replaceAll("[", "[[")
.replaceAll("]", "]]")
+
text.substring(zIndex + 1)
;
}
// No more substitutions
else break;
}
// Unescape all remaining control characters
return (text
.replaceAll("{{", "{")
.replaceAll("}}", "}")
.replaceAll("[[", "[")
.replaceAll("]]", "]")
);
}
///////////////////////////// Package Methods /////////////////////////////
// Reduce an object to a single level of depth
static flatten(obj, ret = {}, id) {
for (let entry of Object.entries(obj)) {
let key = (id ? id + "." : "") + entry[0].toLowerCase();
let value = entry[1];
if (value instanceof Object)
this.flatten(value, ret, key);
else ret[key] = value;
}
return ret;
}
// Register a component for localization management
localize(component) {
if (this.localized.indexOf(component) != -1)
return;
this.localized.push(component);
component.translate();
}
// Locate a substitution control character in a string
static subCtrl(text, index) {
for (; index < text.length; index++) {
let c = text.charAt(index);
if ("{}[]".indexOf(c) == -1)
continue;
if (index < text.length - 1 || text.charAt(index + 1) != c)
return index;
index++;
}
return -1;
}
}).initializer();
export { Toolkit };

View File

@ -1,699 +0,0 @@
import { Component } from /**/"./Component.js";
let Toolkit;
///////////////////////////////////////////////////////////////////////////////
// Window //
///////////////////////////////////////////////////////////////////////////////
// Standalone movable dialog
class Window extends Component {
static Component = Component;
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className : "tk tk-window",
focusable : true,
role : "dialog",
tabStop : false,
tagName : "div",
visibility: true,
style : {
position: "absolute"
}
});
// Configure instance fields
this.firstShown = false;
this.lastFocus = null;
// DOM container
this.contents = document.createElement("div");
this.contents.style.display = "flex";
this.contents.style.flexDirection = "column";
this.element.append(this.contents);
// Sizing borders
this.borders = {}
this.border("n" ); this.border("w" );
this.border("e" ); this.border("s" );
this.border("nw"); this.border("ne");
this.border("sw"); this.border("se");
// Title bar
this.titleBar = document.createElement("div");
this.titleBar.className = "tk tk-title";
this.titleBar.style.display = "flex";
this.contents.append(this.titleBar);
this.titleBar.addEventListener(
"pointerdown", e=>this.onTitlePointerDown(e));
this.titleBar.addEventListener(
"pointermove", e=>this.onTitlePointerMove(e));
this.titleBar.addEventListener(
"pointerup" , e=>this.onTitlePointerUp (e));
// Title bar text
this.titleText = document.createElement("div");
this.titleText.className = "tk tk-text";
this.titleText.id = Toolkit.id();
this.titleText.style.flexGrow = "1";
this.titleText.style.position = "relative";
this.titleBar.append(this.titleText);
this.setAttribute("aria-labelledby", this.titleText.id);
// Close button
this.titleClose = document.createElement("div");
this.titleClose.className = "tk tk-close";
this.titleClose.setAttribute("aria-hidden", "true");
this.titleBar.append(this.titleClose);
this.titleClose.addEventListener(
"pointerdown", e=>this.onClosePointerDown(e));
this.titleClose.addEventListener(
"pointermove", e=>this.onClosePointerMove(e));
this.titleClose.addEventListener(
"pointerup" , e=>this.onClosePointerUp (e));
// Window client area
this.client = document.createElement("div");
this.client.className = "tk tk-client";
this.client.style.flexGrow = "1";
this.client.style.minHeight = "0";
this.client.style.minWidth = "0";
this.client.style.overflow = "hidden";
this.client.style.position = "relative";
this.contents.append(this.client);
// User agent behavior override
let observer = new ResizeObserver(
()=>this.titleBar.style.width =
this.client.getBoundingClientRect().width + "px"
);
observer.observe(this.client);
// Configure element
this.setAttribute("aria-modal", "false");
this.setBounds(
options.x , options.y,
options.width, options.height
);
// Configure component
this.gui.localize(this);
this.setTitle (options.title );
this.setCloseToolTip(options.closeToolTip);
this.addEventListener("focus" , e=>this.onFocus(e), true);
this.addEventListener("keydown" , e=>this.onWindowKeyDown (e));
this.addEventListener("pointerdown", e=>this.onWindowPointerDown(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Border pointer down
onBorderPointerDown(e, edge) {
if (e.target.hasPointerCapture(e.pointerId) || e.button != 0)
return;
e.target.setPointerCapture(e.pointerId);
e.preventDefault();
let bndClient = this.client.getBoundingClientRect();
let bndWindow = this .getBounds ();
let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow;
let coords = Toolkit.screenCoords(e);
this.drag = {
clickX : coords.x,
clickY : coords.y,
mode : "resize",
pointerId : e.pointerId,
startHeight: bndClient.height,
startWidth : bndClient.width,
startX : bndWindow.x - bndDesktop.x,
startY : bndWindow.y - bndDesktop.y,
target : e.target
};
}
// Border pointer move
onBorderPointerMove(e, edge) {
if (!e.target.hasPointerCapture(e.pointerId))
return;
e.stopPropagation();
e.preventDefault();
let bndWindow = this.getBounds();
this["resize" + edge.toUpperCase()](
Toolkit.screenCoords(e),
this.client .getBoundingClientRect(),
this.parent ? this.parent.getBounds() : bndWindow,
bndWindow,
this.titleBar.getBoundingClientRect()
);
}
// Border pointer up
onBorderPointerUp(e, edge) {
if (!e.target.hasPointerCapture(e.pointerId) || e.button != 0)
return;
e.target.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
}
// Close pointer down
onClosePointerDown(e) {
if (this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleClose.setPointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
this.titleClose.classList.add("active");
this.drag = {
mode: "close",
x : e.offsetX,
y : e.offsetY
};
}
// Close pointer move
onClosePointerMove(e) {
if (!this.titleClose.hasPointerCapture(e.pointerId))
return;
e.stopPropagation();
e.preventDefault();
if (Toolkit.isInside(this.titleClose, e))
this.titleClose.classList.add("active");
else this.titleClose.classList.remove("active");
}
// Close pointer up
onClosePointerUp(e) {
if (!this.titleClose.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleClose.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
this.titleClose.classList.remove("active");
if (Toolkit.isInside(this.titleClose, e))
this.element.dispatchEvent(Toolkit.event("close", this));
this.drag = null;
}
// Focus capture
onFocus(e) {
// Bring this window to the foreground of its siblings
if (!this.contains(e.relatedTarget) && this.parent)
this.parent.bringToFront(this);
// The target is not the window itself
if (e.target != this.element) {
this.lastFocus = e.target;
return;
}
// Select the first focusable child
if (this.lastFocus == null)
this.lastFocus = Toolkit.listFocusables(this.element)[0] || null;
// Send focus to the most recently focused element
if (this.lastFocus)
this.lastFocus.focus();
}
// Title pointer down
onTitlePointerDown(e) {
if (this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleBar.setPointerCapture(e.pointerId);
e.preventDefault();
let bndWindow = this.getBounds();
let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow;
let coords = Toolkit.screenCoords(e);
this.drag = {
clickX : coords.x,
clickY : coords.y,
mode : "move",
pointerId: e.pointerId,
startX : bndWindow.x - bndDesktop.x,
startY : bndWindow.y - bndDesktop.y
};
}
// Title pointer move
onTitlePointerMove(e) {
if (!this.titleBar.hasPointerCapture(e.pointerId))
return;
e.stopPropagation();
e.preventDefault();
let coords = Toolkit.screenCoords(e);
let valid = this.getValidLocations(
this.drag.startX + coords.x - this.drag.clickX,
this.drag.startY + coords.y - this.drag.clickY
);
this.setLocation(valid.x, valid.y);
}
// Title pointer up
onTitlePointerUp(e) {
if (!this.titleBar.hasPointerCapture(e.pointerId) || e.button != 0)
return;
this.titleBar.releasePointerCapture(e.pointerId);
e.stopPropagation();
e.preventDefault();
this.drag = null;
}
// Window key press
onWindowKeyDown(e) {
// Process by key
switch (e.key) {
// Undo un-committed bounds modifications
case "Escape":
// Not dragging
if (this.drag == null)
return;
// Moving
if (this.drag.mode == "move") {
this.titleBar.releasePointerCapture(this.drag.pointerId);
this.setLocation(this.drag.startX, this.drag.startY);
this.drag = null;
}
// Resizing
else if (this.drag.mode == "resize") {
this.drag.target
.releasePointerCapture(this.drag.pointerId);
this.setBounds(
this.drag.startX , this.drag.startY,
this.drag.startWidth, this.drag.startHeight
);
this.drag = null;
}
break;
// Transfer focus to another element
case "Tab":
default: return;
}
// The event was handled
e.stopPropagation();
e.preventDefault();
}
// Window pointer down
onWindowPointerDown(e) {
this.focus(e);
e.stopPropagation();
e.preventDefault();
}
///////////////////////////// Public Methods //////////////////////////////
// Add a DOM element to this component's element
append(child) {
let element = child instanceof Element ? child : child.element;
this.client.append(element);
}
// Position the window in the center of the parent Desktop
center() {
if (!this.parent)
return;
let bndParent = this.parent.getBounds();
let bndWindow = this .getBounds();
this.setLocation(
Math.max(Math.floor((bndParent.width - bndWindow.width ) / 2), 0),
Math.max(Math.floor((bndParent.height - bndWindow.height) / 2), 0)
);
}
// Programmatically close the window
close() {
this.event("close");
}
// Add a DOM element to the beginning of this component's children
prepend(child) {
let element = child instanceof Element ? child : child.element;
this.element.prepend(element);
}
// Specify a new position and size for the window
setBounds(x, y, width, height) {
this.setSize(width, height);
this.setLocation(x, y);
}
// Specify the over text for the close button
setCloseToolTip(key) {
this.closeToolTip = key;
this.translate();
}
// Specify a new position for the window
setLocation(x, y) {
Object.assign(this.element.style, {
left: Math.round(parseFloat(x) || 0) + "px",
top : Math.round(parseFloat(y) || 0) + "px"
});
}
// Specify a new size for the window
setSize(width, height) {
Object.assign(this.client.style, {
width : Math.max(Math.round(parseFloat(width ) || 0, 32)) + "px",
height: Math.max(Math.round(parseFloat(height) || 0, 32)) + "px"
});
}
// Specify the window title text
setTitle(key) {
this.title = key;
this.translate();
}
// Specify whether the component is visible
setVisible(visible) {
super.setVisible(visible);
if (!visible || this.firstShown)
return;
this.firstShown = true;
this.event("firstshow", this);
}
///////////////////////////// Package Methods /////////////////////////////
// Ensure the window is partially visible within its desktop
contain() {
let valid = this.getValidLocations();
this.setLocation(valid.x, valid.y);
}
// Determine the range of valid window coordinates
getValidLocations(x, y) {
// Measure the bounding boxes of the relevant elements
let bndClient = this.client .getBoundingClientRect();
let bndWindow = this .getBounds ();
let bndTitleBar = this.titleBar.getBoundingClientRect();
let bndDesktop = this.parent ? this.parent.getBounds() : bndWindow;
// Compute the minimum and maximum valid window coordinates
let ret = {
maxX: bndDesktop .width - bndTitleBar.height -
bndTitleBar.x + bndWindow .x,
maxY: bndDesktop .height - bndClient .y +
bndWindow .y,
minX: bndTitleBar.height - bndWindow .width +
bndWindow .right - bndTitleBar.right,
minY: 0
};
// Compute the effective "best" window coordinates
ret.x = Math.max(ret.minX, Math.min(ret.maxX,
x === undefined ? bndWindow.x - bndDesktop.x : x));
ret.y = Math.max(ret.minY, Math.min(ret.maxY,
y === undefined ? bndWindow.y - bndDesktop.y : y));
return ret;
}
// Update the global Toolkit object
static setToolkit(toolkit) {
Toolkit = toolkit;
}
// Regenerate localized display text
translate() {
if (!this.titleText)
return;
this.titleText.innerText = this.gui.translate(this.title, this);
if (this.closeToolTip)
this.titleClose.setAttribute("title",
this.gui.translate(this.closeToolTip, this));
else this.titleClose.removeAttribute("title");
}
///////////////////////////// Private Methods /////////////////////////////
// Produce a border element and add it to the window
border(edge) {
let border = this.borders[edge] = document.createElement("div");
border.className = "tk tk-" + edge;
border.style.cursor = edge + "-resize";
border.style.position = "absolute";
this.contents.append(border);
border.addEventListener(
"pointerdown", e=>this.onBorderPointerDown(e, edge));
border.addEventListener(
"pointermove", e=>this.onBorderPointerMove(e, edge));
border.addEventListener(
"pointerup" , e=>this.onBorderPointerUp (e, edge));
}
// Compute client bounds when resizing on the east border
constrainE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let w = this.drag.startWidth + coords.x - this.drag.clickX;
w = Math.max(w, bndTitleBar.height * 4);
if (bndClient.x - bndDesktop.x < 0)
w = Math.max(w, bndDesktop.x - bndClient.x + bndTitleBar.height);
return w;
}
// Compute client bounds when resizing on the north border
constrainN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let delta = coords.y - this.drag.clickY;
let y = this.drag.startY + delta;
let h = this.drag.startHeight - delta;
let min = Math.max(0, bndClient.bottom - bndDesktop.bottom);
if (h < min) {
delta = min - h;
h += delta;
y -= delta;
}
if (y < 0) {
h += y;
y = 0;
}
return {
height: h,
y : y
};
}
// Compute client bounds when resizing on the south border
constrainS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
return Math.max(0, this.drag.startHeight+coords.y-this.drag.clickY);
}
// Compute client bounds when resizing on the west border
constrainW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let delta = coords.x - this.drag.clickX;
let x = this.drag.startX + delta;
let w = this.drag.startWidth - delta;
let min = bndTitleBar.height * 4;
if (bndClient.right - bndDesktop.right > 0) {
min = Math.max(min, bndClient.right -
bndDesktop.right + bndTitleBar.height);
}
if (w < min) {
delta = min - w;
w += delta;
x -= delta;
}
return {
x : x,
width: w
};
}
// Resize on the east border
resizeE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
this.setSize(
this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
this.drag.startHeight
);
}
// Resize on the north border
resizeN(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainN(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
this.drag.startX , con.y,
this.drag.startWidth, con.height
);
}
// Resize on the northeast border
resizeNE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainN(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
this.drag.startX, con.y,
this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
con.height
);
}
// Resize on the northwest border
resizeNW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let conN = this.constrainN(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
let conW = this.constrainW(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(conW.x, conN.y, conW.width, conN.height);
}
// Resize on the south border
resizeS(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
this.setSize(
this.drag.startWidth,
this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
);
}
// Resize on the southeast border
resizeSE(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
this.setSize(
this.constrainE(coords,bndClient,bndDesktop,bndWindow,bndTitleBar),
this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar)
);
}
// Resize on the southwest border
resizeSW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainW(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
con.x , this.drag.startY,
con.width,
this.constrainS(coords,bndClient,bndDesktop,bndWindow,bndTitleBar)
);
}
// Resize on the west border
resizeW(coords, bndClient, bndDesktop, bndWindow, bndTitleBar) {
let con = this.constrainW(coords,
bndClient, bndDesktop, bndWindow, bndTitleBar);
this.setBounds(
con.x , this.drag.startY,
con.width, this.drag.startHeight
);
}
};
///////////////////////////////////////////////////////////////////////////////
// Desktop //
///////////////////////////////////////////////////////////////////////////////
// Parent container for encapsulating groups of Windows
class Desktop extends Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(gui, options) {
super(gui, options, {
className: "tk tk-desktop",
role : "group",
tagName : "div",
style : {
position: "relative"
}
});
// Configure event handlers
this.addEventListener("resize", e=>this.onResize(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Element resized
onResize(e) {
for (let wnd of this.children)
wnd.contain();
}
///////////////////////////// Public Methods //////////////////////////////
// Re-order windows to bring a particular one to the foreground
bringToFront(wnd) {
// The window is not a child of this Desktop
let index = this.children.indexOf(wnd);
if (index == -1)
return;
// The window is already in the foreground
let afters = this.children.slice(index + 1).map(c=>c.element);
if (afters.length == 0)
return;
// Record scroll pane positions
let scrolls = [];
for (let after of afters)
for (let scroll of
after.querySelectorAll(".tk-scrollpane > .tk-viewport")) {
scrolls.push({
element: scroll,
left : scroll.scrollLeft,
top : scroll.scrollTop
});
}
// Update window collection
wnd.element.before(... this.children.slice(index+1).map(c=>c.element));
this.children.splice(index, 1);
this.children.push(wnd);
// Restore scroll pane positions
for (let scroll of scrolls) {
Object.assign(scroll.element, {
scrollLeft: scroll.left,
scrollTop : scroll.top
});
}
}
// Position a window in the center of the viewable area
center(wnd) {
// The window is not a child of the desktop pane
if (this.children.indexOf(wnd) == -1)
return;
let bndDesktop = this.getBounds();
let bndWindow = wnd .getBounds();
wnd.setLocation(
Math.max(0, Math.round((bndDesktop.width - bndWindow.width) / 2)),
Math.max(0, Math.round((bndDesktop.height-bndWindow.height) / 2))
);
}
};
export { Desktop, Window };

Binary file not shown.

View File

@ -1701,51 +1701,4 @@ static int cpuEmulate(VB *sim, uint32_t clocks) {
}
/* Simulate a hardware reset */
static void cpuReset(VB *sim) {
int x; /* Iterator */
/* Registers */
vbSetProgramCounter(sim, 0xFFFFFFF0);
vbSetSystemRegister(sim, VB_ECR, 0x0000FFF0);
vbSetSystemRegister(sim, VB_PSW, 0x00008000);
/* Cache */
vbSetSystemRegister(sim, VB_CHCW, 0x00000000);
/* Other state */
for (x = 0; x < 5; x++)
sim->cpu.irq[x] = 0;
/* Other registers (the hardware does not do this) */
for (x = 0; x < 32; x++)
sim->cpu.program[x] = 0x00000000;
sim->cpu.adtre = 0x00000000;
sim->cpu.eipc = 0x00000000;
sim->cpu.eipsw = 0x00000000;
sim->cpu.fepc = 0x00000000;
sim->cpu.fepsw = 0x00000000;
sim->cpu.sr29 = 0x00000000;
sim->cpu.sr31 = 0x00000000;
}
/* Determine the number of clocks before a break condititon could occur */
static uint32_t cpuUntil(VB *sim, uint32_t clocks) {
/* Cannot break */
if (
sim->cpu.state == CPU_HALTED ||
sim->cpu.state == CPU_FATAL || (
sim->onException == NULL &&
sim->onExecute == NULL &&
sim->onFetch == NULL &&
sim->onRead == NULL &&
sim->onWrite == NULL
)) return clocks;
/* Will not break before next operation */
return sim->cpu.clocks < clocks ? sim->cpu.clocks : clocks;
}
#endif /* WSAPI */
static void cpuReset(VB *sim)

View File

@ -284,25 +284,4 @@ int vbSetROM(VB *sim, void *rom, uint32_t size) {
}
/* Supply an SRAM buffer */
int vbSetSRAM(VB *sim, void *sram, uint32_t size) {
/* Check the buffer size */
if (size == 0 || ((size - 1) & size) != 0)
return 0;
/* Configure the SRAM buffer */
sim->cart.sram = (uint8_t *) sram;
sim->cart.sramSize = size;
return 1;
}
/* Specify a new value for a system register */
uint32_t vbSetSystemRegister(VB *sim, int id, uint32_t value) {
return cpuSetSystemRegister(sim, id, value, 1);
}
/* Write a data unit to the bus */
void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) {
if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES)
busWrite(sim, address, type, value, debug);
}
int vbSetSRAM(VB *sim, void *sram, uint32_t size)

BIN
core/vb.h

Binary file not shown.

BIN
makefile

Binary file not shown.

View File

@ -1,126 +0,0 @@
#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <emscripten/emscripten.h>
#include <vb.h>
/////////////////////////////// Module Commands ///////////////////////////////
// Allocate and initialize multiple simulations
EMSCRIPTEN_KEEPALIVE VB** Create(int count) {
VB **sims = malloc(count * sizeof (void *));
for (int x = 0; x < count; x++)
vbReset(sims[x] = malloc(sizeof (VB)));
return sims;
}
// Delete a simulation
EMSCRIPTEN_KEEPALIVE void Destroy(VB *sim) {
free(&sim->cart.rom);
free(&sim->cart.sram);
free(sim);
}
// Proxy for free()
EMSCRIPTEN_KEEPALIVE void Free(void *ptr) {
free(ptr);
}
// Proxy for malloc()
EMSCRIPTEN_KEEPALIVE void* Malloc(int size) {
return malloc(size);
}
// Determine the size in bytes of a pointer
EMSCRIPTEN_KEEPALIVE int PointerSize() {
return sizeof (void *);
}
// Read multiple bytes from the bus
EMSCRIPTEN_KEEPALIVE void ReadBuffer(
VB* sim, uint8_t *dest, uint32_t address, uint32_t size) {
for (; size > 0; address++, size--, dest++)
*dest = vbRead(sim, address, VB_U8, 1);
}
// Supply a ROM buffer
EMSCRIPTEN_KEEPALIVE int SetROM(VB *sim, uint8_t *rom, uint32_t size) {
uint8_t *prev = vbGetROM(sim, NULL);
int ret = vbSetROM(sim, rom, size);
if (ret) {
free(prev);
vbReset(sim);
}
return ret;
}
// Write multiple bytes to the bus
EMSCRIPTEN_KEEPALIVE void WriteBuffer(
VB* sim, uint8_t *src, uint32_t address, uint32_t size) {
for (; size > 0; address++, size--, src++)
vbWrite(sim, address, VB_U8, *src, 1);
}
////////////////////////////// Debugger Commands //////////////////////////////
// Attempt to execute until the following instruction
static uint32_t RunNextPC;
static int RunNextProcB(VB *sim, int fetch, VB_ACCESS *acc) {
if (fetch == 0 && vbGetProgramCounter(sim) == RunNextPC)
return 1;
acc->value = vbRead(sim, acc->address, acc->type, 0);
return 0;
}
static int RunNextProcA(VB *sim, VB_INSTRUCTION *inst) {
RunNextPC = vbGetProgramCounter(sim) + inst->size;
vbSetCallback(sim, VB_ONEXECUTE, NULL);
vbSetCallback(sim, VB_ONFETCH, &RunNextProcB);
return 0;
}
EMSCRIPTEN_KEEPALIVE void RunNext(VB *sim0, VB *sim1) {
uint32_t clocks = 400000; // 1/50s
VB *sims[2];
vbSetCallback(sim0, VB_ONEXECUTE, &RunNextProcA);
if (sim1 != NULL) {
sims[0] = sim0;
sims[1] = sim1;
vbEmulateMulti(sims, 2, &clocks);
}
else vbEmulate(sim0, &clocks);
vbSetCallback(sim0, VB_ONFETCH, NULL);
}
// Execute one instruction
static uint32_t SingleStepPC;
static int SingleStepProc(VB *sim, int fetch, VB_ACCESS *acc) {
if (fetch == 0 && vbGetProgramCounter(sim) != SingleStepPC)
return 1;
acc->value = vbRead(sim, acc->address, acc->type, 0);
return 0;
}
EMSCRIPTEN_KEEPALIVE void SingleStep(VB *sim0, VB *sim1) {
uint32_t clocks = 400000; // 1/50s
VB *sims[2];
SingleStepPC = vbGetProgramCounter(sim0);
vbSetCallback(sim0, VB_ONFETCH, &SingleStepProc);
if (sim1 != NULL) {
sims[0] = sim0;
sims[1] = sim1;
vbEmulateMulti(sims, 2, &clocks);
}
else vbEmulate(sim0, &clocks);
vbSetCallback(sim0, VB_ONFETCH, NULL);
}

680
web/App.js Normal file
View File

@ -0,0 +1,680 @@
import { Core } from /**/"./core/Core.js";
import { Debugger } from /**/"./debugger/Debugger.js";
import { Disassembler } from /**/"./core/Disassembler.js";
import { Toolkit } from /**/"./toolkit/Toolkit.js";
// Front-end emulator application
class App extends Toolkit.App {
///////////////////////// Initialization Methods //////////////////////////
constructor(bundle) {
super({
style: {
display : "grid",
gridTemplateRows: "max-content auto"
},
visibility: true,
visible : false
});
// Configure instance fields
this.bundle = bundle;
this.debugMode = true;
this.dualMode = false;
this.text = null;
}
async init() {
// Theme
Object.assign(document.body.style, { margin:"0", overflow:"hidden" });
this.stylesheet(/**/"web/theme/kiosk.css", false);
this.stylesheet(/**/"web/theme/vbemu.css", false);
this._theme = "auto";
this.themes = {
dark : this.stylesheet(/**/"web/theme/dark.css" ),
light : this.stylesheet(/**/"web/theme/light.css" ),
virtual: this.stylesheet(/**/"web/theme/virtual.css")
};
// Watch for dark mode preference changes
this.isDark = window.matchMedia("(prefers-color-scheme: dark)");
this.isDark.addEventListener("change", e=>this.onDark());
this.onDark();
// Locales
await this.addLocale(/**/"web/locale/en-US.json");
for (let id of [].concat(navigator.languages, ["en-US"])) {
if (this.setLocale(id))
break;
}
this.setTitle("{app.title}", true);
// Element
document.body.append(this.element);
window.addEventListener("resize", e=>{
this.element.style.height = window.innerHeight + "px";
this.element.style.width = window.innerWidth + "px";
});
window.dispatchEvent(new Event("resize"));
this.addEventListener("keydown", e=>this.onKeyDown(e));
// Menus
this.menuBar = new Toolkit.MenuBar(this);
this.menuBar.setLabel("{menu._}", true);
this.add(this.menuBar);
this.initFileMenu ();
this.initEmulationMenu();
this.initDebugMenu (0, this.debugMode);
this.initDebugMenu (1, this.debugMode && this.dualMode);
this.initThemeMenu ();
// Fallback for bubbled key events
document.body.addEventListener("focusout", e=>this.onBlur(e));
window .addEventListener("keydown" , e=>this.onKey (e));
window .addEventListener("keyup" , e=>this.onKey (e));
// Temporary: Faux game mode display
this.display = new Toolkit.Component(this, {
class : "tk display",
style : { position: "relative" },
visible: !this.debugMode
});
this.image1 = new Toolkit.Component(this, { style: {
background: "#000000",
position : "absolute"
}});
this.display.add(this.image1);
this.image2 = new Toolkit.Component(this, { style: {
background: "#000000",
position : "absolute"
}});
this.display.add(this.image2);
this.display.addEventListener("resize", e=>this.onDisplay());
this.add(this.display);
// Temporary: Faux debug mode display
this.desktop = new Toolkit.Desktop(this, {
visible: this.debugMode
});
this.add(this.desktop);
// Emulation core
this.core = await new Core().init();
let sims = (await this.core.create(2)).sims;
this.core.onsubscription = (k,m)=>this.onSubscription(k, m);
// Debugging managers
this.dasm = new Disassembler();
this.debug = new Array(sims.length);
for (let x = 0; x < sims.length; x++) {
let dbg = this.debug[x] = new Debugger(this, sims[x], x);
if (x == 0 && !this.dualMode) {
dbg.cpu .substitute("#", "");
dbg.memory.substitute("#", "");
}
this.desktop.add(dbg.cpu);
this.desktop.add(dbg.memory);
}
// Reveal the application
this.visible = true;
this.restoreFocus();
}
// Initialize File menu
initFileMenu() {
let bar = this.menuBar;
let item = bar.file = new Toolkit.MenuItem(this);
item.setText("{menu.file._}");
bar.add(item);
let menu = item.menu = new Toolkit.Menu(this);
item = bar.file.loadROM0 = new Toolkit.MenuItem(this);
item.setText("{menu.file.loadROM}");
item.substitute("#", this.dualMode ? " 1" : "", false);
item.addEventListener("action", e=>this.onLoadROM(0));
menu.add(item);
item = bar.file.loadROM1 = new Toolkit.MenuItem(this,
{ visible: this.dualMode });
item.setText("{menu.file.loadROM}");
item.substitute("#", " 2", false);
item.addEventListener("action", e=>this.onLoadROM(1));
menu.add(item);
item = bar.file.dualMode = new Toolkit.MenuItem(this,
{ checked: this.dualMode, type: "checkbox" });
item.setText("{menu.file.dualMode}");
item.addEventListener("action", e=>this.onDualMode());
menu.add(item);
item = bar.file.debugMode = new Toolkit.MenuItem(this,
{ checked: this.debugMode, disabled: true, type: "checkbox" });
item.setText("{menu.file.debugMode}");
item.addEventListener("action", e=>this.onDebugMode());
menu.add(item);
menu.addSeparator();
item = new Toolkit.MenuItem(this);
item.setText("Export source...", false);
item.addEventListener("action", ()=>this.bundle.save());
menu.add(item);
}
// Initialize Emulation menu
initEmulationMenu() {
let bar = this.menuBar;
let item = bar.emulation = new Toolkit.MenuItem(this);
item.setText("{menu.emulation._}");
bar.add(item);
let menu = item.menu = new Toolkit.Menu(this);
item = bar.emulation.run =
new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.emulation.run}");
menu.add(item);
item = bar.emulation.reset0 = new Toolkit.MenuItem(this);
item.setText("{menu.emulation.reset}");
item.substitute("#", this.dualMode ? " 1" : "", false);
item.addEventListener("action", e=>this.onReset(0));
menu.add(item);
item = bar.emulation.reset1 = new Toolkit.MenuItem(this,
{ visible: this.dualMode });
item.setText("{menu.emulation.reset}");
item.substitute("#", " 2", false);
item.addEventListener("action", e=>this.onReset(1));
menu.add(item);
item = bar.emulation.linkSims = new Toolkit.MenuItem(this,
{ disabled: true, type: "checkbox", visible: this.dualMode });
item.setText("{menu.emulation.linkSims}");
menu.add(item);
}
// Initialize Debug menu
initDebugMenu(index, visible) {
let bar = this.menuBar;
let item = bar["debug" + index] = new Toolkit.MenuItem(this,
{ visible: visible }), top = item;
item.setText("{menu.debug._}");
item.substitute("#",
index == 0 ? this.dualMode ? "1" : "" : " 2", false);
bar.add(item);
let menu = item.menu = new Toolkit.Menu(this);
item = top.console = new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.debug.console}");
menu.add(item);
item = top.memory = new Toolkit.MenuItem(this);
item.setText("{menu.debug.memory}");
item.addEventListener("action",
e=>this.showWindow(this.debug[index].memory));
menu.add(item);
item = top.cpu = new Toolkit.MenuItem(this);
item.setText("{menu.debug.cpu}");
item.addEventListener("action",
e=>this.showWindow(this.debug[index].cpu));
menu.add(item);
item=top.breakpoints = new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.debug.breakpoints}");
menu.add(item);
menu.addSeparator();
item = top.palettes = new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.debug.palettes}");
menu.add(item);
item = top.characters = new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.debug.characters}");
menu.add(item);
item = top.bgMaps = new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.debug.bgMaps}");
menu.add(item);
item = top.backgrounds = new Toolkit.MenuItem(this, { disabled:true });
item.setText("{menu.debug.backgrounds}");
menu.add(item);
item = top.objects = new Toolkit.MenuItem(this, { disabled: true });
item.setText("{menu.debug.objects}");
menu.add(item);
item = top.frameBuffers = new Toolkit.MenuItem(this, {disabled: true});
item.setText("{menu.debug.frameBuffers}");
menu.add(item);
}
// Initialize Theme menu
initThemeMenu() {
let bar = this.menuBar;
let item = bar.theme = new Toolkit.MenuItem(this);
item.setText("{menu.theme._}");
bar.add(item);
let menu = item.menu = new Toolkit.Menu(this);
item = bar.theme.auto = new Toolkit.MenuItem(this,
{ checked: true, type: "checkbox" });
item.setText("{menu.theme.auto}");
item.theme = "auto";
item.addEventListener("action", e=>this.theme = "auto");
menu.add(item);
item = bar.theme.light = new Toolkit.MenuItem(this,
{ checked: false, type: "checkbox" });
item.setText("{menu.theme.light}");
item.theme = "light";
item.addEventListener("action", e=>this.theme = "light");
menu.add(item);
item = bar.theme.dark = new Toolkit.MenuItem(this,
{ checked: false, type: "checkbox" });
item.setText("{menu.theme.dark}");
item.theme = "dark";
item.addEventListener("action", e=>this.theme = "dark");
menu.add(item);
item = bar.theme.light = new Toolkit.MenuItem(this,
{ checked: false, type: "checkbox" });
item.setText("{menu.theme.virtual}");
item.theme = "virtual";
item.addEventListener("action", e=>this.theme = "virtual");
menu.add(item);
}
///////////////////////////// Event Handlers //////////////////////////////
// All elements have lost focus
onBlur(e) {
if (
e.relatedTarget == null ||
e.relatedTarget == document.body
) this.restoreFocus();
}
// Dark mode preference changed
onDark() {
if (this._theme != "auto")
return;
let isDark = this.isDark.matches;
this.themes.light.disabled = isDark;
this.themes.dark .disabled = !isDark;
}
// Game mode display resized
onDisplay() {
let bounds = this.display.element.getBoundingClientRect();
let width = Math.max(1, bounds.width);
let height = Math.max(1, bounds.height);
let scale, x1, y1, x2, y2;
// Single mode
if (!this.dualMode) {
this.image2.visible = false;
scale = Math.max(1, Math.min(
Math.floor(width / 384),
Math.floor(height / 224)
));
x1 = Math.max(0, Math.floor((width - 384 * scale) / 2));
y1 = Math.max(0, Math.floor((height - 224 * scale) / 2));
x2 = y2 = 0;
}
// Dual mode
else {
this.image2.visible = true;
// Horizontal orientation
if (true) {
scale = Math.max(1, Math.min(
Math.floor(width / 768),
Math.floor(height / 224)
));
let gap = Math.max(0, width - 768 * scale);
gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0);
x1 = gap;
x2 = Math.max(384 * scale, width - 384 * scale - gap);
y1 = y2 = Math.max(0, Math.floor((height - 224 * scale) / 2));
}
// Vertical orientation
else {
scale = Math.max(1, Math.min(
Math.floor(width / 384),
Math.floor(height / 448)
));
let gap = Math.max(0, height - 448 * scale);
gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0);
x1 = x2 = Math.max(0, Math.floor((width - 384 * scale) / 2));
y1 = gap;
y2 = Math.max(224 * scale, height - 224 * scale - gap);
}
}
width = 384 * scale + "px";
height = 224 * scale + "px";
Object.assign(this.image1.element.style,
{ left: x1+"px", top: y1+"px", width: width, height: height });
Object.assign(this.image2.element.style,
{ left: x2+"px", top: y2+"px", width: width, height: height });
}
// File -> Debug mode
onDebugMode() {
this.debugMode =!this.debugMode;
this.display.visible =!this.debugMode;
this.desktop.visible = this.debugMode;
this.configMenus();
this.onDisplay();
}
// Emulation -> Dual mode
onDualMode() {
this.setDualMode(!this.dualMode);
this.configMenus();
this.onDisplay();
}
// Key press
onKeyDown(e) {
// Take no action
if (!e.altKey || e.key != "F10" ||
this.menuBar.contains(document.activeElement))
return;
// Move focus into the menu bar
this.menuBar.focus();
Toolkit.handle(e);
}
// File -> Load ROM
async onLoadROM(index) {
// Add a file picker to the document
let file = document.createElement("input");
file.type = "file";
file.style.position = "absolute";
file.style.visibility = "hidden";
document.body.appendChild(file);
// Prompt the user to select a file
await new Promise(resolve=>{
file.addEventListener("input", resolve);
file.click();
});
file.remove();
// No file was selected
file = file.files[0];
if (!file)
return;
// Load the file
let data = null;
try { data = new Uint8Array(await file.arrayBuffer()); }
catch {
alert(this.localize("{menu.file.loadROMError}"));
return;
}
// Attempt to process the file as an ISX binary
try { data = Debugger.isx(data).toROM(); } catch { }
// Error checking
if (
data.length < 1024 ||
data.length > 0x1000000 ||
(data.length & data.length - 1)
) {
alert(this.localize("{menu.file.loadROMInvalid}"));
return;
}
// Load the ROM into simulation memory
let rep = await this.core.setROM(this.debug[index].sim, data,
{ refresh: true });
if (!rep.success) {
alert(this.localize("{menu.file.loadROMError}"));
return;
}
this.debug[index].followPC(0xFFFFFFF0);
}
// Emulation -> Reset
async onReset(index) {
await this.core.reset(this.debug[index].sim, { refresh: true });
this.debug[index].followPC(0xFFFFFFF0);
}
// Core subscription
onSubscription(key, msg) {
let target = this.debug; // Handler object
for (let x = 1; x < key.length - 1; x++)
target = target[key[x]];
target[key[key.length - 1]].call(target, msg);
}
///////////////////////////// Public Methods //////////////////////////////
// Specify document title
setTitle(title, localize) {
this.setString("text", title, localize);
}
// Specify the color theme
get theme() { return this._theme; }
set theme(theme) {
switch (theme) {
case "light": case "dark": case "virtual":
this._theme = theme;
for (let entry of Object.entries(this.themes))
entry[1].disabled = entry[0] != theme;
break;
default:
this._theme = "auto";
this.themes["virtual"].disabled = true;
this.onDark();
}
for (let item of this.menuBar.theme.menu.children)
item.checked = item.theme == theme;
}
///////////////////////////// Package Methods /////////////////////////////
// Configure components for automatic localization, or localize a message
localize(a, b) {
// Default behavior
if (a && a != this)
return super.localize(a, b);
// Update localization strings
if (this.text != null) {
let text = this.text;
document.title = !text[1] ? text[0] :
this.localize(text[0], this.substitutions);
}
}
// Return focus to the most recent focused element
restoreFocus() {
// Focus was successfully restored
if (super.restoreFocus())
return true;
// Select the foremost visible window
let wnd = this.desktop.getActiveWindow();
if (wnd) {
wnd.focus();
return true;
}
// Select the menu bar
this.menuBar.focus();
return true;
}
// Perform a Run Next command on one of the simulations
runNext(index, options) {
let debugs = [ this.debug[index] ];
if (this.dualMode)
debugs.push(this.debug[index ^ 1]);
let ret = this.core.runNext(debugs.map(d=>d.sim), options);
if (ret instanceof Promise) ret.then(msg=>{
for (let x = 0; x < debugs.length; x++)
debugs[x].followPC(msg.pcs[x]);
});
}
// Perform a Single Step command on one of the simulations
singleStep(index, options) {
let debugs = [ this.debug[index] ];
if (this.dualMode)
debugs.push(this.debug[index ^ 1]);
let ret = this.core.singleStep(debugs.map(d=>d.sim), options);
if (ret instanceof Promise) ret.then(msg=>{
for (let x = 0; x < debugs.length; x++)
debugs[x].followPC(msg.pcs[x]);
});
}
///////////////////////////// Private Methods /////////////////////////////
// Configure menu item visibility
configMenus() {
let bar = this.menuBar;
bar.file.debugMode .checked = this.debugMode;
bar.file.loadROM1 .visible = this.dualMode;
bar.emulation.reset1 .visible = this.dualMode;
bar.emulation.linkSims.visible = this.dualMode;
bar.debug0 .visible = this.debugMode;
bar.debug1 .visible = this.debugMode && this.dualMode;
}
// Specify whether dual mode is active
setDualMode(dualMode) {
// Update state
if (dualMode == this.dualMode)
return;
this.dualMode = dualMode;
// Working variables
let index = dualMode ? " 1" : "";
// Update menus
let bar = this.menuBar;
bar.file.loadROM0 .substitute("#", index, false);
bar.debug0 .substitute("#", index, false);
bar.emulation.reset0.substitute("#", index, false);
bar.file.dualMode .checked = this.dualMode;
// Update sim 1 debug windows
let dbg = this.debug[0];
dbg.cpu .substitute("#", index);
dbg.memory.substitute("#", index);
// Re-show any sim 2 debug windows that were previously visible
if (dualMode) {
for (let wnd of this.desktop.children) {
if (wnd.index == 1 && wnd.wasVisible)
wnd.visible = true;
}
}
// Hide any visible sim 2 debug windows
else for (let wnd of this.desktop.children) {
if (wnd.index == 0)
continue;
wnd.wasVisible = wnd.visible;
wnd.visible = false;
}
}
// Ensure a debugger window is visible
showWindow(wnd) {
let adjust = false;
// The window is already visible
if (wnd.visible) {
// The window is already in the foreground
if (wnd == this.desktop.getActiveWindow())
;//adjust = true;
// Bring the window to the foreground
else this.desktop.bringToFront(wnd);
}
// The window is not visible
else {
adjust = !wnd.shown;
wnd.visible = true;
this.desktop.bringToFront(wnd);
}
// Adjust the window position
if (adjust) {
let bounds = this.desktop.element.getBoundingClientRect();
wnd.left = Math.max(0, (bounds.width - wnd.outerWidth ) / 2);
wnd.top = Math.max(0, (bounds.height - wnd.outerHeight) / 2);
}
// Transfer focus into the window
wnd.focus();
}
// Install a stylesheet. Returns the resulting <link> element
stylesheet(filename, disabled = true) {
let ret = document.createElement("link");
ret.href = filename;
ret.rel = "stylesheet";
ret.type = "text/css";
ret.disabled = disabled;
document.head.append(ret);
return ret;
}
///////////////////////////// Program Methods /////////////////////////////
// Program entry point
static main(bundle) {
new App(bundle).init();
}
}
export { App };

203
web/Bundle.java Normal file
View File

@ -0,0 +1,203 @@
import java.awt.image.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import javax.imageio.*;
public class Bundle {
/////////////////////////////// BundledFile ///////////////////////////////
// Individual packaged resource file
static class BundledFile implements Comparable<BundledFile> {
// Instance fields
byte[] data; // File data loaded from disk
File file; // Source file
String filename; // Logical filename
// Constructor
BundledFile(BundledFile parent, File file) {
// Configure instance fields
this.file = file;
filename = parent == null ? "" : parent.filename + file.getName();
// Load file data if file
if (file.isFile()) {
try (var stream = new FileInputStream(file)) {
data = stream.readAllBytes();
} catch (Exception e) { }
}
// Update filename if directory
else if (parent != null) filename += "/";
}
// Comparator
public int compareTo(BundledFile o) {
return
filename.equals("web/_boot.js") ? -1 :
o.filename.equals("web/_boot.js") ? +1 :
filename.compareTo(o.filename)
;
}
// Produce a list of child files or directories
BundledFile[] listFiles(String name, boolean isDirectory) {
// Produce a filtered list of files
var files = this.file.listFiles(f->{
// Does not satisfy the directory requirement
if (f.isDirectory() != isDirectory)
return false;
// Current directory is not root
if (!filename.equals(""))
return true;
// Filter specific files from being bundled
String filename = f.getName();
return !(
filename.startsWith(".git" ) ||
filename.startsWith(name + "_") &&
filename.endsWith (".html" )
);
});
// Process all files for bundling
var ret = new BundledFile[files.length];
for (int x = 0; x < files.length; x++)
ret[x] = new BundledFile(this, files[x]);
return ret;
}
}
///////////////////////////// Program Methods /////////////////////////////
// Program entry point
public static void main(String[] args) {
String name = name(args[0]);
var files = listFiles(args[0]);
var prepend = prepend(name, files);
var bundle = bundle(prepend, files);
var image = image(bundle);
var url = url(image);
patch(name, url);
}
// Produce a buffer of the bundled files
static byte[] bundle(byte[] prepend, BundledFile[] files) {
try (var stream = new ByteArrayOutputStream()) {
stream.write(prepend);
stream.write(files[0].data); // web/_boot.js
stream.write(0);
for (int x = 1; x < files.length; x++)
stream.write(files[x].data);
return stream.toByteArray();
} catch (Exception e) { return null; }
}
// Convert a bundle buffer into a PNG-encoded image buffer
static byte[] image(byte[] bundle) {
int width = (int) Math.ceil(Math.sqrt(bundle.length));
int height = (bundle.length + width - 1) / width;
var pixels = new int[width * height];
// Encode the buffer as a pixel array
for (int x = 0; x < bundle.length; x++) {
int b = bundle[x] & 0xFF;
pixels[x] = 0xFF000000 | b << 16 | b << 8 | b;
}
// Produce an image using the pixels
var image = new BufferedImage(
width, height, BufferedImage.TYPE_INT_RGB);
image.setRGB(0, 0, width, height, pixels, 0, width);
// Encode the image as a PNG buffer
try (var stream = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", stream);
return stream.toByteArray();
} catch (Exception e) { return null; }
}
// List all files
static BundledFile[] listFiles(String name) {
var dirs = new ArrayList<BundledFile>();
var files = new ArrayList<BundledFile>();
// Propagate down the file system tree
dirs.add(new BundledFile(null, new File(".")));
while (!dirs.isEmpty()) {
var dir = dirs.remove(0);
for (var sub : dir.listFiles(name, true ))
dirs.add(sub );
for (var file : dir.listFiles(name, false))
files.add(file);
}
// Return the set of files as a sorted array
Collections.sort(files);
return files.toArray(new BundledFile[files.size()]);
}
// Generate a filename for the bundle
static String name(String name) {
var calendar = Calendar.getInstance();
return String.format("%s_%04d%02d%02d",
name,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH)
);
}
// Produce the output HTML from the template
static void patch(String name, String url) {
String markup = null;
try (var stream = new FileInputStream("web/template.html")) {
markup = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
} catch (Exception e) { }
markup = markup.replace("src=\"\"", "src=\"" + url + "\"");
try (var stream = new FileOutputStream(name + ".html")) {
stream.write(markup.getBytes(StandardCharsets.UTF_8));
} catch (Exception e) { }
}
// Produce source data to prepend to web/_boot.js
static byte[] prepend(String name, BundledFile[] files) {
var ret = new StringBuilder();
// Arguments
ret.append("let " +
"buffer=arguments[0]," +
"image=arguments[1]," +
"name=\"" + name + "\"" +
";");
// Bundle manifest
ret.append("let manifest=[");
for (var file : files) {
if (file != files[0])
ret.append(",");
ret.append("[\"" +
file.filename + "\", " + file.data.length + "]");
}
ret.append("];");
// Convert to byte array
return ret.toString().getBytes(StandardCharsets.UTF_8);
}
// Convert an image buffer to a data URL
static String url(byte[] image) {
return "data:image/png;base64," +
Base64.getMimeEncoder(0, new byte[0]).encodeToString(image);
}
}

310
web/_boot.js Normal file
View File

@ -0,0 +1,310 @@
// Running as an async function
// Prepended by Bundle.java: buffer, image, manifest, name
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// Resource manager for bundled files
class Bundle extends Array {
//////////////////////////////// Constants ////////////////////////////////
// .ZIP support
static CRC_LOOKUP = new Uint32Array(256);
// Text processing
static DECODER = new TextDecoder();
static ENCODER = new TextEncoder();
static initializer() {
// Generate the CRC32 lookup table
for (let x = 0; x <= 255; x++) {
let l = x;
for (let j = 7; j >= 0; j--)
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
this.CRC_LOOKUP[x] = l;
}
}
///////////////////////// Initialization Methods //////////////////////////
constructor(name, url, settings, isDebug) {
super();
this.isDebug = isDebug;
this.name = name;
this.settings = settings;
this.url = url;
}
///////////////////////////// Public Methods //////////////////////////////
// Export all bundled resources to a .ZIP file
save() {
let centrals = new Array(this.length);
let locals = new Array(this.length);
let offset = 0;
let size = 0;
// Encode file and directory entries
for (let x = 0; x < this.length; x++) {
let file = this[x];
let sum = Bundle.crc32(file.data);
locals [x] = file.toZipHeader(sum);
centrals[x] = file.toZipHeader(sum, offset);
offset += locals [x].length;
size += centrals[x].length;
}
// Encode end of central directory
let end = [];
Bundle.writeInt(end, 4, 0x06054B50); // Signature
Bundle.writeInt(end, 2, 0); // Disk number
Bundle.writeInt(end, 2, 0); // Central dir start disk
Bundle.writeInt(end, 2, this.length); // # central dir this disk
Bundle.writeInt(end, 2, this.length); // # central dir total
Bundle.writeInt(end, 4, size); // Size of central dir
Bundle.writeInt(end, 4, offset); // Offset of central dir
Bundle.writeInt(end, 2, 0); // .ZIP comment length
// Prompt the user to save the resulting file
let a = document.createElement("a");
a.download = this.name + ".zip";
a.href = URL.createObjectURL(new Blob(
locals.concat(centrals).concat([Uint8Array.from(end)]),
{ type: "application/zip" }
));
a.style.visibility = "hidden";
document.body.appendChild(a);
a.click();
a.remove();
}
///////////////////////////// Package Methods /////////////////////////////
// Add a BundledFile to the collection
add(file) {
file.bundle = this;
this.push(this[file.name] = file);
}
// Write a byte array into an output buffer
static writeBytes(data, bytes) {
for (let b of bytes)
data.push(b);
}
// Write an integer into an output buffer
static writeInt(data, size, value) {
for (; size > 0; size--, value >>= 8)
data.push(value & 0xFF);
}
// Write a string of text as bytes into an output buffer
static writeString(data, text) {
this.writeBytes(data, this.ENCODER.encode(text));
}
///////////////////////////// Private Methods /////////////////////////////
// Calculate the CRC32 checksum for a byte array
static crc32(data) {
let c = 0xFFFFFFFF;
for (let x = 0; x < data.length; x++)
c = ((c >>> 8) ^ this.CRC_LOOKUP[(c ^ data[x]) & 0xFF]);
return ~c & 0xFFFFFFFF;
}
}
Bundle.initializer();
///////////////////////////////////////////////////////////////////////////////
// BundledFile //
///////////////////////////////////////////////////////////////////////////////
// Individual file in the bundled data
class BundledFile {
//////////////////////////////// Constants ////////////////////////////////
// MIME types
static MIMES = {
".css" : "text/css;charset=UTF-8",
".frag" : "text/plain;charset=UTF-8",
".js" : "text/javascript;charset=UTF-8",
".json" : "application/json;charset=UTF-8",
".png" : "image/png",
".svg" : "image/svg+xml;charset=UTF-8",
".txt" : "text/plain;charset=UTF-8",
".vert" : "text/plain;charset=UTF-8",
".wasm" : "application/wasm",
".woff2": "font/woff2"
};
///////////////////////// Initialization Methods //////////////////////////
constructor(name, buffer, offset, length) {
// Configure instance fields
this.data = buffer.slice(offset, offset + length);
this.name = name;
// Resolve the MIME type
let index = name.lastIndexOf(".");
this.mime = index != -1 && BundledFile.MIMES[name.substring(index)] ||
"application/octet-stream";
}
///////////////////////////// Public Methods //////////////////////////////
// Represent the file with a blob URL
toBlobURL() {
return this.blobURL || (this.blobURL = URL.createObjectURL(
new Blob([ this.data ], { type: this.mime })));
}
// Encode the file data as a data URL
toDataURL() {
return "data:" + this.mime + ";base64," + btoa(
Array.from(this.data).map(b=>String.fromCharCode(b)).join(""));
}
// Pre-process URLs in a bundled file's contents
toProcURL(asDataURL = false) {
// The URL has already been computed
if (this.url)
return this.url;
// Working variables
let content = this.toString();
let pattern = /\/\*\*?\*\//g;
let parts = content.split(pattern);
let ret = [ parts.shift() ];
// Process all URLs prefixed with /**/ or /***/
for (let part of parts) {
let start = part.indexOf("\"");
let end = part.indexOf("\"", start + 1);
let filename = part.substring(start + 1, end);
let asData = pattern.exec(content)[0] == "/***/";
// Relative to current file
if (filename.startsWith(".")) {
let path = this.name.split("/");
path.pop(); // Current filename
// Navigate to the path of the target file
for (let dir of filename.split("/")) {
switch (dir) {
case "..": path.pop(); // Fallthrough
case "." : break;
default : path.push(dir);
}
}
// Produce the fully-qualified filename
filename = path.join("/");
}
// Append the file as a data URL
let file = this.bundle[filename];
ret.push(
part.substring(0, start + 1),
file[
file.mime.startsWith("text/javascript") ||
file.mime.startsWith("text/css") ?
"toProcURL" : asData ? "toDataURL" : "toBlobURL"
](asData),
part.substring(end)
);
}
// Represent the transformed source as a URL
return this.url = asDataURL ?
"data:" + this.mime + ";base64," + btoa(ret.join("")) :
URL.createObjectURL(new Blob(ret, { type: this.mime }))
;
}
// Decode the file data as a UTF-8 string
toString() {
return Bundle.DECODER.decode(this.data);
}
///////////////////////////// Package Methods /////////////////////////////
// Produce a .ZIP header for export
toZipHeader(crc32, offset) {
let central = offset !== undefined;
let ret = [];
if (central) {
Bundle.writeInt (ret, 4, 0x02014B50); // Signature
Bundle.writeInt (ret, 2, 20); // Version created by
} else
Bundle.writeInt (ret, 4, 0x04034B50); // Signature
Bundle.writeInt (ret, 2, 20); // Version required
Bundle.writeInt (ret, 2, 0); // Bit flags
Bundle.writeInt (ret, 2, 0); // Compression method
Bundle.writeInt (ret, 2, 0); // Modified time
Bundle.writeInt (ret, 2, 0); // Modified date
Bundle.writeInt (ret, 4, crc32); // Checksum
Bundle.writeInt (ret, 4, this.data.length); // Compressed size
Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size
Bundle.writeInt (ret, 2, this.name.length); // Filename length
Bundle.writeInt (ret, 2, 0); // Extra field length
if (central) {
Bundle.writeInt (ret, 2, 0); // File comment length
Bundle.writeInt (ret, 2, 0); // Disk number start
Bundle.writeInt (ret, 2, 0); // Internal attributes
Bundle.writeInt (ret, 4, 0); // External attributes
Bundle.writeInt (ret, 4, offset); // Relative offset
}
Bundle.writeString (ret, this.name); // Filename
if (!central)
Bundle.writeBytes(ret, this.data); // File data
return Uint8Array.from(ret);
}
}
///////////////////////////////////////////////////////////////////////////////
// Boot Program //
///////////////////////////////////////////////////////////////////////////////
// Produce the application bundle
let bundle = new Bundle(name, image.src, image.getAttribute("settings") || "",
location.protocol != "file:" && location.hash == "#debug");
for (let x=0,offset=buffer.indexOf(0)-manifest[0][1]; x<manifest.length; x++) {
let entry = manifest[x];
bundle.add(new BundledFile(entry[0], buffer, offset, entry[1]));
offset += entry[1] + (x == 0 ? 1 : 0);
}
// Begin program operations
(await import(bundle.isDebug ?
"./web/App.js" :
bundle["web/App.js"].toProcURL()
)).App.main(bundle);

88
web/core/AudioThread.js Normal file
View File

@ -0,0 +1,88 @@
"use strict";
// Dedicated audio output thread
class AudioThread extends AudioWorkletProcessor {
///////////////////////// Initialization Methods //////////////////////////
constructor() {
super();
// Configure instance fields
this.buffers = []; // Input sample buffer queue
this.offset = 0; // Offset into oldest buffer
// Wait for initializer message from parent thread
this.port.onmessage = m=>this.init(m.data);
}
async init(main) {
// Configure message ports
this.core = this.port;
this.core.onmessage = m=>this.onCore(m.data);
this.main = main;
this.main.onmessage = m=>this.onMain(m.data);
// Notify main thread
this.port.postMessage(0);
}
///////////////////////////// Public Methods //////////////////////////////
// Produce output samples (called by the user agent)
process(inputs, outputs, parameters) {
let output = outputs[0];
let length = output [0].length;
let empty = null;
// Process all samples
for (let x = 0; x < length;) {
// No bufferfed samples are available
if (this.buffers.length == 0) {
for (; x < length; x++)
output[0] = output[1] = 0;
break;
}
// Transfer samples from the oldest buffer
let y, buffer = this.buffers[0];
for (y = this.offset; x < length && y < buffer.length; x++, y+=2) {
output[0][x] = buffer[y ];
output[1][x] = buffer[y + 1];
}
// Advance to the next buffer
if ((this.offset = y) == buffer.length) {
if (empty == null)
empty = [];
empty.push(this.buffers.shift());
this.offset = 0;
}
}
// Return emptied sample buffers to the core thread
if (empty != null)
this.core.postMessage(empty, empty.map(e=>e.buffer));
return true;
}
///////////////////////////// Message Methods /////////////////////////////
// Message received from core thread
onCore(msg) {
}
// Message received from main thread
onMain(msg) {
}
}
registerProcessor("AudioThread", AudioThread);

293
web/core/Core.js Normal file
View File

@ -0,0 +1,293 @@
// Interface between application and WebAssembly worker thread
class Core {
///////////////////////// Initialization Methods //////////////////////////
constructor() {
// Configure instance fields
this.promises = [];
}
async init(coreUrl, wasmUrl, audioUrl) {
// Open audio output stream
this.audio = new AudioContext({
latencyHint: "interactive",
sampleRate : 41700
});
await this.audio.suspend();
// Launch the audio thread
await this.audio.audioWorklet.addModule(
Core.url(audioUrl, "AudioThread.js", /***/"./AudioThread.js"));
let node = new AudioWorkletNode(this.audio, "AudioThread", {
numberOfInputs : 0,
numberOfOutputs : 1,
outputChannelCount: [2]
});
node.connect(this.audio.destination);
// Attach a second MessagePort to the audio thread
let channel = new MessageChannel();
this.audio.port = channel.port1;
await new Promise(resolve=>{
node.port.onmessage = resolve;
node.port.postMessage(channel.port2, [channel.port2]);
});
this.audio.port.onmessage = m=>this.onAudio(m.data);
// Launch the core thread
this.core = new Worker(
Core.url(wasmUrl, "CoreThread.js", /***/"./CoreThread.js"));
await new Promise(resolve=>{
this.core.onmessage = resolve;
this.core.postMessage({
audio : node.port,
wasmUrl: Core.url(wasmUrl, "core.wasm", /***/"./core.wasm")
}, [node.port]);
});
this.core.onmessage = m=>this.onCore(m.data);
return this;
}
///////////////////////////// Static Methods //////////////////////////////
// Select a URL in the same path as the current script
static url(arg, name, bundled) {
// The input argument was provided
if (arg)
return arg;
// Running from a bundle distribution
if (bundled.startsWith("blob:") || bundled.startsWith("data:"))
return bundled;
// Compute the URL for the given filename
let url = new URL(import.meta.url).pathname;
return url.substring(0, url.lastIndexOf("/") + 1) + name;
}
///////////////////////////// Event Handlers //////////////////////////////
// Message received from audio thread
onAudio(msg) {
}
// Message received from core thread
onCore(msg) {
// Process subscriptions
if (msg.subscriptions && this.onsubscription instanceof Function) {
for (let sub of msg.subscriptions) {
let key = sub.subscription;
delete sub.subscription;
this.onsubscription(key, sub, this);
}
delete msg.subscriptions;
}
// The main thread is waiting on a reply
if (msg.isReply) {
delete msg.isReply;
// For "create", produce sim objects
if (msg.isCreate) {
delete msg.isCreate;
msg.sims = msg.sims.map(s=>({ pointer: s }));
}
// Notify the caller
this.promises.shift()(msg);
}
}
///////////////////////////// Public Methods //////////////////////////////
// Create and initialize simulations
create(count, options) {
return this.message({
command: "create",
count : count
}, [], options);
}
// Delete a simulation
delete(sim, options) {
return this.message({
command: "delete",
sim : sim.pointer
}, [], options);
}
// Retrieve the value of all CPU registers
getAllRegisters(sim, options) {
return this.message({
command: "getAllRegisters",
sim : sim.pointer
}, [], options);
}
// Retrieve the value of PC
getProgramCounter(sim, options) {
return this.message({
command: "getProgramCounter",
sim : sim.pointer
}, [], options);
}
// Retrieve the value of a system register
getSystemRegister(sim, id, options) {
return this.message({
command: "getSystemRegister",
id : id,
sim : sim.pointer
}, [], options);
}
// Read multiple bytes from memory
read(sim, address, length, options) {
return this.message({
command: "read",
address: address,
length : length,
sim : sim.pointer
}, [], options);
}
// Refresh subscriptions
refresh(subscriptions = null, options) {
return this.message({
command : "refresh",
subscriptions: subscriptions
}, [], options);
}
// Simulate a hardware reset
reset(sim, options) {
return this.message({
command: "reset",
sim : sim.pointer
}, [], options);
}
// Execute until the next current instruction
runNext(sims, options) {
return this.message({
command: "runNext",
sims : Array.isArray(sims) ?
sims.map(s=>s.pointer) : [ sims.pointer ]
}, [], options);
}
// Specify a value for the program counter
setProgramCounter(sim, value, options) {
return this.message({
command: "setProgramCounter",
sim : sim.pointer,
value : value
}, [], options);
}
// Specify a value for a program register
setProgramRegister(sim, index, value, options) {
return this.message({
command: "setProgramRegister",
index : index,
sim : sim.pointer,
value : value
}, [], options);
}
// Specify a cartridge ROM buffer
setROM(sim, data, options = {}) {
data = data.slice();
return this.message({
command: "setROM",
data : data,
reset : !("reset" in options) || !!options.reset,
sim : sim.pointer
}, [data.buffer], options);
}
// Specify a value for a system register
setSystemRegister(sim, id, value, options) {
return this.message({
command: "setSystemRegister",
id : id,
sim : sim.pointer,
value : value
}, [], options);
}
// Execute the current instruction
singleStep(sims, options) {
return this.message({
command: "singleStep",
sims : Array.isArray(sims) ?
sims.map(s=>s.pointer) : [ sims.pointer ]
}, [], options);
}
// Cancel a subscription
unsubscribe(subscription, options) {
return this.message({
command : "unsubscribe",
subscription: subscription
}, [], options);
}
// Write multiple bytes to memory
write(sim, address, data, options) {
data = data.slice();
return this.message({
address: address,
command: "write",
data : data,
sim : sim.pointer
}, [data.buffer], options);
}
///////////////////////////// Private Methods /////////////////////////////
// Send a message to the core thread
message(msg, transfers, options = {}) {
// Configure options
if (!(options instanceof Object))
options = { reply: options };
if (!("reply" in options) || options.reply)
msg.reply = true;
if ("refresh" in options)
msg.refresh = options.refresh;
if ("subscription" in options)
msg.subscription = options.subscription;
if ("tag" in options)
msg.tag = options.tag;
// Send the command to the core thread
return msg.reply ?
new Promise(resolve=>{
this.promises.push(resolve);
this.core.postMessage(msg, transfers);
}) :
this.core.postMessage(msg, transfers);
;
}
}
export { Core };

330
web/core/CoreThread.js Normal file
View File

@ -0,0 +1,330 @@
"use strict";
// Dedicated emulation thread
class CoreThread {
///////////////////////// Initialization Methods //////////////////////////
constructor() {
// Configure instance fields
this.subscriptions = new Map();
// Wait for initializer message from parent thread
onmessage = m=>this.init(m.data.audio, m.data.wasmUrl);
}
async init(audio, wasmUrl) {
// Configure message ports
this.audio = audio;
this.audio.onmessage = m=>this.onAudio (m.data);
this.main = globalThis;
this.main .onmessage = m=>this.onMessage(m.data);
// Load and instantiate the WebAssembly module
this.wasm = (await WebAssembly.instantiateStreaming(
fetch(wasmUrl), {
env: { emscripten_notify_memory_growth: ()=>this.onGrowth() }
})).instance;
this.onGrowth();
this.pointerSize = this.PointerSize();
this.pointerType = this.pointerSize == 8 ? Uint64Array : Uint32Array;
// Notify main thread
this.main.postMessage(0);
}
///////////////////////////// Event Handlers //////////////////////////////
// Message received from audio thread
onAudio(frames) {
// Audio processing was suspended
if (frames == 0) {
return;
}
// Wait for more frames
this.audio.postMessage(0);
}
// Emscripten has grown the linear memory
onGrowth() {
Object.assign(this, this.wasm.exports);
}
// Message received from main thread
onMessage(msg) {
// Subscribe to the command
if (msg.subscription && msg.command != "refresh")
this.subscriptions.set(CoreThread.key(msg.subscription), msg);
// Process the command
let rep = this[msg.command](msg);
// Do not send a reply
if (!msg.reply)
return;
// Configure the reply
if (!rep)
rep = {};
if (msg.reply)
rep.isReply = true;
if ("tag" in msg)
rep.tag = msg.tag;
// Send the reply to the main thread
let transfers = rep.transfers;
if (transfers)
delete rep.transfers;
this.main.postMessage(rep, transfers || []);
// Refresh subscriptions
if (msg.refresh && msg.command != "refresh") {
let subs = {};
if (Array.isArray(msg.refresh))
subs.subscriptions = msg.refresh;
this.refresh(subs);
}
}
//////////////////////////////// Commands /////////////////////////////////
// Create and initialize a new simulation
create(msg) {
let sims = new Array(msg.count);
for (let x = 0; x < msg.count; x++)
sims[x] = this.Create();
return {
isCreate: true,
sims : sims
};
}
// Delete all memory used by a simulation
delete(msg) {
this.Delete(msg.sim);
}
// Retrieve the values of all CPU registers
getAllRegisters(msg) {
let program = new Int32Array (32);
let system = new Uint32Array(32);
for (let x = 0; x < 32; x++) {
program[x] = this.vbGetProgramRegister(msg.sim, x);
system [x] = this.vbGetSystemRegister (msg.sim, x);
}
return {
pc : this.vbGetProgramCounter(msg.sim) >>> 0,
program : program,
system : system,
transfers: [ program.buffer, system.buffer ]
};
}
// Retrieve the value of PC
getProgramCounter(msg) {
return { value: this.vbGetProgramCounter(msg.sim) >>> 0 };
}
// Retrieve the value of a system register
getSystemRegister(msg) {
return { value: this.vbGetSystemRegister(msg.sim, msg.id) >>> 0 };
}
// Read multiple bytes from memory
read(msg) {
let buffer = this.malloc(msg.length);
this.vbReadEx(msg.sim, msg.address, buffer.pointer, msg.length);
let data = buffer.slice();
this.free(buffer);
return {
address : msg.address,
data : data,
transfers: [data.buffer]
};
}
// Process subscriptions
refresh(msg) {
let subscriptions = [];
let transfers = [];
// Select the key set to refresh
let keys = Array.isArray(msg.subscriptions) ?
msg.subscriptions.map(s=>CoreThread.key(s)) :
this.subscriptions.keys()
;
// Process all subscriptions
for (let key of keys) {
// Process the subscription
let sub = this.subscriptions.get(key);
let rep = this[sub.command](sub);
// There is no result
if (!rep)
continue;
// Add the result to the response
rep.subscription = sub.subscription;
if ("tag" in sub)
rep.tag = sub.tag;
subscriptions.push(rep);
// Add the transfers to the response
if (!rep.transfers)
continue;
transfers = transfers.concat(rep.transfers);
delete rep.transfers;
}
// Do not send a reply
if (subscriptions.length == 0 && !msg.reply)
return;
// Send the response to the main thread
this.main.postMessage({
isReply : !!msg.reply,
subscriptions: subscriptions.sort(CoreThread.REFRESH_ORDER)
}, transfers);
}
// Simulate a hardware reset
reset(msg) {
this.vbReset(msg.sim);
}
// Execute until the next current instruction
runNext(msg) {
let sims = this.malloc(msg.sims.length, true);
for (let x = 0; x < msg.sims.length; x++)
sims[x] = msg.sims[x];
this.RunNext(sims.pointer, msg.sims.length);
this.free(sims);
let pcs = new Array(msg.sims.length);
for (let x = 0; x < msg.sims.length; x++)
pcs[x] = this.vbGetProgramCounter(msg.sims[x]) >>> 0;
return { pcs: pcs };
}
// Specify a value for the program counter
setProgramCounter(msg) {
return { value: this.vbSetProgramCounter(msg.sim, msg.value) >>> 0 };
}
// Specify a value for a program register
setProgramRegister(msg) {
return {value:this.vbSetProgramRegister(msg.sim,msg.index,msg.value)};
}
// Specify a cartridge ROM buffer
setROM(msg) {
let prev = this.vbGetROM(msg.sim, 0);
let success = true;
// Specify a new ROM
if (msg.data != null) {
let data = this.malloc(msg.data.length);
for (let x = 0; x < data.length; x++)
data[x] = msg.data[x];
success = !this.vbSetROM(msg.sim, data.pointer, data.length);
}
// Operation was successful
if (success) {
// Delete the previous ROM
this.Free(prev);
// Reset the simulation
if (msg.reset)
this.vbReset(msg.sim);
}
return { success: success };
}
// Specify a value for a system register
setSystemRegister(msg) {
return {value:this.vbSetSystemRegister(msg.sim,msg.id,msg.value)>>>0};
}
// Execute the current instruction
singleStep(msg) {
let sims = this.malloc(msg.sims.length, true);
for (let x = 0; x < msg.sims.length; x++)
sims[x] = msg.sims[x];
this.SingleStep(sims.pointer, msg.sims.length);
this.free(sims);
let pcs = new Array(msg.sims.length);
for (let x = 0; x < msg.sims.length; x++)
pcs[x] = this.vbGetProgramCounter(msg.sims[x]) >>> 0;
return { pcs: pcs };
}
// Delete a subscription
unsubscribe(msg) {
this.subscriptions.delete(CoreThread.key(msg.subscription));
}
// Write multiple bytes to memory
write(msg) {
let data = this.malloc(msg.data.length);
for (let x = 0; x < data.length; x++)
data[x] = msg.data[x];
this.vbWriteEx(msg.sim, msg.address, data.pointer, data.length);
this.free(data);
}
///////////////////////////// Private Methods /////////////////////////////
// Delete a byte array in WebAssembly memory
free(buffer) {
this.Free(buffer.pointer);
}
// Format a subscription key as a string
static key(subscription) {
return subscription.map(k=>k.toString()).join("\n");
}
// Allocate a byte array in WebAssembly memory
malloc(length, pointers = false) {
let size = pointers ? length * this.pointerSize : length;
return this.map(this.Malloc(size), length, pointers);
}
// Map a typed array into WebAssembly memory
map(address, length, pointers = false) {
let ret = new (pointers ? this.pointerType : Uint8Array)
(this.memory.buffer, address, length);
ret.pointer = address;
return ret;
}
// Comparator for subscriptions within the refresh command
static REFRESH_ORDER(a, b) {
a = a.subscription[0];
b = b.subscription[0];
return a < b ? -1 : a > b ? 1 : 0;
}
}
new CoreThread();

542
web/core/Disassembler.js Normal file
View File

@ -0,0 +1,542 @@
// Machine code to human readable text converter
class Disassembler {
//////////////////////////////// Constants ////////////////////////////////
// Default settings
static DEFAULTS = {
condCL : "L", // Use C/NC or L/NL for conditions
condEZ : "E", // Use E/NE or Z/NZ for conditions
condNames : true, // Use condition names
condUppercase: false, // Condition names uppercase
hexPrefix : "0x", // Hexadecimal prefix
hexSuffix : "", // Hexadecimal suffix
hexUppercase : true, // Hexadecimal uppercase
instUppercase: true, // Mnemonics uppercase
jumpAddress : true, // Jump/branch shows target address
memInside : false, // Use [reg1 + disp] notation
opDestFirst : false, // Destination operand first
proNames : true, // Use program register names
proUppercase : false, // Program register names uppercase
splitBcond : false, // BCOND condition as an operand
splitSetf : true, // SETF condition as an operand
sysNames : true, // Use system register names
sysUppercase : false // System register names uppercase
};
/////////////////////////// Disassembly Lookup ////////////////////////////
// Opcode descriptors
static OPDEFS = [
[ "MOV" , [ "opReg1" , "opReg2" ] ], // 000000
[ "ADD" , [ "opReg1" , "opReg2" ] ],
[ "SUB" , [ "opReg1" , "opReg2" ] ],
[ "CMP" , [ "opReg1" , "opReg2" ] ],
[ "SHL" , [ "opReg1" , "opReg2" ] ],
[ "SHR" , [ "opReg1" , "opReg2" ] ],
[ "JMP" , [ "opReg1Ind" ] ],
[ "SAR" , [ "opReg1" , "opReg2" ] ],
[ "MUL" , [ "opReg1" , "opReg2" ] ],
[ "DIV" , [ "opReg1" , "opReg2" ] ],
[ "MULU" , [ "opReg1" , "opReg2" ] ],
[ "DIVU" , [ "opReg1" , "opReg2" ] ],
[ "OR" , [ "opReg1" , "opReg2" ] ],
[ "AND" , [ "opReg1" , "opReg2" ] ],
[ "XOR" , [ "opReg1" , "opReg2" ] ],
[ "NOT" , [ "opReg1" , "opReg2" ] ],
[ "MOV" , [ "opImm5S", "opReg2" ] ], // 010000
[ "ADD" , [ "opImm5S", "opReg2" ] ],
null, // SETF: special
[ "CMP" , [ "opImm5S", "opReg2" ] ],
[ "SHL" , [ "opImm5U", "opReg2" ] ],
[ "SHR" , [ "opImm5U", "opReg2" ] ],
[ "CLI" , [ ] ],
[ "SAR" , [ "opImm5U", "opReg2" ] ],
[ "TRAP" , [ "opImm5U" ] ],
[ "RETI" , [ ] ],
[ "HALT" , [ ] ],
null, // Invalid
[ "LDSR" , [ "opReg2" , "opSys" ] ],
[ "STSR" , [ "opSys" , "opReg2" ] ],
[ "SEI" , [ ] ],
null, // Bit string: special
null, // BCOND: special // 100000
null, // BCOND: special
null, // BCOND: special
null, // BCOND: special
null, // BCOND: special
null, // BCOND: special
null, // BCOND: special
null, // BCOND: special
[ "MOVEA", [ "opImm16U" , "opReg1", "opReg2" ] ],
[ "ADDI" , [ "opImm16S" , "opReg1", "opReg2" ] ],
[ "JR" , [ "opDisp26" ] ],
[ "JAL" , [ "opDisp26" ] ],
[ "ORI" , [ "opImm16U" , "opReg1", "opReg2" ] ],
[ "ANDI" , [ "opImm16U" , "opReg1", "opReg2" ] ],
[ "XORI" , [ "opImm16U" , "opReg1", "opReg2" ] ],
[ "MOVHI", [ "opImm16U" , "opReg1", "opReg2" ] ],
[ "LD.B" , [ "opReg1Disp", "opReg2" ] ], // 110000
[ "LD.H" , [ "opReg1Disp", "opReg2" ] ],
null, // Invalid
[ "LD.W" , [ "opReg1Disp", "opReg2" ] ],
[ "ST.B" , [ "opReg2" , "opReg1Disp" ] ],
[ "ST.H" , [ "opReg2" , "opReg1Disp" ] ],
null, // Invalid
[ "ST.W" , [ "opReg2" , "opReg1Disp" ] ],
[ "IN.B" , [ "opReg1Disp", "opReg2" ] ],
[ "IN.H" , [ "opReg1Disp", "opReg2" ] ],
[ "CAXI" , [ "opReg1Disp", "opReg2" ] ],
[ "IN.W" , [ "opReg1Disp", "opReg2" ] ],
[ "OUT.B", [ "opReg2" , "opReg1Disp" ] ],
[ "OUT.H", [ "opReg2" , "opReg1Disp" ] ],
null, // Floating-point/Nintendo: special
[ "OUT.W", [ "opReg2" , "opReg1Disp" ] ]
];
// Bit string sub-opcode descriptors
static BITSTRING = [
"SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD",
null , null , null , null ,
"ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" ,
"ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" ,
null , null , null , null ,
null , null , null , null ,
null , null , null , null ,
null , null , null , null
];
// Floating-point/Nintendo sub-opcode descriptors
static FLOATENDO = [
[ "CMPF.S" , [ "opReg1", "opReg2" ] ],
null, // Invalid
[ "CVT.WS" , [ "opReg1", "opReg2" ] ],
[ "CVT.SW" , [ "opReg1", "opReg2" ] ],
[ "ADDF.S" , [ "opReg1", "opReg2" ] ],
[ "SUBF.S" , [ "opReg1", "opReg2" ] ],
[ "MULF.S" , [ "opReg1", "opReg2" ] ],
[ "DIVF.S" , [ "opReg1", "opReg2" ] ],
[ "XB" , [ "opReg2" ] ],
[ "XH" , [ "opReg2" ] ],
[ "REV" , [ "opReg1", "opReg2" ] ],
[ "TRNC.SW", [ "opReg1", "opReg2" ] ],
[ "MPYHW" , [ "opReg1", "opReg2" ] ],
null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null,
null, null, null, null, null, null, null, null
];
// Condition mnemonics
static CONDITIONS = [
"V" , "C" , "E" , "NH", "N", "T", "LT", "LE",
"NV", "NC", "NE", "H" , "P", "F", "GE", "GT"
];
// Program register names
static PRONAMES = [
"r0" , "r1" , "hp" , "sp" , "gp" , "tp" , "r6" , "r7" ,
"r8" , "r9" , "r10", "r11", "r12", "r13", "r14", "r15",
"r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23",
"r24", "r25", "r26", "r27", "r28", "r29", "r30", "lp"
];
// System register names
static SYSNAMES = [
"EIPC", "EIPSW", "FEPC", "FEPSW", "ECR", "PSW", "PIR", "TKCW",
"8" , "9" , "10" , "11" , "12" , "13" , "14" , "15" ,
"16" , "17" , "18" , "19" , "20" , "21" , "22" , "23" ,
"CHCW", "ADTRE", "26" , "27" , "28" , "29" , "30" , "31"
];
///////////////////////////// Static Methods //////////////////////////////
// Determine the bounds of a data buffer to represent all lines of output
static dataBounds(address, line, length) {
let before = 10; // Number of lines before the first line of output
let max = 4; // Maximum number of bytes that can appear on a line
// The reference line is before the preferred earliest line
if (line < -before) {
length = (length - line) * max;
}
// The reference line is before the first line
else if (line < 0) {
address -= (line + before) * max;
length = (length + before) * max;
}
// The reference line is at or after the first line
else {
address -= (line + before) * max;
length = (Math.max(length, line) + before) * max;
}
return {
address: (address & ~1) >>> 0,
length : length
};
}
///////////////////////// Initialization Methods //////////////////////////
constructor() {
Object.assign(this, Disassembler.DEFAULTS);
}
///////////////////////////// Public Methods //////////////////////////////
// Disassemble a region of memory
disassemble(data, dataAddress, refAddress, refLine, length, pc = null) {
let pcOffset = pc === null ? -1 : pc - dataAddress >>> 0;
// Locate the offset of the first line of output in the buffer
let offset = 0;
for (let
addr = dataAddress,
circle = refLine > 0 ? new Array(refLine) : null,
index = 0,
more = [],
remain = null
;;) {
// Determine the size of the current line
if (more.length == 0)
this.more(more, data, offset);
let size = more.shift();
// The current line contains the reference address
if (refAddress - addr >>> 0 < size) {
// The next item in the buffer is the first line of output
if (refLine > 0) {
offset = circle[index];
break;
}
// This line is the first line of output
if (refLine == 0)
break;
// Count more lines for the first line of output
remain = refLine;
}
// Record the offset of the current instruction
if (refLine > 0) {
circle[index] = offset;
index = (index + 1) % circle.length;
}
// Advance to the next line
let sizeToPC = pcOffset - offset >>> 0;
if (offset != pcOffset && sizeToPC < size) {
size = sizeToPC;
more.splice();
}
addr = addr + size >>> 0;
offset += size;
if (remain !== null && ++remain == 0)
break; // The next line is the first line of output
}
// Process all lines of output
let lines = new Array(length);
for (let
addr = dataAddress + offset,
more = [],
x = 0;
x < length; x++
) {
// Determine the size of the current line
if (more.length == 0)
this.more(more, data, offset, pcOffset);
let size = more.shift();
// Add the line to the response
lines[x] = this.format({
rawAddress: addr,
rawBytes : data.slice(offset, offset + size)
});
// Advance to the next line
let sizeToPC = pcOffset - offset >>> 0;
if (offset != pcOffset && sizeToPC < size) {
size = sizeToPC;
more.splice();
}
addr = addr + size >>> 0;
offset += size;
}
return lines;
}
/////////////////////////// Formatting Methods ////////////////////////////
// Format a line as human-readable text
format(line) {
let canReverse = true;
let opcode = line.rawBytes[1] >>> 2;
let opdef;
let code = [
line.rawBytes[1] << 8 | line.rawBytes[0],
line.rawBytes.length == 2 ? null :
line.rawBytes[3] << 8 | line.rawBytes[2]
];
// BCOND
if ((opcode & 0b111000) == 0b100000) {
let cond = code[0] >>> 9 & 15;
opdef =
cond == 13 ? [ "NOP", [ ] ] :
this.splitBcond ? [ "BCOND", [ "opBCond", "opDisp9" ] ] :
[
cond == 5 ? "BR" : "B" + this.condition(cond, true),
[ "opDisp9" ]
]
;
canReverse = false;
}
// Processing by opcode
else switch (opcode) {
// SETF
case 0b010010:
opdef = !this.splitSetf ?
[
"SETF" + Disassembler.CONDITIONS[code[0] & 15],
[ "opReg2" ]
] :
[ "SETF", [ "opCond", "opReg2" ] ]
;
break;
// Bit string
case 0b011111:
opdef = Disassembler.BITSTRING[code[0] & 31];
if (opdef != null)
opdef = [ opdef, [] ];
break;
// Floating-point/Nintendo
case 0b111110:
opdef = Disassembler.FLOATENDO[code[1] >>> 10];
break;
// All others
default: opdef = Disassembler.OPDEFS[opcode];
}
// The opcode is undefined
if (opdef == null)
opdef = [ "---", [] ];
// Format the line's display text
line.address = this.hex(line.rawAddress, 8, false);
line.bytes = new Array(line.rawBytes.length);
line.mnemonic = this.instUppercase ? opdef[0] : opdef[0].toLowerCase();
line.operands = new Array(opdef[1].length);
for (let x = 0; x < line.bytes.length; x++)
line.bytes[x] = this.hex(line.rawBytes[x], 2, false);
for (let x = 0; x < line.operands.length; x++)
line.operands[x] = this[opdef[1][x]](line, code);
if (this.opDestFirst && canReverse)
line.operands.reverse();
return line;
}
// Format a condition operand in a BCOND instruction
opBCond(line, code) {
return this.condition(code[0] >>> 9 & 15);
}
// Format a condition operand in a SETF instruction
opCond(line, code) {
return this.condition(code[0] & 15);
}
// Format a 9-bit displacement operand
opDisp9(line, code) {
let disp = code[0] << 23 >> 23;
return this.jump(line.rawAddress, disp);
}
// Format a 26-bit displacement operand
opDisp26(line, code) {
let disp = (code[0] << 16 | code[1]) << 6 >> 6;
return this.jump(line.rawAddress, disp);
}
// Format a 5-bit signed immediate operand
opImm5S(line, code) {
return (code[0] & 31) << 27 >> 27;
}
// Format a 5-bit unsigned immediate operand
opImm5U(line, code) {
return code[0] & 31;
}
// Format a 16-bit signed immediate operand
opImm16S(line, code) {
let ret = code[1] << 16 >> 16;
return (
ret < -256 ? "-" + this.hex(-ret) :
ret > 256 ? this.hex( ret) :
ret
);
}
// Format a 16-bit unsigned immediate operand
opImm16U(line, code) {
return this.hex(code[1], 4);
}
// Format a Reg1 operand
opReg1(line, code) {
return this.programRegister(code[0] & 31);
}
// Format a disp[reg1] operand
opReg1Disp(line, code) {
let disp = code[1] << 16 >> 16;
let reg1 = this.programRegister(code[0] & 31);
// Do not print the displacement
if (disp == 0)
return "[" + reg1 + "]";
// Format the displacement amount
disp =
disp < -256 ? "-" + this.hex(-disp) :
disp > 256 ? this.hex( disp) :
disp.toString()
;
// [reg1 + disp] notation
if (this.memInside) {
return "[" + reg1 + (disp.startsWith("-") ?
" - " + disp.substring(1) :
" + " + disp
) + "]";
}
// disp[reg1] notation
return disp + "[" + reg1 + "]";
}
// Format a [Reg1] operand
opReg1Ind(line, code) {
return "[" + this.programRegister(code[0] & 31) + "]";
}
// Format a Reg2 operand
opReg2(line, code) {
return this.programRegister(code[0] >> 5 & 31);
}
// Format a system register operand
opSys(line, code) {
return this.systemRegister(code[0] & 31);
}
///////////////////////////// Private Methods /////////////////////////////
// Select the mnemonic for a condition
condition(index, forceUppercase = false) {
if (!this.condNames)
return index.toString();
let ret =
index == 1 ? this.condCL :
index == 2 ? this.condEZ :
index == 9 ? "N" + this.condCL :
index == 10 ? "N" + this.condEZ :
Disassembler.CONDITIONS[index]
;
if (!forceUppercase && !this.condUppercase)
ret = ret.toLowerCase();
return ret;
}
// Format a number as a hexadecimal string
hex(value, digits = null, decorated = true) {
value = value.toString(16);
if (this.hexUppercase)
value = value.toUpperCase();
if (digits != null)
value = value.padStart(digits, "0");
if (decorated) {
value = this.hexPrefix + value + this.hexSuffix;
if (this.hexPrefix == "" && "0123456789".indexOf(value[0]) == -1)
value = "0" + value;
}
return value;
}
// Format a jump or branch destination
jump(address, disp) {
return (
this.jumpAddress ?
this.hex(address + disp >>> 0, 8, false) :
disp < -256 ? "-" + this.hex(-disp) :
disp > 256 ? "+" + this.hex( disp) :
disp.toString()
);
}
// Determine the number of bytes in the next line(s) of disassembly
more(more, data, offset) {
// Error checking
if (offset + 1 >= data.length)
throw new Error("Disassembly error: Unexpected EoF");
// Determine the instruction's size from its opcode
let opcode = data[offset + 1] >>> 2;
more.push(
opcode < 0b101000 || // 16-bit instruction
opcode == 0b110010 || // Illegal opcode
opcode == 0b110110 // Illegal opcode
? 2 : 4);
}
// Format a program register
programRegister(index) {
let ret = this.proNames ? Disassembler.PRONAMES[index] : "r" + index;
if (this.proUppercase)
ret = ret.toUpperCase();
return ret;
}
// Format a system register
systemRegister(index) {
let ret = this.sysNames ?
Disassembler.SYSNAMES[index] : index.toString();
if (!this.sysUppercase && this.sysNames)
ret = ret.toLowerCase();
return ret;
}
}
export { Disassembler };

78
web/core/wasm.c Normal file
View File

@ -0,0 +1,78 @@
#undef VBAPI
#include <stdlib.h>
#include <emscripten/emscripten.h>
#include <vb.h>
/////////////////////////////// Module Commands ///////////////////////////////
// Create and initialize a new simulation
EMSCRIPTEN_KEEPALIVE VB* Create() {
VB *vb = malloc(sizeof (VB));
vbInit(vb);
return vb;
}
// Delete all memory used by a simulation
EMSCRIPTEN_KEEPALIVE void Delete(VB *vb) {
free(vb->cart.ram);
free(vb->cart.rom);
free(vb);
}
// Proxy for free()
EMSCRIPTEN_KEEPALIVE void Free(void *ptr) {
free(ptr);
}
// Proxy for malloc()
EMSCRIPTEN_KEEPALIVE void* Malloc(int size) {
return malloc(size);
}
// Size in bytes of a pointer
EMSCRIPTEN_KEEPALIVE int PointerSize() {
return sizeof (void *);
}
////////////////////////////// Debugger Commands //////////////////////////////
// Execute until the following instruction
uint32_t RunNextAddress;
static int RunNextFetch(VB *vb, int fetch, VBAccess *access) {
return access->address == RunNextAddress;
}
static int RunNextExecute(VB *vb, VBInstruction *inst) {
RunNextAddress = inst->address + inst->size;
vbSetOnExecute(vb, NULL);
vbSetOnFetch(vb, &RunNextFetch);
return 0;
}
EMSCRIPTEN_KEEPALIVE void RunNext(VB **vbs, int count) {
uint32_t clocks = 20000000; // 1s
vbSetOnExecute(vbs[0], &RunNextExecute);
vbEmulateEx (vbs, count, &clocks);
vbSetOnExecute(vbs[0], NULL);
vbSetOnFetch (vbs[0], NULL);
}
// Execute the current instruction
static int SingleStepBreak;
static int SingleStepFetch(VB *vb, int fetch, VBAccess *access) {
if (fetch != 0)
return 0;
if (SingleStepBreak == 1)
return 1;
SingleStepBreak = 1;
return 0;
}
EMSCRIPTEN_KEEPALIVE void SingleStep(VB **vbs, int count) {
uint32_t clocks = 20000000; // 1s
SingleStepBreak = vbs[0]->cpu.stage == 0 ? 0 : 1;
vbSetOnFetch(vbs[0], &SingleStepFetch);
vbEmulateEx (vbs, count, &clocks);
vbSetOnFetch(vbs[0], NULL);
}

1442
web/debugger/CPU.js Normal file

File diff suppressed because it is too large Load Diff

98
web/debugger/Debugger.js Normal file
View File

@ -0,0 +1,98 @@
import { ISX } from /**/"./ISX.js";
// Debug mode UI manager
class Debugger {
///////////////////////////// Static Methods //////////////////////////////
// Data type conversions
static F32 = new Float32Array(1);
static U32 = new Uint32Array(this.F32.buffer);
// Reinterpret a float32 as a u32
static fxi(x) {
this.F32[0] = x;
return this.U32[0];
}
// Process file data as ISM
static isx(data) {
return new ISX(data);
}
// Reinterpret a u32 as a float32
static ixf(x) {
this.U32[0] = x;
return this.F32[0];
}
// Compute the number of lines scrolled by a WheelEvent
static linesScrolled(e, lineHeight, pageLines, delta) {
let ret = {
delta: delta,
lines: 0
};
// No scrolling occurred
if (e.deltaY == 0);
// Scrolling by pixel
else if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
ret.delta += e.deltaY;
ret.lines = Math.sign(ret.delta) *
Math.floor(Math.abs(ret.delta) / lineHeight);
ret.delta -= ret.lines * lineHeight;
}
// Scrolling by line
else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE)
ret.lines = Math.trunc(e.deltaY);
// Scrolling by page
else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE)
ret.lines = Math.trunc(e.deltaY) * pageLines;
// Unknown scrolling mode
else ret.lines = 3 * Math.sign(e.deltaY);
return ret;
}
///////////////////////// Initialization Methods //////////////////////////
constructor(app, sim, index) {
// Configure instance fields
this.app = app;
this.core = app.core;
this.dasm = app.dasm;
this.sim = sim;
// Configure debugger windows
this.cpu = new Debugger.CPU (this, index);
this.memory = new Debugger.Memory(this, index);
}
///////////////////////////// Package Methods /////////////////////////////
// Ensure PC is visible in the disassembler
followPC(pc = null) {
this.cpu.disassembler.followPC(pc);
}
// Format a number as hexadecimal
hex(value, digits = null, decorated = true) {
return this.dasm.hex(value, digits, decorated);
}
}
// Register component classes
(await import(/**/"./CPU.js" )).register(Debugger);
(await import(/**/"./Memory.js")).register(Debugger);
export { Debugger };

177
web/debugger/ISX.js Normal file
View File

@ -0,0 +1,177 @@
// Debug manager for Intelligent Systems binaries
class ISX {
///////////////////////// Initialization Methods //////////////////////////
// Throws on decoding error
constructor(data) {
// Configure instance fields
this.data = data;
this.offset = 0;
this.ranges = [];
this.symbols = [];
this.codes = [];
// Skip any header that may be present
if (data.length >= 32 && this.readInt(3) == 0x585349)
this.offset = 32;
else this.offset = 0;
// Process all records
while (this.offset < this.data.length) {
switch (this.readInt(1)) {
// Virtual Boy records
case 0x11: this.code (); break;
case 0x13: this.range (); break;
case 0x14: this.symbol(); break;
// System records
case 0x20:
case 0x21:
case 0x22:
let length = this.readInt(4);
this.offset += length;
break;
// Other records
default: throw "ISX decode error";
}
}
// Cleanup instance fields
delete this.data;
delete this.decoder;
delete this.offset;
}
///////////////////////////// Public Methods //////////////////////////////
// Produce a .vb format ROM file from the ISX code segments
toROM() {
let head = 0x00000000;
let tail = 0x01000000;
// Inspect all code segments
for (let code of this.codes) {
let start = code.address & 0x00FFFFFF;
let end = start + code.data.length;
// Segment begins in the first half of ROM
if (start < 0x00800000) {
// Segment ends in the second half of ROM
if (end > 0x00800000) {
head = tail = 0;
break;
}
// Segment ends in the first half of ROM
else if (end > head)
head = end;
}
// Segment begins in the second half of ROM
else if (start < tail)
tail = start;
}
// Prepare the output buffer
let min = head + 0x01000000 - tail;
let size = 1;
for (; size < min; size <<= 1);
let rom = new Uint8Array(size);
// Output all code segments
for (let code of this.codes) {
let dest = code.address & rom.length - 1;
for (let src = 0; src < code.data.length; src++, dest++)
rom[dest] = code.data[src];
}
return rom;
}
///////////////////////////// Private Methods /////////////////////////////
// Process a code record
code() {
let address = this.readInt(4);
let length = this.readInt(4);
let data = this.readBytes(length);
if (
length == 0 ||
length > 0x01000000 ||
(address & 0x07000000) != 0x07000000 ||
(address & 0x07000000) + length > 0x08000000
) throw "ISX decode error";
this.codes.push({
address: address,
data : data
});
}
// Process a range record
range() {
let count = this.readInt(2);
while (count--) {
let start = this.readInt(4);
let end = this.readInt(4);
let type = this.readInt(1);
this.ranges.push({
end : end,
start: start,
type : type
});
}
}
// Process a symbol record
symbol() {
let count = this.readInt(2);
while (count--) {
let length = this.readInt(1);
let name = this.readString(length);
let flags = this.readInt(2);
let address = this.readInt(4);
this.symbols.push({
address: address,
flags : flags,
name : name
});
}
}
// Read a byte buffer
readBytes(size) {
if (this.offset + size > this.data.length)
throw "ISX decode error";
let ret = this.data.slice(this.offset, this.offset + size);
this.offset += size;
return ret;
}
// Read an integer
readInt(size) {
if (this.offset + size > this.data.length)
throw "ISX decode error";
let ret = new Uint32Array(1);
for (let shift = 0; size > 0; size--, shift += 8)
ret[0] |= this.data[this.offset++] << shift;
return ret[0];
}
// Read a text string
readString(size) {
return (this.decoder = this.decoder || new TextDecoder()
).decode(this.readBytes(size));
}
}
export { ISX };

558
web/debugger/Memory.js Normal file
View File

@ -0,0 +1,558 @@
import { Toolkit } from /**/"../toolkit/Toolkit.js";
let register = Debugger => Debugger.Memory =
// Debugger memory window
class Memory extends Toolkit.Window {
//////////////////////////////// Constants ////////////////////////////////
// Bus indexes
static MEMORY = 0;
///////////////////////// Initialization Methods //////////////////////////
constructor(debug, index) {
super(debug.app, {
class: "tk window memory"
});
// Configure instance fields
this.data = null,
this.dataAddress = null,
this.debug = debug;
this.delta = 0;
this.editDigit = null;
this.height = 300;
this.index = index;
this.lines = [];
this.pending = false;
this.shown = false;
this.subscription = [ 0, index, "memory", "refresh" ];
this.width = 400;
// Available buses
this.buses = [
{
index : Memory.MEMORY,
editAddress: 0x05000000,
viewAddress: 0x05000000
}
];
this.bus = this.buses[Memory.MEMORY];
// Window
this.setTitle("{debug.memory._}", true);
this.substitute("#", " " + (index + 1));
if (index == 1)
this.element.classList.add("two");
this.addEventListener("close" , e=>this.visible = false);
this.addEventListener("visibility", e=>this.onVisibility(e));
// Client area
Object.assign(this.client.style, {
display : "grid",
gridTemplateRows: "max-content auto"
});
// Bus drop-down
this.drpBus = new Toolkit.DropDown(debug.app);
this.drpBus.setLabel("{debug.memory.bus}", true);
this.drpBus.setTitle("{debug.memory.bus}", true);
this.drpBus.add("{debug.memory.busMemory}", true,
this.buses[Memory.MEMORY]);
this.drpBus.addEventListener("input", e=>this.busInput());
this.add(this.drpBus);
// Hex editor
this.hexEditor = new Toolkit.Component(debug.app, {
class : "tk mono hex-editor",
role : "application",
tabIndex: "0",
style : {
display : "grid",
gridTemplateColumns: "repeat(17, max-content)",
height : "100%",
minWidth : "100%",
overflow : "hidden",
position : "relative",
width : "max-content"
}
});
this.hexEditor.localize = ()=>{
this.hexEditor.localizeRoleDescription();
this.hexEditor.localizeLabel();
};
this.hexEditor.setLabel("{debug.memory.hexEditor}", true);
this.hexEditor.setRoleDescription("{debug.memory.hexEditor}", true);
this.hexEditor.addEventListener("keydown", e=>this.hexKeyDown(e));
this.hexEditor.addEventListener("resize" , e=>this.hexResize ( ));
this.hexEditor.addEventListener("wheel" , e=>this.hexWheel (e));
this.hexEditor.addEventListener(
"pointerdown", e=>this.hexPointerDown(e));
this.lastFocus = this.hexEditor;
// Label for measuring text dimensions
this.sizer = new Toolkit.Label(debug.app, {
class : "tk label mono",
visible : false,
visibility: true,
style: {
position: "absolute"
}
});
this.sizer.setText("\u00a0", false); // &nbsp;
this.hexEditor.append(this.sizer);
// Hex editor scroll pane
this.scrHex = new Toolkit.ScrollPane(debug.app, {
overflowX: "auto",
overflowY: "hidden",
view : this.hexEditor
});
this.add(this.scrHex);
// Hide the bus drop-down: Virtual Boy only has one bus
this.drpBus.visible = false;
this.client.style.gridTemplateRows = "auto";
}
///////////////////////////// Event Handlers //////////////////////////////
// Bus drop-down selection
busInput() {
// An edit is in progress
if (this.editDigit !== null)
this.commit(false);
// Switch to the new bus
this.bus = this.drpBus.value;
this.fetch();
}
// Hex editor key press
hexKeyDown(e) {
// Error checking
if (e.altKey)
return;
// Processing by key, Ctrl pressed
if (e.ctrlKey) switch (e.key) {
case "g": case "G":
Toolkit.handle(e);
this.goto();
return;
default: return;
}
// Processing by key, scroll lock off
if (!e.getModifierState("ScrollLock")) switch (e.key) {
case "ArrowDown":
this.commit();
this.setEditAddress(this.bus.editAddress + 16);
Toolkit.handle(e);
return;
case "ArrowLeft":
this.commit();
this.setEditAddress(this.bus.editAddress - 1);
Toolkit.handle(e);
return;
case "ArrowRight":
this.commit();
this.setEditAddress(this.bus.editAddress + 1);
Toolkit.handle(e);
return;
case "ArrowUp":
this.commit();
this.setEditAddress(this.bus.editAddress - 16);
Toolkit.handle(e);
return;
case "PageDown":
this.commit();
this.setEditAddress(this.bus.editAddress + this.tall(true)*16);
Toolkit.handle(e);
return;
case "PageUp":
this.commit();
this.setEditAddress(this.bus.editAddress - this.tall(true)*16);
Toolkit.handle(e);
return;
}
// Processing by key, scroll lock on
else switch (e.key) {
case "ArrowDown":
this.bus.viewAddress += 16;
this.fetch();
Toolkit.handle(e);
return;
case "ArrowLeft":
this.scrHex.scrollLeft -= this.scrHex.hscroll.unitIncrement;
Toolkit.handle(e);
return;
case "ArrowRight":
this.scrHex.scrollLeft += this.scrHex.hscroll.unitIncrement;
Toolkit.handle(e);
return;
case "ArrowUp":
this.bus.viewAddress -= 16;
this.fetch();
Toolkit.handle(e);
return;
case "PageDown":
this.bus.viewAddress += this.tall(true) * 16;
this.fetch();
Toolkit.handle(e);
return;
case "PageUp":
this.bus.viewAddress -= this.tall(true) * 16;
this.fetch();
Toolkit.handle(e);
return;
}
// Processing by key, editing
switch (e.key) {
case "0": case "1": case "2": case "3": case "4":
case "5": case "6": case "7": case "8": case "9":
case "a": case "A": case "b": case "B": case "c":
case "C": case "d": case "D": case "e": case "E":
case "f": case "F":
let digit = parseInt(e.key, 16);
if (this.editDigit === null) {
this.editDigit = digit;
this.setEditAddress(this.bus.editAddress);
} else {
this.editDigit = this.editDigit << 4 | digit;
this.commit();
this.setEditAddress(this.bus.editAddress + 1);
}
break;
// Commit the current edit
case "Enter":
if (this.editDigit === null)
break;
this.commit();
this.setEditAddress(this.bus.editAddress + 1);
break;
// Cancel the current edit
case "Escape":
if (this.editDigit === null)
return;
this.editDigit = null;
this.setEditAddress(this.bus.editAddress);
break;
default: return;
}
Toolkit.handle(e);
}
// Hex editor pointer down
hexPointerDown(e) {
// Error checking
if (e.button != 0)
return;
// Working variables
let cols = this.lines[0].lblBytes.map(l=>l.getBoundingClientRect());
let y = Math.max(0, Math.floor((e.clientY-cols[0].y)/cols[0].height));
let x = 15;
// Determine which column is closest to the touch point
if (e.clientX < cols[15].right) {
for (let l = 0; l < 15; l++) {
if (e.clientX > (cols[l].right + cols[l + 1].x) / 2)
continue;
x = l;
break;
}
}
// Update the selection address
let address = this.bus.viewAddress + y * 16 + x >>> 0;
if (this.editDigit !== null && address != this.bus.editAddress)
this.commit();
this.setEditAddress(address);
}
// Hex editor resized
hexResize() {
let tall = this.tall(false);
let grew = this.lines.length < tall;
// Process all visible lines
for (let y = this.lines.length; y < tall; y++) {
let line = {
lblAddress: document.createElement("div"),
lblBytes : []
};
// Address label
line.lblAddress.className = "addr" + (y == 0 ? " first" : "");
this.hexEditor.append(line.lblAddress);
// Byte labels
for (let x = 0; x < 16; x++) {
let lbl = line.lblBytes[x] = document.createElement("div");
lbl.className = "byte b" + x + (y == 0 ? " first" : "");
this.hexEditor.append(lbl);
}
this.lines.push(line);
}
// Remove lines that are no longer visible
while (tall < this.lines.length) {
let line = this.lines[tall];
line.lblAddress.remove();
for (let lbl of line.lblBytes)
lbl.remove();
this.lines.splice(tall, 1);
}
// Configure scroll bar
this.scrHex.hscroll.unitIncrement =
this.sizer.element.getBoundingClientRect().height;
// Update components
if (grew)
this.fetch();
else this.refresh();
}
// Hex editor mouse wheel
hexWheel(e) {
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey)
return;
// Always handle the event
Toolkit.handle(e);
// Determine how many full lines were scrolled
let scr = Debugger.linesScrolled(e,
this.sizer.element.getBoundingClientRect().height,
this.tall(true),
this.delta
);
this.delta = scr.delta;
scr.lines = Math.max(-3, Math.min(3, scr.lines));
// No lines were scrolled
if (scr.lines == 0)
return;
// Scroll the view
this.bus.viewAddress = this.bus.viewAddress + scr.lines * 16 >>> 0;
this.fetch();
}
// Window visibility
onVisibility(e) {
this.shown = this.shown || e.visible;
if (!e.visible)
this.debug.core.unsubscribe(this.subscription, false);
else this.fetch();
}
///////////////////////////// Package Methods /////////////////////////////
// Prompt the user to navigate to a new editing address
goto() {
// Retrieve the value from the user
let addr = prompt(this.app.localize("{debug.memory.goto}"));
if (addr === null)
return;
addr = parseInt(addr.trim(), 16);
if (
!Number.isInteger(addr) ||
addr < 0 ||
addr > 4294967295
) return;
// Commit an outstanding edit
if (this.editDigit !== null && this.bus.editAddress != addr)
this.commit();
// Navigate to the given address
this.setEditAddress(addr, 1/3);
}
///////////////////////////// Private Methods /////////////////////////////
// Write the edited value to the simulation state
commit(refresh = true) {
// Error checking
if (this.editDigit === null)
return;
// The edited value is in the bus's data buffer
if (this.data != null) {
let offset = this.bus.editAddress - this.dataAddress >>> 0;
if (offset < this.data.length)
this.data[offset] = this.editDigit;
}
// Write one byte to the simulation state
let data = new Uint8Array(1);
data[0] = this.editDigit;
this.editDigit = null;
this.debug.core.write(this.debug.sim, this.bus.editAddress,
data, { refresh: refresh });
}
// Retrieve data from the simulation state
async fetch() {
// Select the parameters for the simulation fetch
let params = {
address: this.bus.viewAddress - 10 * 16,
length : (this.tall(false) + 20) * 16
};
// A communication with the core thread is already underway
if (this.pending) {
this.pending = params;
this.refresh();
return;
}
// Retrieve data from the simulation state
this.pending = params;
for (let data=null, promise=null; this.pending instanceof Object;) {
// Wait for a transaction to complete
if (promise != null) {
this.pending = true;
data = await promise;
promise = null;
}
// Initiate a new transaction
if (this.pending instanceof Object) {
params = this.pending;
let options = {};
if (this.isVisible())
options.subscription = this.subscription;
promise = this.debug.core.read(this.debug.sim,
params.address, params.length, options);
}
// Process the result of a transaction
if (data != null) {
this.refresh(data);
data = null;
}
};
this.pending = false;
}
// Update hex editor
refresh(msg = null) {
// Receiving data from the simulation state
if (msg != null) {
this.data = msg.data;
this.dataAddress = msg.address;
}
// Process all lines
for (let y = 0; y < this.lines.length; y++) {
let address = this.bus.viewAddress + y * 16 >>> 0;
let line = this.lines[y];
// Address label
line.lblAddress.innerText = this.debug.hex(address, 8, false);
// Process all bytes
for (let x = 0; x < 16; x++) {
let label = line.lblBytes[x];
let text = "--";
// Currently editing this byte
if (address+x==this.bus.editAddress && this.editDigit!==null) {
text = this.debug.hex(this.editDigit, 1, false);
}
// Bus data exists
else if (this.data != null) {
let offset = address - this.dataAddress + x >>> 0;
// The byte is contained in the bus data buffer
if (offset >= 0 && offset < this.data.length)
text = this.debug.hex(this.data[offset], 2, false);
}
label.innerText = text;
label.classList[address + x == this.bus.editAddress ?
"add" : "remove"]("edit");
}
}
}
// Specify the address of the hex editor's selection
setEditAddress(address, auto = false) {
let col = this.lines[0].lblBytes[address&15].getBoundingClientRect();
let port = this.scrHex.viewport.element.getBoundingClientRect();
let row = (address & ~15) >>> 0;
let scr = this.scrHex.scrollLeft;
let tall = this.tall(true, 0);
// Ensure the data row is fully visible
if (row - this.bus.viewAddress >>> 0 >= tall * 16) {
if (!auto) {
this.bus.viewAddress =
this.bus.viewAddress - row >>> 0 <=
row - (this.bus.viewAddress + tall * 16) >>> 0
? row : row - (tall - 1) * 16;
} else this.bus.viewAddress = row - Math.floor(tall * auto) * 16;
this.fetch();
}
// Ensure the column is fully visible
this.scrHex.scrollLeft =
Math.min(
Math.max(
scr,
scr + col.right - port.right
),
scr - port.x + col.x
)
;
// Refresh the display;
this.bus.editAddress = address;
this.refresh();
}
// Measure the number of lines visible in the view
tall(fully = null, plus = 1) {
return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"](
this.scrHex.viewport.element.getBoundingClientRect().height /
this.sizer .element.getBoundingClientRect().height
)) + plus;
}
}
export { register };

77
web/locale/en-US.json Normal file
View File

@ -0,0 +1,77 @@
{
"id" : "en-US",
"name": "English (US)",
"app": {
"title": "Virtual Boy Emulator"
},
"menu._": "Main menu",
"menu.file": {
"_" : "File",
"loadROM" : "Load ROM{#}...",
"loadROMError" : "Error loading ROM file",
"loadROMInvalid": "The selected file is not a Virtual Boy ROM.",
"dualMode" : "Dual mode",
"debugMode" : "Debug mode"
},
"menu.emulation": {
"_" : "Emulation",
"run" : "Run",
"pause" : "Pause",
"reset" : "Reset{#}",
"linkSims": "Link sims"
},
"menu.debug": {
"_" : "Debug{#}",
"backgrounds" : "Backgrounds",
"bgMaps" : "BG maps",
"breakpoints" : "Breakpoints",
"characters" : "Characters",
"console" : "Console",
"cpu" : "CPU",
"frameBuffers": "Frame buffers",
"memory" : "Memory",
"objects" : "Objects",
"palettes" : "Palettes"
},
"menu.theme": {
"_" : "Theme",
"auto" : "Auto",
"dark" : "Dark",
"light" : "Light",
"virtual": "Virtual"
},
"window": {
"close": "Close"
},
"debug.cpu": {
"_" : "CPU{#}",
"disassembler" : "Disassembler",
"float" : "Float",
"format" : "Format",
"goto" : "Enter the address to seek to:",
"hex" : "Hex",
"infinity" : "Infinity",
"programRegisters": "Program registers",
"signed" : "Signed",
"systemRegisters" : "System registers",
"unsigned" : "Unsigned",
"value" : "Value"
},
"debug.memory": {
"_" : "Memory{#}",
"bus" : "Bus",
"busMemory": "Memory",
"goto" : "Enter the address to seek to:",
"hexEditor": "Hex editor"
}
}

1
web/template.html Normal file
View File

@ -0,0 +1 @@
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon"href="data:;base64,iVBORw0KGgo="><title>Virtual Boy Emulator</title></head><body><img style="display:none;"onload="let a=this,b=a.width,c=a.height,d=document.createElement('canvas');a.remove();d.width=b;d.height=c;d=d.getContext('2d');d.drawImage(a,0,0);d=d.getImageData(0,0,b,c).data.filter((e,f)=>!(f&3));new(async()=>{}).constructor(Array.from(d.slice(0,d.indexOf(0))).map(e=>String.fromCharCode(e)).join(''))(d,a)"src=""></body></html>

View File

Before

Width:  |  Height:  |  Size: 459 B

After

Width:  |  Height:  |  Size: 459 B

View File

Before

Width:  |  Height:  |  Size: 652 B

After

Width:  |  Height:  |  Size: 652 B

View File

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 367 B

28
web/theme/dark.css Normal file
View File

@ -0,0 +1,28 @@
:root {
--tk-control : #333333;
--tk-control-active : #555555;
--tk-control-border : #cccccc;
--tk-control-highlight : #444444;
--tk-control-shadow : #9b9b9b;
--tk-control-text : #cccccc;
--tk-desktop : #111111;
--tk-selected : #008542;
--tk-selected-blur : #325342;
--tk-selected-blur-text : #ffffff;
--tk-selected-text : #ffffff;
--tk-splitter-focus : #008542c0;
--tk-window : #222222;
--tk-window-blur-close : #d9aeae;
--tk-window-blur-close-text : #eeeeee;
--tk-window-blur-title : #9fafb9;
--tk-window-blur-title2 : #c0b0a0;
--tk-window-blur-title-text : #444444;
--tk-window-close : #ee9999;
--tk-window-close-focus : #99ee99;
--tk-window-close-focus-text: #333333;
--tk-window-close-text : #ffffff;
--tk-window-text : #cccccc;
--tk-window-title : #80ccff;
--tk-window-title2 : #ffb894;
--tk-window-title-text : #000000;
}

View File

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

548
web/theme/kiosk.css Normal file
View File

@ -0,0 +1,548 @@
:root {
--tk-font-dialog : "Roboto", sans-serif;
--tk-font-mono : "Inconsolata SemiExpanded Medium", monospace;
--tk-font-size : 12px;
}
@font-face {
font-family: "Roboto";
src : /**/url("./roboto.woff2") format("woff2");
}
@font-face {
font-family: "Inconsolata SemiExpanded Medium";
src : /**/url("./inconsolata.woff2") format("woff2");
}
body {
background: var(--tk-control);
}
.tk {
box-sizing : border-box;
font-family: var(--tk-font-dialog);
font-size : var(--tk-font-size);
line-height: 1em;
margin : 0;
outline : none; /* User agent focus indicator */
padding : 0;
}
table.tk {
border : none;
border-spacing: 0;
}
.tk.mono {
font-family: var(--tk-font-mono);
}
.tk::selection,
.tk *::selection {
background: var(--tk-selected);
color : var(--tk-selected-text);
}
.tk:not(:focus-within)::selection,
.tk *:not(:focus-within)::selection {
background: var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
}
.tk.display {
background: var(--tk-desktop);
}
.tk.desktop {
background: var(--tk-desktop);
}
/********************************** Button ***********************************/
.tk.button {
align-items : stretch;
display : inline-grid;
grid-template-columns: auto;
justify-content : stretch;
padding : 0 1px 1px 0;
}
.tk.button .label {
align-items : center;
background : var(--tk-control);
border : 1px solid var(--tk-control-border);
box-shadow : 1px 1px 0 var(--tk-control-border);
color : var(--tk-control-text);
display : grid;
grid-template-columns: auto;
justify-content : center;
padding : 2px;
}
.tk.button:focus .label {
background: var(--tk-control-active);
}
.tk.button.pushed {
padding: 1px 0 0 1px;
}
.tk.button.pushed .label {
box-shadow: none;
}
.tk.button[aria-disabled="true"] .label {
color : var(--tk-control-shadow);
border : 1px solid var(--tk-control-shadow);
box-shadow: 1px 1px 0 var(--tk-control-shadow);
}
/********************************* Checkbox **********************************/
.tk.checkbox {
column-gap: 2px;
}
.tk.checkbox .box {
border: 1px solid var(--tk-control-shadow);
color : var(--tk-control-text);
}
.tk.checkbox:focus .box {
background: var(--tk-control-active);
}
.tk.checkbox .box:before {
background : transparent;
content : "";
display : block;
height : 10px;
mask : /**/url("./check.svg") center no-repeat;
-webkit-mask: /**/url("./check.svg") center no-repeat;
width : 10px;
}
.tk.checkbox[aria-checked="true"] .box:before {
background: currentcolor;
}
.tk.checkbox.pushed .box:before {
background: var(--tk-control-shadow);
}
.tk.checkbox[aria-disabled="true"] .box {
background: var(--tk-control);
color : var(--tk-control-shadow);
}
.tk.checkbox[aria-disabled="true"] .label {
color: var(--tk-control-shadow);
}
/********************************* DropDown **********************************/
.tk.drop-down {
background: var(--tk-window);
border : 1px solid var(--tk-control-shadow);
color : var(--tk-window-text);
padding : 2px;
}
.tk.drop-down:focus {
background: var(--tk-control-active);
}
.tk.drop-down[aria-disabled="true"] {
color: var(--tk-control-shadow);
}
/*********************************** Menus ***********************************/
.tk.menu-bar {
background : var(--tk-control);
border-bottom: 1px solid var(--tk-control-border);
color : var(--tk-control-text);
cursor : default;
padding : 2px;
position : relative;
}
.tk.menu {
background: var(--tk-control);
border : 1px solid var(--tk-control-border);
box-shadow: 1px 1px 0 var(--tk-control-border);
color : var(--tk-control-text);
margin : -1px 0 0 1px;
padding : 2px;
}
.tk.menu-item[aria-disabled="true"] {
color: var(--tk-control-shadow);
}
.tk.menu-item > * {
align-items: center;
border : 1px solid transparent;
column-gap : 4px;
display : flex;
margin : 0 1px 1px 0;
padding : 2px;
user-select: none;
}
.tk.menu-item .icon {
box-sizing: border-box;
height : 1em;
width : 1em;
}
.tk.menu-item .icon:before {
content: "";
display: block;
height : 100%;
width : 100%;
}
.tk.menu-bar > .menu-item .icon,
.tk.menu:not(.icons) > .menu-item .icon {
display: none;
}
.tk.menu-item.checkbox .icon {
border: 1px solid currentcolor;
}
.tk.menu-item.checkbox[aria-checked="true"] .icon:before {
background : currentcolor;
mask : /**/url("./check.svg") center no-repeat;
-webkit-mask: /**/url("./check.svg") center no-repeat;
}
.tk.menu-item .label {
flex-grow: 1;
}
.tk.menu-item:not([aria-expanded="true"],
[aria-disabled="true"], .pushed):hover > *,
.tk.menu-item:not([aria-expanded="true"], .pushed):focus > * {
border : 1px solid var(--tk-control-shadow);
box-shadow: 1px 1px 0 var(--tk-control-shadow);
}
.tk.menu-item:focus > * {
background: var(--tk-control-active);
}
.tk.menu-item.pushed > *,
.tk.menu-item[aria-expanded="true"] > * {
background: var(--tk-control-active);
border : 1px solid var(--tk-control-shadow);
box-shadow: none;
margin : 1px 0 0 1px;
}
.tk.menu > [role="separator"] {
border : solid var(--tk-control-shadow);
border-width: 1px 0 0 0;
margin : 4px 2px;
}
/*********************************** Radio ***********************************/
.tk.radio {
column-gap: 2px;
}
.tk.radio .box {
border : 1px solid var(--tk-control-shadow);
border-radius: 50%;
color : var(--tk-control-text);
margin : 1px;
}
.tk.radio:focus .box {
background: var(--tk-control-active);
}
.tk.radio .box:before {
background : transparent;
border-radius: 50%;
content : "";
display : block;
height : 4px;
margin : 2px;
width : 4px;
}
.tk.radio[aria-checked="true"] .box:before {
background: currentcolor;
}
.tk.radio.pushed .box:before {
background: var(--tk-control-shadow);
}
.tk.radio[aria-disabled="true"] .box {
background: var(--tk-control);
color : var(--tk-control-shadow);
}
.tk.radio[aria-disabled="true"] .label {
color: var(--tk-control-shadow);
}
/********************************* ScrollBar *********************************/
.tk.scroll-bar {
border : 1px solid var(--tk-control-shadow);
box-sizing: border-box;
}
.tk.scroll-bar .unit-less,
.tk.scroll-bar .unit-more {
background: var(--tk-control);
border : 0 solid var(--tk-control-shadow);
color : var(--tk-control-text);
height : 11px;
width : 11px;
}
.tk.scroll-bar[aria-orientation="horizontal"] .unit-less {
border-right-width: 1px;
}
.tk.scroll-bar[aria-orientation="horizontal"] .unit-more {
border-left-width: 1px;
}
.tk.scroll-bar[aria-orientation="vertical"] .unit-less {
border-bottom-width: 1px;
}
.tk.scroll-bar[aria-orientation="vertical"] .unit-more {
border-top-width: 1px;
}
.tk.scroll-bar .unit-less:before,
.tk.scroll-bar .unit-more:before {
background : currentColor;
content : "";
display : block;
height : 100%;
mask : /**/url("./scroll.svg") center no-repeat;
-webkit-mask: /**/url("./scroll.svg") center no-repeat;
width : 100%;
}
.tk.scroll-bar .unit-less.pushed:before,
.tk.scroll-bar .unit-more.pushed:before {
mask-size : 9px;
-webkit-mask-size: 9px;
}
.tk.scroll-bar[aria-orientation="horizontal"] .unit-less:before {
transform: rotate(-90deg);
}
.tk.scroll-bar[aria-orientation="horizontal"] .unit-more:before {
transform: rotate(90deg);
}
.tk.scroll-bar[aria-orientation="vertical"] .unit-more:before {
transform: rotate(180deg);
}
.tk.scroll-bar .track {
background: var(--tk-control-highlight);
}
.tk.scroll-bar .thumb {
background: var(--tk-control);
box-shadow: 0 0 0 1px var(--tk-control-shadow);
}
.tk.scroll-bar .block-less.pushed,
.tk.scroll-bar .block-more.pushed {
background: var(--tk-control-shadow);
opacity : 0.5;
}
.tk.scroll-bar:focus .unit-less,
.tk.scroll-bar:focus .unit-more,
.tk.scroll-bar:focus .thumb {
background: var(--tk-control-active);
}
.tk.scroll-bar[aria-disabled="true"] .unit-less,
.tk.scroll-bar[aria-disabled="true"] .unit-more,
.tk.scroll-bar.unneeded .unit-less,
.tk.scroll-bar.unneeded .unit-more,
.tk.scroll-bar[aria-disabled="true"] .thumb {
color: var(--tk-control-shadow);
}
.tk.scroll-bar.unneeded .thumb {
visibility: hidden;
}
/******************************** ScrollPane *********************************/
.tk.scroll-pane {
border: 1px solid var(--tk-control-shadow);
}
.tk.scroll-pane > .scroll-bar[aria-orientation="horizontal"] {
border-width: 1px 1px 0 0;
}
.tk.scroll-pane:not(.vertical) > .scroll-bar[aria-orientation="horizontal"] {
border-width: 1px 0 0 0;
}
.tk.scroll-pane > .scroll-bar[aria-orientation="vertical"] {
border-width: 0 0 1px 1px;
}
.tk.scroll-pane:not(.horizontal) > .scroll-bar[aria-orientation="vertical"] {
border-width: 0 0 0 1px;
}
.tk.scroll-pane > .viewport,
.tk.scroll-pane > .corner {
background: var(--tk-control);
}
/********************************* SplitPane *********************************/
.tk.split-pane > [role="separator"]:focus {
background: var(--tk-splitter-focus);
}
.tk.split-pane > .horizontal[role="separator"] {
width: 3px;
}
.tk.split-pane > .vertical[role="separator"] {
height: 3px;
}
/********************************** TextBox **********************************/
.tk.text-box {
background : var(--tk-window);
border : 1px solid var(--tk-control-border);
color : var(--tk-window-text);
line-height: 1em;
height : calc(1em + 2px);
padding : 0;
margin : 0;
min-width : 0;
}
.tk.text-box.[aria-disabled="true"] {
background: var(--tk-control-shadow);
color : var(--tk-window-text);
}
/********************************** Windows **********************************/
.tk.window {
background: var(--tk-control);
border : 1px solid var(--tk-control-shadow);
box-shadow: 1px 1px 0 var(--tk-control-shadow);
}
.tk.window:focus-within {
border : 1px solid var(--tk-control-border);
box-shadow: 1px 1px 0 var(--tk-control-border);
}
.tk.window > .nw1 { left : -2px; top : -2px; width : 8px; height: 3px; }
.tk.window > .nw2 { left : -2px; top : 1px; width : 3px; height: 5px; }
.tk.window > .n { left : 6px; top : -2px; right : 6px; height: 3px; }
.tk.window > .ne1 { right: -2px; top : -2px; width : 8px; height: 3px; }
.tk.window > .ne2 { right: -2px; top : 1px; width : 3px; height: 5px; }
.tk.window > .w { left : -2px; top : 6px; bottom: 6px; width : 3px; }
.tk.window > .e { right: -2px; top : 6px; bottom: 6px; width : 3px; }
.tk.window > .sw1 { left : -2px; bottom: -2px; width : 8px; height: 3px; }
.tk.window > .sw2 { left : -2px; bottom: 1px; width : 3px; height: 5px; }
.tk.window > .s { left : 6px; bottom: -2px; right : 6px; height: 3px; }
.tk.window > .se1 { right: -2px; bottom: -2px; width : 8px; height: 3px; }
.tk.window > .se2 { right: -2px; bottom: 1px; width : 3px; height: 5px; }
.tk.window > .title {
align-items : center;
background : var(--tk-window-blur-title);
border-bottom: 1px solid var(--tk-control-shadow);
color : var(--tk-window-blur-title-text);
padding : 1px;
user-select : none;
}
.tk.window:focus-within > .title {
background: var(--tk-window-title);
color : var(--tk-window-title-text);
}
.tk.window.two > .title {
background: var(--tk-window-blur-title2);
}
.tk.window.two:focus-within > .title {
background: var(--tk-window-title2);
}
.tk.window > .title .text {
cursor : default;
font-weight : bold;
overflow : hidden;
text-align : center;
text-overflow: ellipsis;
white-space : nowrap;
}
.tk.window > .title .close-button {
background: var(--tk-window-blur-close);
border : 1px solid var(--tk-control-shadow);
box-sizing: border-box;
color : var(--tk-window-close-text);
height : 13px;
width : 13px;
}
.tk.window:focus-within > .title .close-button {
background: var(--tk-window-close);
}
.tk.window > .title .close-button:focus {
background: var(--tk-window-close-focus);
color : var(--tk-window-close-focus-text);
outline : 1px solid var(--tk-control);
}
.tk.window > .title .close-button:before {
background : currentcolor;
content : "";
display : block;
height : 11px;
mask : /**/url("./close.svg") center no-repeat;
-webkit-mask: /**/url("./close.svg") center no-repeat;
width : 11px;
}
.tk.window > .title .close-button.pushed:before {
mask-size : 9px;
-webkit-mask-size: 9px;
}
.tk.window > .client {
overflow: hidden;
}

28
web/theme/light.css Normal file
View File

@ -0,0 +1,28 @@
:root {
--tk-control : #eeeeee;
--tk-control-active : #cccccc;
--tk-control-border : #000000;
--tk-control-highlight : #f8f8f8;
--tk-control-shadow : #6c6c6c;
--tk-control-text : #000000;
--tk-desktop : #cccccc;
--tk-selected : #008542;
--tk-selected-blur : #5e7d70;
--tk-selected-blur-text : #ffffff;
--tk-selected-text : #ffffff;
--tk-splitter-focus : #008542c0;
--tk-window : #ffffff;
--tk-window-blur-close : #d9aeae;
--tk-window-blur-close-text : #eeeeee;
--tk-window-blur-title : #aac4d5;
--tk-window-blur-title2 : #dbc4b8;
--tk-window-blur-title-text : #444444;
--tk-window-close : #ee9999;
--tk-window-close-focus : #99ee99;
--tk-window-close-focus-text: #333333;
--tk-window-close-text : #ffffff;
--tk-window-text : #000000;
--tk-window-title : #80ccff;
--tk-window-title2 : #ffb894;
--tk-window-title-text : #000000;
}

View File

Before

Width:  |  Height:  |  Size: 361 B

After

Width:  |  Height:  |  Size: 361 B

View File

Before

Width:  |  Height:  |  Size: 484 B

After

Width:  |  Height:  |  Size: 484 B

223
web/theme/vbemu.css Normal file
View File

@ -0,0 +1,223 @@
/******************************** CPU Window *********************************/
.tk.window.cpu .client {
padding: 1px;
}
.tk.window.cpu .scr-dasm {
border-right-width: 0;
box-shadow : 1px 0 0 var(--tk-control-shadow);
}
.tk.window.cpu .scr-system {
border-width: 1px 1px 0 0;
box-shadow : -0.5px 0.5px 0 0.5px var(--tk-control-shadow);
}
.tk.window.cpu .scr-program {
border-width: 0 1px 1px 0;
box-shadow : -0.5px -0.5px 0 0.5px var(--tk-control-shadow);
}
.tk.window.cpu .disassembler {
background : var(--tk-window);
color : var(--tk-window-text);
}
.tk.window.cpu .disassembler div {
cursor : default;
line-height: calc(1em + 2px);
user-select: none;
}
.tk.window.cpu .disassembler .addr {
margin-left: 2px;
}
.tk.window.cpu .disassembler .spacer {
margin-right: 2px;
}
.tk.window.cpu .disassembler .byte {
margin-left: 0.5em;
text-align : center;
}
.tk.window.cpu .disassembler .byte.b0,
.tk.window.cpu .disassembler .inst,
.tk.window.cpu .disassembler .ops {
margin-left: 1em;
}
.tk.window.cpu .disassembler .pc {
background: var(--tk-selected-blur);
box-shadow: 0 1px 0 var(--tk-selected-blur),
0 -1px 0 var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
}
.tk.window.cpu .disassembler:focus-within .pc {
background: var(--tk-selected);
box-shadow: 0 1px 0 var(--tk-selected),
0 -1px 0 var(--tk-selected);
color : var(--tk-selected-text);
}
.tk.window.cpu .disassembler .pc.addr {
box-shadow: 0 0 0 1px var(--tk-selected-blur);
}
.tk.window.cpu .disassembler:focus-within .pc.addr {
box-shadow: 0 0 0 1px var(--tk-selected);
}
.tk.window.cpu .disassembler .pc:is(.byte) {
box-shadow: 0 0 0 1px var(--tk-selected-blur),
calc(-0.5em + 1px) 0 0 1px var(--tk-selected-blur);
}
.tk.window.cpu .disassembler:focus-within .pc:is(.byte) {
box-shadow: 0 0 0 1px var(--tk-selected),
calc(-0.5em + 1px) 0 0 1px var(--tk-selected);
}
.tk.window.cpu .disassembler .pc:is(.byte.b0, .inst, .ops) {
box-shadow: 0 0 0 1px var(--tk-selected-blur),
calc(-1em + 1px) 0 0 1px var(--tk-selected-blur);
}
.tk.window.cpu .disassembler:focus-within .pc:is(.byte.b0, .inst, .ops) {
box-shadow: 0 0 0 1px var(--tk-selected),
calc(-1em + 1px) 0 0 1px var(--tk-selected);
}
.tk.window.cpu .disassembler .spacer.pc {
box-shadow: 1px 1px 0 var(--tk-selected-blur),
1px -1px 0 var(--tk-selected-blur);
}
.tk.window.cpu .disassembler:focus-within .spacer.pc {
box-shadow: 1px 1px 0 var(--tk-selected),
1px -1px 0 var(--tk-selected);
}
.tk.window.cpu .registers {
background: var(--tk-window);
color : var(--tk-window-text);
}
.tk.window.cpu .registers > * {
padding: 0 1px;
}
.tk.window.cpu .registers > *:first-child {
padding-top: 1px;
}
.tk.window.cpu .registers > *:last-child {
padding-bottom: 1px;
}
.tk.window.cpu .registers .icon {
border-radius: 2px;
margin : 0 1px 1px 0;
}
.tk.window.cpu .registers .expand:focus .icon {
background: var(--tk-control-active);
}
.tk.window.cpu .registers .expand .icon:before {
content: "";
display: block;
height : 11px;
width : 11px;
}
.tk.window.cpu .registers .expand[aria-expanded="false"] .icon:before {
background : currentcolor;
mask : /**/url("./expand.svg") center no-repeat;
-webkit-mask: /**/url("./expand.svg") center no-repeat;
}
.tk.window.cpu .registers .expand[aria-expanded="true"] .icon:before {
background : currentcolor;
mask : /**/url("./collapse.svg") center no-repeat;
-webkit-mask: /**/url("./collapse.svg") center no-repeat;
}
.tk.window.cpu .registers .expansion {
gap : 1px 1em;
padding: 2px 2px 2px 1.4em;
}
.tk.window.cpu .registers .main {
column-gap: 0.5em;
}
.tk.window.cpu .registers .text-box {
background: transparent;
border : none;
padding : 0 1px;
}
.tk.window.cpu .registers .text-box:focus {
outline: 1px solid var(--tk-selected);
}
.tk.window.cpu .registers .text-dec {
column-gap: 2px;
}
.tk.window.cpu .registers .text-dec .label {
text-align: center;
min-width : 13px;
}
.tk.window.cpu .registers *[aria-disabled="true"]:is(.label, .text-box) {
color: var(--tk-control-shadow);
}
/******************************* Memory Window *******************************/
.tk.window.memory .client {
gap : 1px;
padding: 1px;
}
.tk.window.memory .hex-editor {
align-items: center;
background : var(--tk-window);
color : var(--tk-window-text);
}
.tk.window.memory .hex-editor div {
cursor : default;
line-height: calc(1em + 2px);
user-select: none;
}
.tk.window.memory .hex-editor .addr {
margin-left: 2px;
}
.tk.window.memory .hex-editor .byte {
margin-left: 0.5em;
text-align : center;
}
.tk.window.memory .hex-editor .b0,
.tk.window.memory .hex-editor .b8 {
margin-left: 1em;
}
.tk.window.memory .hex-editor .b15 {
margin-right: 2px;
}
.tk.window.memory .hex-editor .edit {
background: var(--tk-selected-blur);
color : var(--tk-selected-blur-text);
outline : 1px solid var(--tk-selected-blur);
}
.tk.window.memory .hex-editor:focus-within .edit {
background: var(--tk-selected);
color : var(--tk-selected-text);
outline : 1px solid var(--tk-selected);
}

63
web/theme/virtual.css Normal file
View File

@ -0,0 +1,63 @@
:root {
--tk-control : #000000;
--tk-control-active : #550000;
--tk-control-border : #ff0000;
--tk-control-highlight : #550000;
--tk-control-shadow : #aa0000;
--tk-control-text : #ff0000;
--tk-desktop : #000000;
--tk-selected : #aa0000;
--tk-selected-blur : #550000;
--tk-selected-blur-text : #ff0000;
--tk-selected-text : #000000;
--tk-splitter-focus : #ff0000aa;
--tk-window : #000000;
--tk-window-blur-close : #000000;
--tk-window-blur-close-text : #aa0000;
--tk-window-blur-title : #000000;
--tk-window-blur-title2 : #000000;
--tk-window-blur-title-text : #aa0000;
--tk-window-close : #550000;
--tk-window-close-focus : #ff0000;
--tk-window-close-focus-text: #550000;
--tk-window-close-text : #ff0000;
--tk-window-text : #ff0000;
--tk-window-title : #550000;
--tk-window-title2 : #550000;
--tk-window-title-text : #ff0000;
}
input, select {
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
}
.tk.scroll-bar .unit-less,
.tk.scroll-bar .unit-more,
.tk.scroll-bar .thumb {
background: #550000;
}
.tk.scroll-bar .track {
background: #000000;
}
.tk.scroll-bar:focus,
.tk.scroll-bar:focus .unit-less,
.tk.scroll-bar:focus .unit-more,
.tk.scroll-bar:focus .thumb {
background : #aa0000;
border-color: #ff0000;
color : #000000;
}
.tk.scroll-bar:focus .track {
background: #550000;
}
.tk.scroll-bar:focus .thumb {
box-shadow: 0 0 0 1px #ff0000;
}
.tk.window {
box-shadow: 1px 1px 0 #550000;
}
.tk.window:focus-within {
box-shadow: 1px 1px 0 #aa0000;
}

249
web/toolkit/App.js Normal file
View File

@ -0,0 +1,249 @@
let register = Toolkit => Toolkit.App =
// Root application container and localization manager
class App extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(options) {
super(null, Object.assign({
tabIndex: -1
}, options));
// Configure instance fields
this.components = new Set();
this.dragElement = null;
this.lastFocus = null;
this.locale = null;
this.locales = new Map();
// Configure event handlers
this.addEventListener("focusin", e=>this.onFocus(e));
this.addEventListener("keydown", e=>this.onKey (e));
this.addEventListener("keyup" , e=>this.onKey (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Child element focus gained
onFocus(e) {
// Error checking
if (e.target != document.activeElement)
return;
// Target is self
if (e.target == this.element)
return this.restoreFocus();
// Ensure the child is not contained in a MenuBar
for (let elm = e.target; elm != this.element; elm = elm.parentNode) {
if (elm.getAttribute("role") == "menubar")
return;
}
// Track the (non-menu) element as the most recent focused component
this.lastFocus = e.target;
}
// Key press, key release
onKey(e) {
if (this.dragElement == null || e.rerouted)
return;
this.dragElement.dispatchEvent(Object.assign(new Event(e.type), {
altKey : e.altKey,
ctrlKey : e.ctrlKey,
key : e.key,
rerouted: true,
shiftKey: e.shiftKey
}));
}
///////////////////////////// Public Methods //////////////////////////////
// Install a locale from URL
async addLocale(url) {
let data;
// Load the file as JSON, using UTF-8 with or without a BOM
try { data = JSON.parse(new TextDecoder().decode(
await (await fetch(url)).arrayBuffer() )); }
catch { return null; }
// Error checking
if (!data.id || !data.name)
return null;
// Flatten the object to keys
let locale = new Map();
let entries = Object.entries(data);
let stack = [];
while (entries.length != 0) {
let entry = entries.shift();
// The value is a non-array object
if (entry[1] instanceof Object && !Array.isArray(entry[1])) {
entries = entries.concat(Object.entries(entry[1])
.map(e=>[ entry[0] + "." + e[0], e[1] ]));
}
// The value is a primitive or array
else locale.set(entry[0].toLowerCase(), entry[1]);
}
this.locales.set(data.id, locale);
return data.id;
}
// Specify a localization dictionary
setLocale(id) {
if (!this.locales.has(id))
return false;
this.locale = this.locales.get(id);
for (let comp of this.components)
comp.localize();
}
///////////////////////////// Package Methods /////////////////////////////
// Begin dragging on an element
get drag() { return this.dragElement; }
set drag(event) {
// Begin dragging
if (event) {
this.dragElement = event.currentTarget;
this.dragPointer = event.pointerId;
this.dragElement.setPointerCapture(event.pointerId);
}
// End dragging
else {
if (this.dragElement)
this.dragElement.releasePointerCapture(this.dragPointer);
this.dragElement = null;
this.dragPointer = null;
}
}
// Configure components for automatic localization, or localize a message
localize(a, b) {
return a instanceof Object ? this.localizeComponents(a, b) :
this.localizeMessage(a, b);
}
// Return focus to the most recent focused element
restoreFocus() {
// Error checking
if (!this.lastFocus)
return false;
// Unable to restore focus
if (!this.isVisible(this.lastFocus))
return false;
// Transfer focus to the most recent element
this.lastFocus.focus({ preventScroll: true });
return true;
}
///////////////////////////// Private Methods /////////////////////////////
// Configure components for automatic localization
localizeComponents(comps, add) {
// Process all components
for (let comp of (Array.isArray(comps) ? comps : [comps])) {
// Error checking
if (
!(comp instanceof Toolkit.Component) ||
!(comp.localize instanceof Function)
) continue;
// Update the collection and component text
this.components[add ? "add" : "delete"](comp);
comp.localize();
}
}
// Localize a message
localizeMessage(message, substs, circle = new Set()) {
let parts = [];
// Separate the substitution keys from the literal text
for (let x = 0;;) {
// Locate the start of the next substitution key
let y = message.indexOf("{", x);
let z = y == -1 ? -1 : message.indexOf("}", y + 1);
// No substitution key or malformed substitution expression
if (z == -1) {
parts.push(message.substring(z == -1 ? x : y));
break;
}
// Append the literal text and the substitution key
parts.push(message.substring(x, y), message.substring(y + 1, z));
x = z + 1;
}
// Process all substitutions
for (let x = 1; x < parts.length; x += 2) {
let key = parts[x].toLowerCase();
let value;
// The substitution key is already in the recursion chain
if (circle.has(key)) {
parts[x] = "{\u21ba" + key.toUpperCase() + "}";
continue;
}
// Resolve the substitution key from the argument
if (substs && substs.has(key)) {
value = substs.get(key);
// Do not recurse for this substitution
if (!value[1]) {
parts[x] = value[0];
continue;
}
// Substitution text
value = value[0];
}
// Resolve the substitution from the current locale
else if (this.locale && this.locale.has(key))
value = this.locale.get(key);
// A matching substitution key was not found
else {
parts[x] = "{\u00d7" + key.toUpperCase() + "}";
continue;
}
// Perform recursive substitution
circle.add(key);
parts[x] = this.localizeMessage(value, substs, circle);
circle.delete(key);
}
return parts.join("");
}
};
export { register };

119
web/toolkit/Button.js Normal file
View File

@ -0,0 +1,119 @@
let register = Toolkit => Toolkit.Button =
// Push button
class Button extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class : "tk button",
role : "button",
tabIndex: "0"
}, options));
// Configure options
if ("disabled" in options)
this.disabled = options.disabled;
this.doNotFocus = !("doNotFocus" in options) || options.doNotFocus;
// Display text
this.content = new Toolkit.Label(app);
this.add(this.content);
// Event handlers
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
if (
!(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) &&
(e.key == " " || e.key == "Enter")
) this.activate();
}
// Pointer down
onPointerDown(e) {
// Gain focus
if (
!this.doNotFocus &&
this.isFocusable() &&
this.element != document.activeElement
) this.element.focus();
else e.preventDefault();
// Do not drag
if (
e.button != 0 ||
this.disabled ||
this.element.hasPointerCapture(e.pointerId)
) return;
// Begin dragging
this.element.setPointerCapture(e.pointerId);
this.element.classList.add("pushed");
Toolkit.handle(e);
}
// Pointer move
onPointerMove(e) {
// Do not drag
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Process dragging
this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed");
Toolkit.handle(e);
}
// Pointer up
onPointerUp(e) {
// Do not activate
if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId))
return;
// End dragging
this.element.releasePointerCapture(e.pointerId);
this.element.classList.remove("pushed");
Toolkit.handle(e);
// Activate the button if applicable
if (this.isWithin(e))
this.activate();
}
///////////////////////////// Public Methods //////////////////////////////
// Simulate a click on the button
activate() {
if (!this.disabled)
this.element.dispatchEvent(new Event("action"));
}
///////////////////////////// Package Methods /////////////////////////////
// Update localization strings
localize() {
this.localizeText(this.content);
this.localizeLabel();
this.localizeTitle();
}
}
export { register };

144
web/toolkit/Checkbox.js Normal file
View File

@ -0,0 +1,144 @@
let register = Toolkit => Toolkit.Checkbox =
// Check box
class Checkbox extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class : "tk checkbox",
role : "checkbox",
tabIndex: "0"
}, options, { style: Object.assign({
alignItems : "center",
display : "inline-grid",
gridTemplateColumns: "max-content auto"
}, options.style || {}) }));
// Configure element
this.element.setAttribute("aria-checked", "false");
this.addEventListener("keydown" , e=>this.onKeyDown (e));
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
this.addEventListener("pointermove", e=>this.onPointerMove(e));
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
// Icon area
this.box = document.createElement("div");
this.box.className = "tk box";
this.append(this.box);
// Display text
this.uiLabel = new Toolkit.Label(app);
this.add(this.uiLabel);
// Configure options
this.checked = options.checked;
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
if (
!(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) &&
(e.key == " " || e.key == "Enter")
) this.setChecked(!this.checked);
}
// Pointer down
onPointerDown(e) {
// Gain focus
if (!this.disabled)
this.element.focus();
else e.preventDefault();
// Do not drag
if (
e.button != 0 ||
this.disabled ||
this.element.hasPointerCapture(e.pointerId)
) return;
// Begin dragging
this.element.setPointerCapture(e.pointerId);
this.element.classList.add("pushed");
Toolkit.handle(e);
}
// Pointer move
onPointerMove(e) {
// Do not drag
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Process dragging
this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed");
Toolkit.handle(e);
}
// Pointer up
onPointerUp(e) {
// Do not activate
if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId))
return;
// End dragging
this.element.releasePointerCapture(e.pointerId);
this.element.classList.remove("pushed");
Toolkit.handle(e);
// Activate the check box if applicable
if (this.isWithin(e))
this.setChecked(!this.checked);
}
///////////////////////////// Public Methods //////////////////////////////
// The check box is checked
get checked() { return this.element.getAttribute("aria-checked")=="true"; }
set checked(checked) {
checked = !!checked;
if (checked == this.checked)
return;
this.element.setAttribute("aria-checked", checked);
}
// Specify the display text
setText(text, localize) {
this.uiLabel.setText(text, localize);
}
///////////////////////////// Package Methods /////////////////////////////
// Update localization strings
localize() {
this.uiLabel.localize();
this.localizeTitle();
}
///////////////////////////// Private Methods /////////////////////////////
// Specify the checked state
setChecked(checked) {
checked = !!checked;
if (checked == this.checked)
return;
this.checked = checked;
this.element.dispatchEvent(new Event("input"));
}
}
export { register };

473
web/toolkit/Component.js Normal file
View File

@ -0,0 +1,473 @@
let register = Toolkit => Toolkit.Component =
// Base class from which all toolkit components are derived
class Component {
//////////////////////////////// Constants ////////////////////////////////
// Non-attributes
static NON_ATTRIBUTES = new Set([
"checked", "disabled", "doNotFocus", "group", "hover", "max", "min",
"name", "orientation", "overflowX", "overflowY", "tag", "text",
"value", "view", "visibility", "visible"
]);
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
// Configure element
this.element = document.createElement(options.tag || "div");
this.element.component = this;
for (let entry of Object.entries(options)) {
if (
Toolkit.Component.NON_ATTRIBUTES.has(entry[0]) ||
entry[0] == "type" && options.tag != "input"
) continue;
if (entry[0] == "style" && entry[1] instanceof Object)
Object.assign(this.element.style, entry[1]);
else this.element.setAttribute(entry[0], entry[1]);
}
// Configure instance fields
this._isLocalized = false;
this.app = app;
this.display = options.style && options.style.display;
this.style = this.element.style;
this.text = null;
this.visibility = !!options.visibility;
this.visible = !("visible" in options) || options.visible;
}
///////////////////////////// Public Methods //////////////////////////////
// Add a child component
add(comp) {
// Error checking
if (
!(comp instanceof Toolkit.Component) ||
comp instanceof Toolkit.App ||
comp.app != (this.app || this)
) return false;
// No components have been added yet
if (!this.children)
this.children = [];
// The child already has a parent: remove it
if (comp.parent) {
comp.parent.children.splice(
comp.parent.children.indexOf(comp), 1);
}
// Add the component to self
this.children.push(comp);
this.append(comp.element);
comp.parent = this;
return true;
}
// Register an event listener on the element
addEventListener(type, listener) {
// No event listeners have been registered yet
if (!this.listeners)
this.listeners = new Map();
if (!this.listeners.has(type))
this.listeners.set(type, []);
// The listener has already been registered for this event
let listeners = this.listeners.get(type);
if (listeners.indexOf(listener) != -1)
return listener;
// Resize events are implemented by a ResizeObserver
if (type == "resize") {
if (!this.resizeObserver) {
this.resizeObserver = new ResizeObserver(()=>
this.element.dispatchEvent(new Event("resize")));
this.resizeObserver.observe(this.element);
}
}
// Visibility events are implemented by an IntersectionObserver
else if (type == "visibility") {
if (!this.visibilityObserver) {
this.visibilityObserver = new IntersectionObserver(
()=>this.element.dispatchEvent(Object.assign(
new Event("visibility"),
{ visible: this.isVisible() }
)),
{ root: document.body }
);
this.visibilityObserver.observe(this.element);
}
}
// Register the listener with the element
listeners.push(listener);
this.element.addEventListener(type, listener);
return listener;
}
// Component cannot be interacted with
get disabled() { return this.element.hasAttribute("disabled"); }
set disabled(disabled) { this.setDisabled(disabled); }
// Move focus into the component
focus() {
this.element.focus({ preventScroll: true });
}
// Specify whether the component is localized
get isLocalized() { return this._isLocalized; }
set isLocalized(isLocalized) {
if (isLocalized == this._isLocalized)
return;
this._isLocalized = isLocalized;
(this instanceof Toolkit.App ? this : this.app)
.localize(this, isLocalized);
}
// Determine whether an element is actually visible
isVisible(element = this.element) {
if (!document.body.contains(element))
return false;
for (; element instanceof Element; element = element.parentNode) {
let style = getComputedStyle(element);
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
}
// Produce an ordered list of registered event listeners for an event type
listEventListeners(type) {
return this.listeners && this.listeners.has(type) &&
this.listeners.get(type).list.slice() || [];
}
// Remove a child component
remove(comp) {
if (comp.parent != this || !this.children)
return false;
let index = this.children.indexOf(comp);
if (index == -1)
return false;
this.children.splice(index, 1);
comp.element.remove();
comp.parent = null;
return true;
}
// Unregister an event listener from the element
removeEventListener(type, listener) {
// Not listening to events of the specified type
if (!this.listeners || !this.listeners.has(type))
return listener;
// Listener is not registered
let listeners = this.listeners.get(type);
let index = listeners.indexOf(listener);
if (index == -1)
return listener;
// Unregister the listener
this.element.removeEventListener(listener);
listeners.splice(index, 1);
// Delete the ResizeObserver
if (
type == "resize" &&
listeners.list.length == 0 &&
this.resizeObserver
) {
this.resizeObserver.disconnect();
delete this.resizeObserver;
}
// Delete the IntersectionObserver
else if (
type == "visibility" &&
listeners.list.length == 0 &&
this.visibilityObserver
) {
this.visibilityObserver.disconnect();
delete this.visibilityObserver;
}
return listener;
}
// Specify accessible name
setLabel(text, localize) {
// Label is another component
if (
text instanceof Toolkit.Component ||
text instanceof HTMLElement
) {
this.element.setAttribute("aria-labelledby",
(text.element || text).id);
this.setString("label", null, false);
}
// Label is the given text
else {
this.element.removeAttribute("aria-labelledby");
this.setString("label", text, localize);
}
}
// Specify role description text
setRoleDescription(text, localize) {
this.setString("roleDescription", text, localize);
}
// Specify inner text
setText(text, localize) {
this.setString("text", text, localize);
}
// Specify tooltip text
setTitle(text, localize) {
this.setString("title", text, localize);
}
// Specify substitution text
substitute(key, text = null, recurse = false) {
if (text === null) {
if (this.substitutions.has(key))
this.substitutions.delete(key);
} else this.substitutions.set(key, [ text, recurse ]);
if (this.localize instanceof Function)
this.localize();
}
// Determine whether the element wants to be visible
get visible() {
let style = this.element.style;
return style.display != "none" && style.visibility != "hidden";
}
// Specify whether the element is visible
set visible(visible) {
visible = !!visible;
// Visibility is not changing
if (visible == this.visible)
return;
let comps = [ this ].concat(
Array.from(this.element.querySelectorAll("*"))
.map(c=>c.component)
).filter(c=>
c instanceof Toolkit.Component &&
c.listeners &&
c.listeners.has("visibility")
)
;
let prevs = comps.map(c=>c.isVisible());
// Allow the component to be shown
if (visible) {
if (!this.visibility) {
if (this.display)
this.element.style.display = this.display;
else this.element.style.removeProperty("display");
} else this.element.style.removeProperty("visibility");
}
// Prevent the component from being shown
else {
this.element.style.setProperty(
this.visibility ? "visibility" : "display",
this.visibility ? "hidden" : "none"
);
}
for (let x = 0; x < comps.length; x++) {
let comp = comps[x];
visible = comp.isVisible();
if (visible == prevs[x])
continue;
comp.element.dispatchEvent(Object.assign(
new Event("visibility"),
{ visible: visible }
));
}
}
///////////////////////////// Package Methods /////////////////////////////
// Add a child component to the primary client region of this component
append(element) {
this.element.append(element instanceof Toolkit.Component ?
element.element : element);
}
// Determine whether a component or element is a child of this component
contains(child) {
return this.element.contains(child instanceof Toolkit.Component ?
child.element : child);
}
// Generate a list of focusable descendant elements
getFocusable(element = this.element) {
let cache;
return Array.from(element.querySelectorAll(
"*:is(a[href],area,button,details,input,textarea,select," +
"[tabindex='0']):not([disabled])"
)).filter(e=>{
for (; e instanceof Element; e = e.parentNode) {
let style =
(cache || (cache = new Map())).get(e) ||
cache.set(e, getComputedStyle(e)).get(e)
;
if (style.display == "none" || style.visibility == "hidden")
return false;
}
return true;
});
}
// Specify the inner text of the primary client region of this component
get innerText() { return this.element.textContent; }
set innerText(text) { this.element.innerText = text; }
// Determine whether an element is focusable
isFocusable(element = this.element) {
return element.matches(
":is(a[href],area,button,details,input,textarea,select," +
"[tabindex='0'],[tabindex='-1']):not([disabled])"
);
}
// Determine whether a pointer event is within the element
isWithin(e, element = this.element) {
let bounds = element.getBoundingClientRect();
return (
e.clientX >= bounds.left && e.clientX < bounds.right &&
e.clientY >= bounds.top && e.clientY < bounds.bottom
);
}
// Common processing for localizing the accessible name
localizeLabel(element = this.element) {
// There is no label or the label is another element
if (!this.label || element.hasAttribute("aria-labelledby")) {
element.removeAttribute("aria-label");
return;
}
// Localize the label
let text = this.label;
text = !text[1] ? text[0] :
this.app.localize(text[0], this.substitutions);
element.setAttribute("aria-label", text);
}
// Common processing for localizing the accessible role description
localizeRoleDescription(element = this.element) {
// There is no role description
if (!this.roleDescription) {
element.removeAttribute("aria-roledescription");
return;
}
// Localize the role description
let text = this.roleDescription;
text = !text[1] ? text[0] :
this.app.localize(text[0], this.substitutions);
element.setAttribute("aria-roledescription", text);
}
// Common processing for localizing inner text
localizeText(element = this.element) {
// There is no title
if (!this.text) {
element.innerText = "";
return;
}
// Localize the text
let text = this.text;
text = !text[1] ? text[0] :
this.app.localize(text[0], this.substitutions);
element.innerText = text;
}
// Common processing for localizing the tooltip text
localizeTitle(element = this.element) {
// There is no title
if (!this.title) {
element.removeAttribute("title");
return;
}
// Localize the title
let text = this.title;
text = !text[1] ? text[0] :
this.app.localize(text[0], this.substitutions);
element.setAttribute("title", text);
}
// Common handler for configuring whether the component is disabled
setDisabled(disabled, element = this.element) {
element[disabled ? "setAttribute" : "removeAttribute"]
("disabled", "");
element.setAttribute("aria-disabled", disabled ? "true" : "false");
}
// Specify display text
setString(key, value, localize = true) {
// There is no method to update the display text
if (!(this.localize instanceof Function))
return;
// Working variables
let app = this instanceof Toolkit.App ? this : this.app;
// Remove the string
if (value === null) {
if (app && this[key] != null && this[key][1])
app.localize(this, false);
this[key] = null;
}
// Set or replace the string
else {
if (app && localize && (this[key] == null || !this[key][1]))
app.localize(this, true);
this[key] = [ value, localize ];
}
// Update the display text
this.localize();
}
// Retrieve the substitutions map
get substitutions() {
if (!this._substitutions)
this._substitutions = new Map();
return this._substitutions;
}
};
export { register };

86
web/toolkit/Desktop.js Normal file
View File

@ -0,0 +1,86 @@
let register = Toolkit => Toolkit.Desktop =
// Layered window manager
class Desktop extends Toolkit.Component {
//////////////////////////////// Constants ////////////////////////////////
// Comparator for ordering child windows
static CHILD_ORDER(a, b) {
return b.element.style.zIndex - a.element.style.zIndex;
}
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, Object.assign({
class: "tk desktop"
}, options, { style: Object.assign({
position: "relative",
zIndex : "0"
}, options.style || {})} ));
// Configure event listeners
this.addEventListener("resize", e=>this.onResize());
}
///////////////////////////// Event Handlers //////////////////////////////
// Element resized
onResize() {
// The element is hidden: its size is indeterminate
if (!this.isVisible())
return;
// Don't allow children to be out-of-frame
if (this.children != null) {
for (let child of this.children) {
child.left = child.left;
child.top = child.top;
}
}
}
///////////////////////////// Public Methods //////////////////////////////
// Add a child component
add(comp) {
super.add(comp);
this.bringToFront(comp);
}
// Retrieve the foremost visible window
getActiveWindow() {
if (this.children != null) {
for (let child of this.children) {
if (child.isVisible())
return child;
}
}
return null;
}
///////////////////////////// Package Methods /////////////////////////////
// Reorder children so that a particular child is in front
bringToFront(child) {
this.children.splice(this.children.indexOf(child), 1);
this.children.push(child);
let z = 1 - this.children.length;
for (let child of this.children)
child.element.style.zIndex = z++;
}
}
export { register };

154
web/toolkit/DropDown.js Normal file
View File

@ -0,0 +1,154 @@
let register = Toolkit => Toolkit.DropDown =
class DropDown extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, Object.assign({
class: "tk drop-down",
tag : "select"
}, options));
// Configure instance fields
this.items = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Add an item
add(text, localize, value) {
// Record the item data
this.items.push([ text, localize, value ]);
// Add an <option> element
let option = document.createElement("option");
this.element.append(option);
option.innerText = !localize ? text :
this.app.localize(text, this.substitutions);
}
// Remove all items
clear() {
this.items.splice(0);
this.element.replaceChildren();
this.element.selectedIndex = -1;
}
// Retrieve an item
get(index) {
// Error checking
if (index < 0 || index >= this.items.length)
return null;
// Return the item as an item with properties
let item = this.items[item];
return {
localize: item[1],
text : item[0],
value : item[2]
};
}
// Number of items in the list
get length() { return this.items.length; }
set length(v) { }
// Remove an item
remove(index) {
// Error checking
if (index < 0 || index >= this.length)
return;
// Determine what selectedIndex will be after the operation
let newIndex = index;
if (
newIndex <= this.selectedIndex ||
newIndex == this.length - 1
) newIndex--;
if (
newIndex == -1 &&
this.length != 0
) newIndex = 0;
// Remove the item
this.items.splice(index, 1);
this.element.options[index].remove();
this.element.selectedIndex = newIndex;
}
// Index of the currently selected item
get selectedIndex() { return this.element.selectedIndex; }
set selectedIndex(index) {
if (index < -1 || index >= this.items.length)
return this.element.selectedIndex;
return this.element.selectedIndex = index;
}
// Update an item
set(index, text, localize) {
// Error checking
if (index < 0 || index >= this.items.length)
return;
// Replace the item data
this.items[index] = [ text, localize ];
// Configure the <option> element
this.element.options[index].innerText = !localize ? text :
this.app.localize(text, this.substitutions);
}
// Update the selectedIndex property, firing an event
setSelectedIndex(index) {
// Error checking
if (index < -1 || index >= this.items.length)
return this.selectedIndex;
// Update the element and fire an event
this.element.selectedIndex = index;
this.element.dispatchEvent(new Event("input"));
return index;
}
// Currently selected value
get value() {
return this.selectedIndex == -1 ? null :
this.items[this.selectedIndex][2];
}
set value(value) {
let index = this.items.findIndex(i=>i[2] == value);
if (index != -1)
this.selectedIndex = index;
}
///////////////////////////// Package Methods /////////////////////////////
// Update localization strings
localize() {
// Label and title
this.localizeLabel();
this.localizeTitle();
// Items
for (let x = 0; x < this.items.length; x++) {
let item = this.items[x];
this.element.options[x].innerText = !item[1] ? item[0] :
this.app.localize(item[0], this.substitutions);
}
}
}
export { register };

46
web/toolkit/Label.js Normal file
View File

@ -0,0 +1,46 @@
let register = Toolkit => Toolkit.Label =
// Presentational text
class Label extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}, autoId = false) {
super(app, Object.assign({
class: "tk label"
}, options, { style: Object.assign({
cursor : "default",
userSelect: "none",
whiteSpace: "nowrap"
}, options.style || {}) }));
// Configure instance fields
if (autoId)
this.id = Toolkit.id();
}
///////////////////////////// Public Methods //////////////////////////////
// Specify the display text
setText(text, localize) {
this.setString("text", text, localize);
}
///////////////////////////// Package Methods /////////////////////////////
// Update localization strings
localize() {
if (this.text != null) {
let text = this.text;
this.element.innerText = !text[1] ? text[0] :
this.app.localize(text[0], this.substitutions);
}
}
}
export { register };

92
web/toolkit/Menu.js Normal file
View File

@ -0,0 +1,92 @@
let register = Toolkit => Toolkit.Menu =
// Pop-up menu container
class Menu extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, Object.assign({
class : "tk menu",
id : Toolkit.id(),
role : "menu",
visibility: true,
visible : false
}, options, { style: Object.assign({
display : "inline-flex",
flexDirection: "column",
position : "absolute",
zIndex : "1"
}, options.style || {}) }));
// Configure instance fields
this.parent = null;
// Configure event handlers
this.addEventListener("focusout", e=>this.onBlur(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Child blur
onBlur(e) {
if (this.parent instanceof Toolkit.MenuItem)
this.parent.onBlur(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Add a child menu item
add(item) {
if (!(item instanceof Toolkit.MenuItem) || !super.add(item))
return false;
this.detectIcons();
return true;
}
// Add a sepearator
addSeparator() {
let item = new Toolkit.Component(this.app, { role: "separator" });
super.add(item);
return item;
}
// Remove the menu from its parent menu item or remove a child menu item
remove() {
// Remove child
if (arguments.length != 0) {
if (!super.remove.apply(this, arguments))
return false;
this.detectIcons();
return true;
}
// Remove from parent
this.parent = null;
this.element.remove();
this.element.removeAttribute("aria-labelledby");
this.setVisible(false);
return null;
}
///////////////////////////// Package Methods /////////////////////////////
// Show or hide the icons column
detectIcons() {
this.element.classList[
this.children && this.children.find(i=>
i.visible && i.type == "checkbox") ?
"add" : "remove"
]("icons");
}
};
export { register };

106
web/toolkit/MenuBar.js Normal file
View File

@ -0,0 +1,106 @@
let register = Toolkit => Toolkit.MenuBar =
// Application menu bar
class MenuBar extends Toolkit.Menu {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, Object.assign({
class : "tk menu-bar",
role : "menubar",
visibility: false,
visible : true
}, options, { style: Object.assign({
display : "flex",
flexDirection: "row",
minWidth : "0",
position : "inline"
}, options.style || {}) }));
// Configure event handlers
this.addEventListener("focusout", e=>this.onBlur (e));
this.addEventListener("focusin" , e=>this.onFocus(e));
}
///////////////////////////// Event Handlers //////////////////////////////
// Child blur
onBlur(e) {
if (
this.contains(e.relatedTarget) ||
!this.children || this.children.length == 0
) return;
this.children.forEach(i=>i.expanded = false);
this.children[0].element.setAttribute("tabindex", "0");
}
// Child focus
onFocus(e) {
if (!this.children || this.children.length == 0)
return;
this.children[0].element.setAttribute("tabindex", "-1");
}
///////////////////////////// Public Methods //////////////////////////////
// Add a menu item
add(item) {
super.add(item);
if (item.menu)
this.element.append(item.menu.element);
this.children[0].element.setAttribute("tabindex", "0");
}
// Move focus into the component
focus() {
if (this.children.length != 0)
this.children[0].focus();
}
// Remove a menu item
remove(item) {
// Remove the menu item
if (item.parent == this && item.menu)
item.menu.remove();
super.remove(item);
// Configure focusability
if (this.children && !this.contains(document.activeElement)) {
for (let x = 0; x < this.children.length; x++) {
this.children[x].element
.setAttribute("tabindex", x == 0 ? "0" : "-1");
}
}
}
///////////////////////////// Package Methods /////////////////////////////
// Return focus to the application
blur() {
if (this.children) {
for (let item of this.children) {
item.expanded = false;
item.element.blur();
}
}
if (!this.parent || !this.parent.restoreFocus())
this.focus();
}
// Update localization strings
localize() {
this.localizeLabel(this.element);
}
}
export { register };

455
web/toolkit/MenuItem.js Normal file
View File

@ -0,0 +1,455 @@
let register = Toolkit => Toolkit.MenuItem =
// Selection within a MenuBar or Menu
class MenuItem extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class : "tk menu-item",
id : Toolkit.id(),
role : "menuitem",
tabIndex: "-1"
}, options));
// Configure instance fields
this._expanded = false;
this._menu = null;
// Element
this.content = document.createElement("div");
this.element.append(this.content);
this.disabled = options.disabled;
// Icon column
this.icon = document.createElement("div");
this.icon.className = "icon";
this.content.append(this.icon);
// Label column
this.label = document.createElement("div");
this.label.className = "label";
this.content.append(this.label);
// Control type
switch (options.type) {
case "checkbox":
this.type = "checkbox";
this.checked = !!options.checked;
break;
default:
this.type = "normal";
}
// Event handlers
this.addEventListener("focusout" , 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));
}
///////////////////////////// Event Handlers //////////////////////////////
// Focus lost
onBlur(e) {
if (this.menu && !this.contains(e.relatedTarget))
this.expanded = false;
}
// Key press
onKeyDown(e) {
// Do not process the event
if (e.altKey || e.ctrlKey || e.shiftKey)
return;
// Working variables
let isBar = this.parent && this.parent instanceof Toolkit.MenuBar;
let next = isBar ? "ArrowRight": "ArrowDown";
let prev = isBar ? "ArrowLeft" : "ArrowUp";
let siblings = this.parent && this.parent.children ?
this.parent.children
.filter(i=>i instanceof Toolkit.MenuItem && i.visible) :
[ this ];
let index = siblings.indexOf(this);
let handled = false;
// Process by relative key code
switch (e.key) {
// Select the next sibling
case next:
index = index == -1 ? 0 :
(index + 1) % siblings.length;
if (index < siblings.length) {
let sibling = siblings[index];
if (isBar && sibling.menu && this.expanded)
sibling.expanded = true;
sibling.focus();
}
handled = true;
break;
// Select the previous sibling
case prev:
index = index == -1 ? 0 :
(index + siblings.length - 1) % siblings.length;
if (index < siblings.length) {
let sibling = siblings[index];
if (isBar && sibling.menu && this.expanded)
sibling.expanded = true;
sibling.focus();
}
handled = true;
break;
}
// Process by absolute key code
if (!handled) switch (e.key) {
// Activate the menu item with handling for checks and radios
case " ":
this.activate(false);
break;
// Activate the menu item if in a MenuBar
case "ArrowDown":
if (isBar && this.menu)
this.activate();
break;
// Cycle through menu items in a MenuBar
case "ArrowLeft":
case "ArrowRight": {
let root = this.getRoot();
if (!(root instanceof Toolkit.MenuBar))
break;
let top = root.children.find(i=>i.expanded);
if (top)
return top.onKeyDown(e);
break;
}
// Select the last sibling
case "End":
if (siblings.length != 0)
siblings[siblings.length - 1].focus();
break;
// Activate the menu item
case "Enter":
this.activate();
break;
// Deactivate the menu and return to the parent menu item
case "Escape": {
if (this.expanded)
this.expanded = false;
else if (this.parent &&
this.parent.parent instanceof Toolkit.MenuItem) {
this.parent.parent.expanded = false;
this.parent.parent.focus();
} else if (this.parent instanceof Toolkit.MenuBar)
this.parent.blur();
break;
}
// Select the first sibling
case "Home":
if (siblings.length != 0)
siblings[0].focus();
break;
// Select the next menu item that begins with the typed character
default: {
if (e.key.length != 1)
return;
let key = e.key.toLowerCase();
for (let x = 0; x < siblings.length; x++) {
let sibling = siblings[(index + x + 1) % siblings.length];
if (
(sibling.content.textContent || " ")[0]
.toLowerCase() == key
) {
if (sibling.menu)
sibling.expanded = true;
if (sibling.menu && sibling.menu.children &&
sibling.menu.children[0])
sibling.menu.children[0].focus();
else sibling.focus();
handled = true;
break;
}
}
if (!handled)
return;
}
}
Toolkit.handle(e);
}
// Pointer down
onPointerDown(e) {
this.focus();
// Do not process the event
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
return;
if (this.disabled)
return Toolkit.handle(e);
// Does not contain a menu
if (!this.menu) {
this.element.setPointerCapture(e.pointerId);
this.element.classList.add("pushed");
}
// Does contain a menu
else this.expanded = !this.expanded;
Toolkit.handle(e);
}
// Pointer move
onPointerMove(e) {
// Do not process the event
if (this.disabled)
return Toolkit.handle(e);
// Not dragging within element
if (!this.element.hasPointerCapture(e.pointerId)) {
let other = this.parent&&this.parent.children.find(i=>i.expanded);
if (other && other != this) {
this.expanded = true;
this.focus();
}
}
// Dragging within element
else this.element.classList
[this.isWithin(e) ? "add" : "remove"]("pushed");
Toolkit.handle(e);
}
// Pointer up
onPointerUp(e) {
// Do not process the event
if (e.button != 0)
return;
if (this.disabled)
return Toolkit.handle(e);
// Stop dragging
if (this.element.hasPointerCapture(e.pointerId)) {
this.element.releasePointerCapture(e.pointerId);
this.element.classList.remove("pushed");
}
// Activate the menu item
if (!this.menu && this.isWithin(e))
this.activate();
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Specify whether or not the checkbox is checked
get checked() { return this._checked; }
set checked(checked) {
checked = !!checked;
if (checked === this._checked || this._type != "checkbox")
return;
this._checked = checked;
this.element.setAttribute("aria-checked", checked ? "true" : "false");
}
// Determine whether a component or element is a child of this component
contains(child) {
return super.contains(child) || this.menu && this.menu.contains(child);
}
// Enable or disable the control
get disabled() { return this._disabled; }
set disabled(disabled) {
disabled = !!disabled;
// Error checking
if (disabled === this._disabled)
return;
// Enable or disable the control
this._disabled = disabled;
this.element[disabled ? "setAttribute" : "removeAttribute"]
("aria-disabled", "true");
if (disabled)
this.expanded = false;
}
// Expand or collapse the menu
get expanded() { return this._expanded; }
set expanded(expanded) {
expanded = !!expanded;
// Error checking
if (this._expanded == expanded || !this.menu)
return;
// Configure instance fields
this._expanded = expanded;
// Expand the menu
if (expanded) {
let bounds = this.element.getBoundingClientRect();
Object.assign(this.menu.element.style, {
left: bounds.left + "px",
top : bounds.bottom + "px"
});
this.menu.visible = true;
this.element.setAttribute("aria-expanded", "true");
}
// Collapse the menu and all child sub-menus
else {
this.menu.visible = false;
this.element.setAttribute("aria-expanded", "false");
if (this.children)
this.children.forEach(i=>i.expanded = false);
}
}
// Specify a new menu
get menu() { return this._menu; }
set menu(menu) {
// Error checking
if (menu == this._menu || menu && menu.parent && menu.parent != this)
return;
// Remove the current menu
if (this._menu) {
this.expanded = false;
this._menu.remove();
}
// Configure as regular menu item
if (!menu) {
this.element.removeAttribute("aria-expanded");
this.element.removeAttribute("aria-haspopup");
return;
}
// Associate the menu with the item
this._menu = menu;
menu.parent = this;
if (this.parent)
this.element.after(menu.element);
menu.element.setAttribute("aria-labelledby", this.element.id);
}
// Specify display text
setText(text, localize) {
this.setString("text", text, localize);
}
// Specify the menu item type
get type() { return this._type; }
set type(type) {
switch (type) {
case "checkbox":
if (this._type == "checkbox")
break;
this._type = "checkbox";
this.checked = null;
this.element.classList.add("checkbox");
break;
default:
if (this._type == "normal")
break;
this._type = "normal";
this._checked = false;
this.element.classList.remove("checkbox");
this.element.removeAttribute("aria-checked");
}
}
// Specify whether the element is visible
get visible() { return super.visible; }
set visible(visible) {
super.visible = visible = !!visible;
if (this.parent instanceof Toolkit.Menu)
this.parent.detectIcons();
}
///////////////////////////// Package Methods /////////////////////////////
// Update localization strings
localize() {
if (this.text != null) {
let text = this.text;
this.label.innerText = !text[1] ? text[0] :
this.app.localize(text[0], this.substitutions);
} else this.label.innerText = "";
}
///////////////////////////// Private Methods /////////////////////////////
// Activate the menu item
activate(blur = true) {
// Error checking
if (this.disabled)
return;
// Expand the sub-menu
if (this.menu) {
this.expanded = true;
if (this.menu.children && this.menu.children[0])
this.menu.children[0].focus();
else this.focus();
}
// Activate the menu item
else {
this.element.dispatchEvent(Toolkit.event("action"));
if (this.type == "normal" || blur) {
let root = this.getRoot();
if (root instanceof Toolkit.MenuBar)
root.blur();
}
}
}
// Locate the root Menu component
getRoot() {
for (let comp = this.parent; comp != this.app; comp = comp.parent) {
if (
comp instanceof Toolkit.Menu &&
!(comp.parent instanceof Toolkit.MenuItem)
) return comp;
}
return null;
}
};
export { register };

113
web/toolkit/Radio.js Normal file
View File

@ -0,0 +1,113 @@
let register = Toolkit => Toolkit.Radio =
// Radio button group manager
class Radio extends Toolkit.Checkbox {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options) {
super(app, options = Object.assign({
class: "tk radio",
role : "radio"
}, options || {}));
// Configure instance fields
this._group = null;
// Configure options
if ("group" in options)
this.group = options.group;
if ("checked" in options) {
this.checked = false;
this.checked = options.checked;
}
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
return;
// Processing by key
let item = null;
switch (e.key) {
// Activate the radio button
case " ":
case "Enter":
this.checked = true;
break;
// Focus the next button in the group
case "ArrowDown":
case "ArrowRight":
if (this.group == null)
return;
item = this.group.next(this);
break;
// Focus the previous button in the group
case "ArrowLeft":
case "ArrowUp":
if (this.group == null)
return;
item = this.group.previous(this);
break;
default: return;
}
// Select and focus another item in the group
if (item != null && item != this) {
this.group.active = item;
item.focus();
item.element.dispatchEvent(new Event("input"));
}
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// The check box is checked
get checked() { return super.checked; }
set checked(checked) {
super.checked = checked;
if (this.group != null && this != this.group.active && checked)
this.group.active = this;
}
// Managing radio button group
get group() { return this._group; }
set group(group) {
if (group == this.group)
return;
if (group)
group.add(this);
this._group = group ? group : null;
}
///////////////////////////// Private Methods /////////////////////////////
// Specify the checked state
setChecked(checked) {
if (!checked || this.checked)
return;
this.checked = true;
this.element.dispatchEvent(new Event("input"));
}
}
export { register };

109
web/toolkit/RadioGroup.js Normal file
View File

@ -0,0 +1,109 @@
let register = Toolkit => Toolkit.RadioGroup =
// Radio button group manager
class RadioGroup {
///////////////////////// Initialization Methods //////////////////////////
constructor() {
this._active = null;
this.items = [];
}
///////////////////////////// Public Methods //////////////////////////////
// The current active radio button
get active() { return this._active; }
set active(item) {
// Error checking
if (
item == this.active ||
item != null && this.items.indexOf(item) == -1
) return;
// De-select the current active item
if (this.active != null) {
this.active.checked = false;
this.active.element.setAttribute("tabindex", "-1");
}
// Select the new active item
this._active = item ? item : null;
if (item == null)
return;
if (!item.checked)
item.checked = true;
item.element.setAttribute("tabindex", "0");
}
// Add a radio button item
add(item) {
// Error checking
if (
item == null ||
this.items.indexOf(item) != -1
) return;
// Remove the item from its current group
if (item.group != null)
item.group.remove(item);
// Add the item to this group
this.items.push(item);
item.group = this;
item.element.setAttribute("tabindex",
this.items.length == 0 ? "0" : "-1");
return item;
}
// Check whether an element is contained in the radio group
contains(element) {
if (element instanceof Toolkit.Component)
element = element.element;
return !!this.items.find(i=>i.element.contains(element));
}
// Remove a radio button item
remove(item) {
let index = this.items.indexOf(item);
// The item is not in the group
if (index == -1)
return null;
// Remove the item from the group
this.items.splice(index, 1);
item.element.setAttribute("tabindex", "0");
if (this.active == item) {
this.active = null;
if (this.items.length != 0)
this.items[0].element.setAttribute("tabindex", "0");
}
return item;
}
///////////////////////////// Package Methods /////////////////////////////
// Determine the next radio button in the group
next(item) {
let index = this.items.indexOf(item);
return index == -1 ? null :
this.items[(index + 1) % this.items.length];
}
// Determine the previous radio button in the group
previous(item) {
let index = this.items.indexOf(item);
return index == -1 ? null :
this.items[(index + this.items.length - 1) % this.items.length];
}
}
export { register };

380
web/toolkit/ScrollBar.js Normal file
View File

@ -0,0 +1,380 @@
let register = Toolkit => Toolkit.ScrollBar =
// Scrolling control
class ScrollBar extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class : "tk scroll-bar",
role : "scrollbar",
tabIndex: "0"
}, options, { style: Object.assign({
display : "inline-grid",
overflow: "hidden"
}, options.style || {}) }));
// Configure instance fields
this._blockIncrement = 50;
this._controls = null;
this._unitIncrement = 1;
// Unit decrement button
this.unitLess = new Toolkit.Button(app,
{ class: "unit-less", role: "", tabIndex: "" });
this.unitLess.addEventListener("action",
e=>this.value -= this.unitIncrement);
this.add(this.unitLess);
// Component
this.addEventListener("keydown" , e=>this.onKeyDown(e));
this.addEventListener("pointerdown", e=>e.preventDefault());
// Track
this.track = new Toolkit.Component(app, {
class: "track",
style: {
display : "grid",
overflow: "hidden"
}
});
this.track.addEventListener("resize", e=>this.onResize());
this.add(this.track);
// Unit increment button
this.unitMore = new Toolkit.Button(app,
{ class: "unit-more", role: "", tabIndex: "" });
this.unitMore.addEventListener("action",
e=>this.value += this.unitIncrement);
this.add(this.unitMore);
// Block decrement track
this.blockLess = new Toolkit.Button(app,
{ class: "block-less", role: "", tabIndex: "" });
this.blockLess.addEventListener("action",
e=>this.value -= this.blockIncrement);
this.track.add(this.blockLess);
// Scroll box
this.thumb = document.createElement("div");
this.thumb.className = "thumb";
this.thumb.addEventListener("pointerdown", e=>this.onThumbDown(e));
this.thumb.addEventListener("pointermove", e=>this.onThumbMove(e));
this.thumb.addEventListener("pointerUp" , e=>this.onThumbUp (e));
this.track.append(this.thumb);
// Block increment track
this.blockMore = new Toolkit.Button(app,
{ class: "block-more", role: "", tabIndex: "" });
this.blockMore.addEventListener("action",
e=>this.value += this.blockIncrement);
this.track.add(this.blockMore);
// Configure options
this.blockIncrement = !("blockIncrement" in options) ?
this._blockIncrement : options.blockIncrement;
this.orientation = !("orientation" in options) ?
"horizontal" : options.orientation;
this.unitIncrement = !("unitIncrement" in options) ?
this._unitIncrement : options.unitIncrement;
// Configure min, max and value
let min = "min" in options ? options.min : 0;
let max = Math.max(min, "max" in options ? options.max : 100);
let value = Math.max(min, Math.min(max,
"value" in options ? options.value : 0));
this.element.setAttribute("aria-valuemax", max);
this.element.setAttribute("aria-valuemin", min);
this.element.setAttribute("aria-valuenow", value);
// Display the element
this.track.element.dispatchEvent(new Event("resize"));
this.onResize();
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Take no action
if (e.altKey || e.ctrlKey || e.shiftKey)
return;
// Processing by key
switch (e.key) {
case "ArrowDown":
if (this.orientation != "vertical")
return;
this.value += this.unitIncrement;
break;
case "ArrowLeft":
if (this.orientation != "horizontal")
return;
this.value -= this.unitIncrement;
break;
case "ArrowRight":
if (this.orientation != "horizontal")
return;
this.value += this.unitIncrement;
break;
case "ArrowUp":
if (this.orientation != "vertical")
return;
this.value -= this.unitIncrement;
break;
case "PageDown":
this.value += this.blockIncrement;
break;
case "PageUp":
this.value -= this.blockIncrement;
break;
default: return;
}
Toolkit.handle(e);
}
// Track resized
onResize() {
let metrics = this.metrics();
let add = metrics.horz ? "width" : "height";
let remove = metrics.horz ? "height" : "width" ;
// Resize the widget elements
this.blockLess.style.removeProperty(remove);
this.blockLess.style.setProperty (add, metrics.pos + "px");
this.thumb .style.removeProperty(remove);
this.thumb .style.setProperty (add, metrics.thumb + "px");
// Indicate whether the entire view is visible
this.element.classList
[metrics.unneeded ? "add" : "remove"]("unneeded");
}
// Thumb pointer down
onThumbDown(e) {
// Prevent the user agent from focusing the element
e.preventDefault();
// Error checking
if (
e.button != 0 ||
this.disabled ||
this.unneeded ||
e.target.hasPointerCapture(e.pointerId)
)
return;
// Begin dragging
this.drag = this.metrics();
this.drag.start = this.drag.horz ? e.screenX : e.screenY;
e.target.setPointerCapture(e.pointerId);
Toolkit.handle(e);
}
// Thumb pointer move
onThumbMove(e) {
// Error checking
if (!e.target.hasPointerCapture(e.pointerId))
return;
// Working variables
let add = this.drag.horz ? "width" : "height";
let remove = this.drag.horz ? "height" : "width" ;
let delta = (this.drag.horz?e.screenX:e.screenY) - this.drag.start;
let max = this.drag.track - this.drag.thumb;
let pos = Math.max(0, Math.min(max, this.drag.pos + delta));
let value = Math.round(this.min + (this.max - this.min) * pos / (this.drag.track || 1));
let scroll = value != this.value;
// Drag the thumb
this.blockLess.style.removeProperty(remove);
this.blockLess.style.setProperty (add, pos + "px");
this.element.setAttribute("aria-valuenow", value);
Toolkit.handle(e);
// Raise a scroll event
if (scroll)
this.event();
}
// Thumb pointer up
onThumbUp(e) {
// Error checking
if (e.button != 0 || !e.target.hasPointerCapture(e))
return;
// Stop dragging
this.drag = null;
e.target.releasePointerCapture(e.pointerId);
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Page adjustment amount
get blockIncrement() { return this._blockIncrement; }
set blockIncrement(amount) {
amount = Math.max(1, amount);
if (amount == this.blockIncrement)
return;
this._blockIncrement = amount;
this.onResize();
}
// Controlling target
get controls() { return this._controls; }
set controls(target) {
if (target) {
this.element.setAttribute("aria-controls",
(target.element || target).id);
} else this.element.removeAttribute("aria-controls");
this._controls = target;
}
// Component cannot be interacted with
get disabled() { return super.disabled; }
set disabled(disabled) {
super .disabled = disabled;
this.unitLess .disabled = disabled;
this.blockLess.disabled = disabled;
this.blockMore.disabled = disabled;
this.unitMore .disabled = disabled;
}
// Maximum value
get max() {
return this.blockIncrement +
parseInt(this.element.getAttribute("aria-valuemax"));
}
set max(max) {
if (max == this.max)
return;
if (max < this.min)
this.element.setAttribute("aria-valuemin", max);
if (max < this.value)
this.element.setAttribute("aria-valuenow", max);
this.element.setAttribute("aria-valuemax", max - this.blockIncrement);
this.onResize();
}
// Minimum value
get min() { return parseInt(this.element.getAttribute("aria-valuemin")); }
set min(min) {
if (min == this.min)
return;
if (min > this.max)
this.element.setAttribute("aria-valuemax",min-this.blockIncrement);
if (min > this.value)
this.element.setAttribute("aria-valuenow", min);
this.element.setAttribute("aria-valuemin", min);
this.onResize();
}
// Layout direction
get orientation() { return this.element.getAttribute("aria-orientation"); }
set orientation(orientation) {
// Orientation is not changing
if (orientation == this.orientation)
return;
// Select which CSS properties to modify
let add, remove;
switch (orientation) {
case "horizontal":
add = "grid-template-columns";
remove = "grid-template-rows";
break;
case "vertical":
add = "grid-template-rows";
remove = "grid-template-columns";
break;
default: return; // Invalid orientation
}
// Configure the element
this.element.style.removeProperty(remove);
this.element.style.setProperty(add, "max-content auto max-content");
this.element.setAttribute("aria-orientation", orientation);
this.track .style.removeProperty(remove);
this.track .style.setProperty(add, "max-content max-content auto");
}
// Line adjustment amount
get unitIncrement() { return this._unitIncrement; }
set unitIncrement(amount) {
amount = Math.max(1, amount);
if (amount == this.unitIncrement)
return;
this._unitIncrement = amount;
this.onResize();
}
// The scroll bar is not needed
get unneeded() { return this.element.classList.contains("unneeded"); }
set unneeded(x) { }
// Current value
get value() {return parseInt(this.element.getAttribute("aria-valuenow"));}
set value(value) {
value = Math.min(this.max - this.blockIncrement,
Math.max(this.min, value));
if (value == this.value)
return;
this.element.setAttribute("aria-valuenow", value);
this.onResize();
this.event();
}
///////////////////////////// Private Methods /////////////////////////////
// Raise a scroll event
event() {
this.element.dispatchEvent(
Object.assign(new Event("scroll"), { scroll: this.value }));
}
// Compute pixel dimensions of inner elements
metrics() {
let horz = this.orientation == "horizontal";
let track = this.track.element.getBoundingClientRect()
[horz ? "width" : "height"];
let block = this.blockIncrement;
let range = this.max - this.min || 1;
let thumb = block >= range ? track :
Math.min(track, Math.max(4, Math.round(block * track / range)));
let pos = Math.round((this.value - this.min) * track / range);
return {
block : block,
horz : horz,
pos : pos,
range : range,
thumb : thumb,
track : track,
unneeded: block >= range
};
}
}
export { register };

317
web/toolkit/ScrollPane.js Normal file
View File

@ -0,0 +1,317 @@
let register = Toolkit => Toolkit.ScrollPane =
// Scrolling container for larger internal elements
class ScrollPane extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class: "tk scroll-pane"
}, options, { style: Object.assign({
display : "inline-grid",
gridAutoRows: "auto max-content",
overflow : "hidden",
position : "relative"
}, options.style || {}) }));
// Configure options
this._overflowX = "auto";
this._overflowY = "auto";
if ("overflowX" in options)
this.overflowX = options.overflowX;
if ("overflowY" in options)
this.overflowY = options.overflowY;
// Component
this.addEventListener("wheel", e=>this.onWheel(e));
// Viewport
this.viewport = new Toolkit.Component(app, {
class: "viewport",
style: {
overflow: "hidden"
}
});
this.viewport.element.id = Toolkit.id();
this.viewport.addEventListener("keydown", e=>this.onKeyDown (e));
this.viewport.addEventListener("resize" , e=>this.onResize ( ));
this.viewport.addEventListener("scroll" , e=>this.onInnerScroll( ));
this.viewport.addEventListener("visibility",
e=>{ if (e.visible) this.onResize(); });
this.add(this.viewport);
// View resize manager
this.viewResizer = new ResizeObserver(()=>this.onResize());
// Vertical scroll bar
this.vscroll = new Toolkit.ScrollBar(app, {
orientation: "vertical",
visibility : true
});
this.vscroll.controls = this.viewport;
this.vscroll.addEventListener("scroll",
e=>this.onOuterScroll(e, true));
this.add(this.vscroll);
// Horizontal scroll bar
this.hscroll = new Toolkit.ScrollBar(app, {
orientation: "horizontal",
visibility : true
});
this.hscroll.controls = this.viewport;
this.hscroll.addEventListener("scroll",
e=>this.onOuterScroll(e, false));
this.add(this.hscroll);
// Corner mask (for when both scroll bars are visible)
this.corner = new Toolkit.Component(app, { class: "corner" });
this.add(this.corner);
// Configure view
this.view = options.view || null;
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
return;
// Processing by key
switch(e.key) {
case "ArrowDown":
this.viewport.element.scrollTop +=
this.vscroll.unitIncrement;
break;
case "ArrowLeft":
this.viewport.element.scrollLeft -=
this.hscroll.unitIncrement;
break;
case "ArrowRight":
this.viewport.element.scrollLeft +=
this.hscroll.unitIncrement;
break;
case "ArrowUp":
this.viewport.element.scrollTop -=
this.vscroll.unitIncrement;
break;
case "PageDown":
this.viewport.element.scrollTop +=
this.vscroll.blockIncrement;
break;
case "PageUp":
this.viewport.element.scrollTop -=
this.vscroll.blockIncrement;
break;
default: return;
}
Toolkit.handle(e);
}
// Component resized
onResize() {
// Error checking
if (!this.viewport)
return;
// Working variables
let viewport = this.viewport.element.getBoundingClientRect();
let scrollHeight =
this.hscroll.element.getBoundingClientRect().height;
let scrollWidth =
this.vscroll.element.getBoundingClientRect().width;
let viewHeight = this.viewHeight;
let viewWidth = this.viewWidth;
let fullHeight =
viewport.height + (this.hscroll.visible ? scrollHeight : 0);
let fullWidth =
viewport.width + (this.vscroll.visible ? scrollWidth : 0);
// Configure scroll bars
this.hscroll.max = viewWidth;
this.hscroll.blockIncrement = viewport.width;
this.hscroll.value = this.viewport.element.scrollLeft;
this.vscroll.max = viewHeight;
this.vscroll.blockIncrement = viewport.height;
this.vscroll.value = this.viewport.element.scrollTop;
// Determine whether the vertical scroll bar is visible
let vert = false;
if (
this.overflowY == "scroll" ||
this.overflowY == "auto" &&
viewHeight > fullHeight &&
scrollWidth <= viewWidth
) {
fullWidth -= scrollWidth;
vert = true;
}
// Determine whether the horizontal scroll bar is visible
let horz = false;
if (
this.overflowX == "scroll" ||
this.overflowX == "auto" &&
viewWidth > fullWidth &&
scrollHeight <= viewHeight
) {
fullHeight -= scrollHeight;
horz = true;
}
// The horizontal scroll bar necessitates the vertical scroll bar
vert = vert ||
this.overflowY == "auto" &&
viewHeight > fullHeight &&
scrollWidth < viewWidth
;
// Configure scroll bar visibility
this.setScrollBars(horz, vert);
}
// View scrolled
onInnerScroll() {
this.hscroll.value = this.viewport.element.scrollLeft;
this.vscroll.value = this.viewport.element.scrollTop;
}
// Scroll bar scrolled
onOuterScroll(e, vertical) {
this.viewport.element[vertical?"scrollTop":"scrollLeft"] = e.scroll;
}
// Mouse wheel scrolled
onWheel(e) {
let delta = e.deltaY;
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey || delta == 0)
return;
// Scrolling by pixel
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
let max = this.vscroll.unitIncrement * 3;
delta = Math.min(max, Math.max(-max, delta));
}
// Scrolling by line
else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) {
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
this.vscroll.unitIncrement;
}
// Scrolling by page
else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) {
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
this.vscroll.blockIncrement;
}
this.viewport.element.scrollTop += delta;
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Horizontal scroll bar policy
get overflowX() { return this._overflowX; }
set overflowX(policy) {
switch (policy) {
case "auto":
case "hidden":
case "scroll":
this._overflowX = policy;
this.onResize();
}
}
// Vertical scroll bar policy
get overflowY() { return this._overflowY; }
set overflowY(policy) {
switch (policy) {
case "auto":
case "hidden":
case "scroll":
this._overflowY = policy;
this.onResize();
}
}
// Horizontal scrolling position
get scrollLeft() { return this.viewport.element.scrollLeft; }
set scrollLeft(left) { this.viewport.element.scrollLeft = left; }
// Vertical scrolling position
get scrollTop() { return this.viewport.element.scrollTop; }
set scrollTop(top) { this.viewport.element.scrollTop = top; }
// Represented scrollable region
get view() {
let ret = this.viewport.element.querySelector("*");
return ret && ret.component || ret || null;
}
set view(view) {
view = view instanceof Toolkit.Component ? view.element : view;
if (view == null) {
view = this.viewport.element.querySelector("*");
if (view)
this.viewResizer.unobserve(view);
this.viewport.element.replaceChildren();
} else {
this.viewport.element.replaceChildren(view);
this.viewResizer.observe(view);
}
}
// Height of the view element
set viewHeight(x) { }
get viewHeight() {
let view = this.view && this.view.element || this.view;
return Math.min(
this.viewport.element.scrollHeight,
view ? view.clientHeight : 0
);
}
// width of the view element
set viewWidth(x) { }
get viewWidth() {
let view = this.view && this.view.element || this.view;
return Math.min(
this.viewport.element.scrollWidth,
view ? view.clientWidth : 0
);
}
///////////////////////////// Private Methods /////////////////////////////
// Configure scroll bar visibility
setScrollBars(horz, vert) {
this.element.style.gridTemplateColumns =
"auto" + (vert ? " max-content" : "");
this.hscroll.visible = horz;
this.hscroll.element.style[horz ? "removeProperty" : "setProperty"]
("position", "absolute");
this.vscroll.visible = vert;
this.vscroll.element.style[vert ? "removeProperty" : "setProperty"]
("position", "absolute");
this.corner.visible = vert && horz;
this.element.classList[horz ? "add" : "remove"]("horizontal");
this.element.classList[vert ? "add" : "remove"]("vertical");
}
}
export { register };

363
web/toolkit/SplitPane.js Normal file
View File

@ -0,0 +1,363 @@
let register = Toolkit => Toolkit.SplitPane =
// Presentational text
class SplitPane extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class: "tk split-pane"
}, options, { style: Object.assign({
display : "grid",
gridTemplateColumns: "max-content max-content auto",
overflow : "hidden"
}, options.style || {}) }));
// Configure instance fields
this.drag = null;
this._orientation = "left";
this._primary = null;
this.resizer = new ResizeObserver(()=>this.priResize());
this.restore = null;
this._secondary = null;
// Primary placeholder
this.noPrimary = document.createElement("div");
this.noPrimary.id = Toolkit.id();
// Separator widget
this.splitter = document.createElement("div");
this.splitter.setAttribute("aria-controls", this.noPrimary.id);
this.splitter.setAttribute("aria-valuemin", "0");
this.splitter.setAttribute("role", "separator");
this.splitter.setAttribute("tabindex", "0");
this.splitter.addEventListener("keydown",
e=>this.splitKeyDown (e));
this.splitter.addEventListener("pointerdown",
e=>this.splitPointerDown(e));
this.splitter.addEventListener("pointermove",
e=>this.splitPointerMove(e));
this.splitter.addEventListener("pointerup" ,
e=>this.splitPointerUp (e));
// Secondary placeholder
this.noSecondary = document.createElement("div");
// Configure options
if ("orientation" in options)
this.orientation = options.orientation;
if ("primary" in options)
this.primary = options.primary;
if ("secondary" in options)
this.secondary = options.secondary;
this.revalidate();
}
///////////////////////////// Event Handlers //////////////////////////////
// Primary pane resized
priResize() {
let metrics = this.measure();
this.splitter.setAttribute("aria-valuemax", metrics.max);
this.splitter.setAttribute("aria-valuenow", metrics.value);
}
// Splitter key press
splitKeyDown(e) {
// Error checking
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
return;
// Drag is in progress
if (this.drag) {
if (e.key != "Escape")
return;
this.splitter.releasePointerCapture(this.drag.pointerId);
this.value = this.drag.size;
this.drag = null;
Toolkit.handle(e);
return;
}
// Processing by key
let edge = this.orientation;
let horz = edge == "left" || edge == "right";
switch (e.key) {
case "ArrowDown":
if (horz)
return;
this.restore = null;
this.value += edge == "top" ? +10 : -10;
break;
case "ArrowLeft":
if (!horz)
return;
this.restore = null;
this.value += edge == "left" ? -10 : +10;
break;
case "ArrowRight":
if (!horz)
return;
this.restore = null;
this.value += edge == "left" ? +10 : -10;
break;
case "ArrowUp":
if (horz)
return;
this.restore = null;
this.value += edge == "top" ? -10 : +10;
break;
case "End":
this.restore = null;
this.value = this.element.getBoundingClientRect()
[horz ? "width" : "height"];
break;
case "Enter":
if (this.restore !== null) {
this.value = this.restore;
this.restore = null;
} else {
this.restore = this.value;
this.value = 0;
}
break;
case "Home":
this.restore = null;
this.value = 0;
break;
default: return;
}
Toolkit.handle(e);
}
// Splitter pointer down
splitPointerDown(e) {
// Do not obtain focus automatically
e.preventDefault();
// Error checking
if (
e.altKey || e.ctrlKey || e.shiftKey || e.button != 0 ||
this.disabled || this.splitter.hasPointerCapture(e.pointerId)
) return;
// Begin dragging
this.splitter.setPointerCapture(e.poinerId);
let horz = this.orientation == "left" || this.orientation == "right";
let prop = horz ? "width" : "height";
this.drag = {
pointerId: e.pointerId,
size : (this._primary || this.noPrimary)
.getBoundingClientRect()[prop],
start : e[horz ? "clientX" : "clientY" ]
};
Toolkit.handle(e);
}
// Splitter pointer move
splitPointerMove(e) {
// Error checking
if (!this.splitter.hasPointerCapture(e.pointerId))
return;
// Working variables
let horz = this.orientation == "left" || this.orientation == "right";
let delta = e[horz ? "clientX" : "clientY"] - this.drag.start;
let scale = this.orientation == "bottom" ||
this.orientation == "right" ? -1 : 1;
// Resize the primary component
this.restore = null;
this.value = Math.round(this.drag.size + scale * delta);
Toolkit.handle(e);
}
// Splitter pointer up
splitPointerUp(e) {
// Error checking
if (e.button != 0 || !this.splitter.hasPointerCapture(e.pointerId))
return;
// End dragging
this.splitter.releasePointerCapture(e.pointerId);
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Edge containing the primary pane
get orientation() { return this._orientation; }
set orientation(orientation) {
switch (orientation) {
case "bottom":
case "left":
case "right":
case "top":
break;
default: return;
}
if (orientation == this.orientation)
return;
this._orientation = orientation;
this.revalidate();
}
// Primary content pane
get primary() {
return !this._primary ? null :
this._primary.component || this._primary;
}
set primary(element) {
if (element instanceof Toolkit.Component)
element = element.element;
if (!(element instanceof HTMLElement) || element == this.element)
return;
this.resizer.unobserve(this._primary || this.noPrimary);
this._primary = element || null;
this.resizer.observe(element || this.noPrimary);
this.splitter.setAttribute("aria-controls",
(element || this.noPrimary).id);
this.revalidate();
}
// Secondary content pane
get secondary() {
return !this._secondary ? null :
this._secondary.component || this._secondary;
}
set secondary(element) {
if (element instanceof Toolkit.Component)
element = element.element;
if (!(element instanceof HTMLElement) || element == this.element)
return;
this._secondary = element || null;
this.revalidate();
}
// Current splitter position
get value() {
return Math.ceil(
(this._primary || this.primary).getBoundingClientRect()
[this.orientation == "left" || this.orientation == "right" ?
"width" : "height"]
);
}
set value(value) {
value = Math.round(value);
// Error checking
if (value == this.value)
return;
// Working variables
let pri = this._primary || this.noPrimary;
let sec = this._secondary || this.noSecondary;
let prop = this.orientation == "left" || this.orientation == "right" ?
"width" : "height";
// Resize the primary component
pri.style[prop] = Math.max(0, value) + "px";
// Ensure the pane didn't become too large due to margin styles
let propPri = pri .getBoundingClientRect()[prop];
let propSec = sec .getBoundingClientRect()[prop];
let propSplit = this.splitter.getBoundingClientRect()[prop];
let propThis = this.element .getBoundingClientRect()[prop];
if (propPri + propSec + propSplit > propThis) {
pri.style[prop] = Math.max(0, Math.floor(
propThis - propSec - propSplit)) + "px";
}
}
///////////////////////////// Private Methods /////////////////////////////
// Measure the current bounds of the child elements
measure() {
let prop = this.orientation == "top" || this.orientation == "bottom" ?
"height" : "width";
let bndThis = this.element .getBoundingClientRect();
let bndSplit = this.splitter.getBoundingClientRect();
let bndPri = (this._primary || this.noPrimary)
.getBoundingClientRect();
return {
max : bndThis[prop],
property: prop,
value : bndPri[prop]
}
}
// Arrange child components
revalidate() {
let horz = true;
let children = [
this._primary || this.noPrimary,
this.splitter,
this._secondary || this.noSecondary
];
// Select styles by orientation
switch (this.orientation) {
case "bottom":
Object.assign(this.element.style, {
gridAutoColumns : "100%",
gridTemplateRows: "auto max-content max-content"
});
horz = false;
children.reverse();
break;
case "left":
Object.assign(this.element.style, {
gridAutoRows : "100%",
gridTemplateColumns: "max-content max-content auto"
});
break;
case "right":
Object.assign(this.element.style, {
gridAutoRows : "100%",
gridTemplateColumns: "auto max-content max-content"
});
children.reverse();
break;
case "top":
Object.assign(this.element.style, {
gridAutoColumns : "100%",
gridTemplateRows: "max-content max-content auto"
});
horz = false;
break;
}
// Update element
if (horz) {
this.element.style.removeProperty("grid-auto-columns");
this.element.style.removeProperty("grid-template-rows");
this.splitter.className = "tk horizontal";
this.splitter.style.cursor = "ew-resize";
} else {
this.element.style.removeProperty("grid-auto-rows");
this.element.style.removeProperty("grid-template-columns");
this.splitter.className = "tk vertical";
this.splitter.style.cursor = "ns-resize";
}
this.element.replaceChildren(... children);
this.priResize();
}
}
export { register };

66
web/toolkit/TextBox.js Normal file
View File

@ -0,0 +1,66 @@
let register = Toolkit => Toolkit.TextBox =
// Check box
class TextBox extends Toolkit.Component {
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, options = Object.assign({
class : "tk text-box",
tag : "input",
type : "text"
}, options));
this.element.addEventListener("focusout", e=>this.commit ( ));
this.element.addEventListener("keydown" , e=>this.onKeyDown(e));
this.value = options.value || null;
}
///////////////////////////// Event Handlers //////////////////////////////
// Key press
onKeyDown(e) {
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
return;
e.stopPropagation();
if (e.key == "Enter")
this.commit();
}
///////////////////////////// Public Methods //////////////////////////////
// Contained text
get value() { return this.element.value; }
set value(value) {
value = value === undefined || value === null ? "" : value.toString();
if (value == this.value)
return;
this.element.value = value;
}
///////////////////////////// Package Methods /////////////////////////////
// Update localization strings
localize() {
this.localizeLabel();
this.localizeTitle();
}
///////////////////////////// Private Methods /////////////////////////////
// Complete editing
commit() {
this.element.dispatchEvent(new Event("action"));
}
}
export { register };

51
web/toolkit/Toolkit.js Normal file
View File

@ -0,0 +1,51 @@
// Namespace container for toolkit classes
class Toolkit {
// Next numeric ID
static nextId = 0;
///////////////////////////// Static Methods //////////////////////////////
// Produce a synthetic Event object
static event(type, properties, bubbles = true) {
return Object.assign(
new Event(type, { bubbles: bubbles }),
properties
);
}
// Finalize an event
static handle(event) {
event.stopPropagation();
event.preventDefault();
}
// Generate a unique DOM element ID
static id() {
return "tk" + this.nextId++;
}
}
// Register component classes
(await import(/**/"./Component.js" )).register(Toolkit);
(await import(/**/"./App.js" )).register(Toolkit);
(await import(/**/"./Button.js" )).register(Toolkit);
(await import(/**/"./Checkbox.js" )).register(Toolkit);
(await import(/**/"./Desktop.js" )).register(Toolkit);
(await import(/**/"./DropDown.js" )).register(Toolkit);
(await import(/**/"./Label.js" )).register(Toolkit);
(await import(/**/"./Menu.js" )).register(Toolkit);
(await import(/**/"./MenuBar.js" )).register(Toolkit);
(await import(/**/"./MenuItem.js" )).register(Toolkit);
(await import(/**/"./ScrollBar.js" )).register(Toolkit);
(await import(/**/"./ScrollPane.js")).register(Toolkit);
(await import(/**/"./Radio.js" )).register(Toolkit);
(await import(/**/"./RadioGroup.js")).register(Toolkit);
(await import(/**/"./SplitPane.js" )).register(Toolkit);
(await import(/**/"./TextBox.js" )).register(Toolkit);
(await import(/**/"./Window.js" )).register(Toolkit);
export { Toolkit };

479
web/toolkit/Window.js Normal file
View File

@ -0,0 +1,479 @@
let register = Toolkit => Toolkit.Window =
class Window extends Toolkit.Component {
//////////////////////////////// Constants ////////////////////////////////
// Resize directions by dragging edge
static RESIZES = {
"nw": { left : true, top : true },
"n" : { top : true },
"ne": { right: true, top : true },
"w" : { left : true },
"e" : { right: true },
"sw": { left : true, bottom: true },
"s" : { bottom: true },
"se": { right: true, bottom: true }
};
///////////////////////// Initialization Methods //////////////////////////
constructor(app, options = {}) {
super(app, Object.assign({
"aria-modal": "false",
class : "tk window",
id : Toolkit.id(),
role : "dialog",
tabIndex : -1,
visibility : true,
visible : false
}, options, { style: Object.assign({
boxSizing : "border-box",
display : "grid",
gridTemplateRows: "max-content auto",
position : "absolute"
}, options.style || {})} ));
// Configure instance fields
this.lastFocus = null;
this.style = getComputedStyle(this.element);
this.text = null;
// Configure event listeners
this.addEventListener("focusin", e=>this.onFocus (e));
this.addEventListener("keydown", e=>this.onKeyDown(e));
// Working variables
let onBorderDown = e=>this.onBorderDown(e);
let onBorderMove = e=>this.onBorderMove(e);
let onBorderUp = e=>this.onBorderUp (e);
let onDragKey = e=>this.onDragKey (e);
// Resizing borders
for (let edge of [ "nw1", "nw2", "n", "ne1", "ne2",
"w", "e", "sw1", "sw2", "s", "se1", "se2"]) {
let border = document.createElement("div");
border.className = edge;
border.edge = edge.replace(/[12]/g, "");
Object.assign(border.style, {
cursor : border.edge + "-resize",
position: "absolute"
});
border.addEventListener("keydown" , onDragKey );
border.addEventListener("pointerdown", onBorderDown);
border.addEventListener("pointermove", onBorderMove);
border.addEventListener("pointerup" , onBorderUp );
this.element.append(border);
}
// Title bar
this.titleBar = document.createElement("div");
this.titleBar.className = "title";
this.titleBar.id = Toolkit.id();
Object.assign(this.titleBar.style, {
display : "grid",
gridTemplateColumns: "auto max-content",
minWidth : "0",
overflow : "hidden"
});
this.element.append(this.titleBar);
this.titleBar.addEventListener("keydown" , e=>this.onDragKey (e));
this.titleBar.addEventListener("pointerdown", e=>this.onTitleDown(e));
this.titleBar.addEventListener("pointermove", e=>this.onTitleMove(e));
this.titleBar.addEventListener("pointerup" , e=>this.onTitleUp (e));
// Title text
this.title = document.createElement("div");
this.title.className = "text";
this.title.id = Toolkit.id();
this.title.innerText = "\u00a0";
this.titleBar.append(this.title);
this.element.setAttribute("aria-labelledby", this.title.id);
// Close button
this.close = new Toolkit.Button(app, {
class : "close-button",
doNotFocus: true
});
this.close.addEventListener("action",
e=>this.element.dispatchEvent(new Event("close")));
this.close.setLabel("{window.close}", true);
this.close.setTitle("{window.close}", true);
this.titleBar.append(this.close.element);
// Client area
this.client = document.createElement("div");
this.client.className = "client";
Object.assign(this.client.style, {
minHeight: "0",
minWidth : "0",
overflow : "hidden"
});
this.element.append(this.client);
}
///////////////////////////// Event Handlers //////////////////////////////
// Border pointer down
onBorderDown(e) {
// Do not drag
if (e.button != 0 || this.app.drag != null)
return;
// Initiate dragging
this.drag = {
height: this.outerHeight,
left : this.left,
top : this.top,
width : this.outerWidth,
x : e.clientX,
y : e.clientY
};
this.drag.bottom = this.drag.top + this.drag.height;
this.drag.right = this.drag.left + this.drag.width;
// Configure event
this.focus();
this.app.drag = e;
Toolkit.handle(e);
}
// Border pointer move
onBorderMove(e) {
// Not dragging
if (this.app.drag != e.target)
return;
// Working variables
let resize = Toolkit.Window.RESIZES[e.target.edge];
let dx = e.clientX - this.drag.x;
let dy = e.clientY - this.drag.y;
let style = getComputedStyle(this.element);
let minHeight =
this.client .getBoundingClientRect().top -
this.titleBar.getBoundingClientRect().top +
parseFloat(style.borderTop ) +
parseFloat(style.borderBottom)
;
// Output bounds
let height = this.drag.height;
let left = this.drag.left;
let top = this.drag.top;
let width = this.drag.width;
// Dragging left
if (resize.left) {
let bParent = this.parent.element.getBoundingClientRect();
left += dx;
left = Math.min(left, this.drag.right - 32);
left = Math.min(left, bParent.width - 16);
width = this.drag.width + this.drag.left - left;
}
// Dragging top
if (resize.top) {
let bParent = this.parent.element.getBoundingClientRect();
top += dy;
top = Math.max(top, 0);
top = Math.min(top, this.drag.bottom - minHeight);
top = Math.min(top, bParent.height - minHeight);
height = this.drag.height + this.drag.top - top;
}
// Dragging right
if (resize.right) {
width += dx;
width = Math.max(width, 32);
width = Math.max(width, 16 - this.drag.left);
}
// Dragging bottom
if (resize.bottom) {
height += dy;
height = Math.max(height, minHeight);
}
// Apply bounds
this.element.style.height = height + "px";
this.element.style.left = left + "px";
this.element.style.top = top + "px";
this.element.style.width = width + "px";
// Configure event
Toolkit.handle(e);
}
// Border pointer up
onBorderUp(e, id) {
// Not dragging
if (e.button != 0 || this.app.drag != e.target)
return;
// Configure instance fields
this.drag = null;
// Configure event
this.app.drag = null;
Toolkit.handle(e);
}
// Key down while dragging
onDragKey(e) {
if (
this.drag != null && e.key == "Escape" &&
!e.ctrlKey && !e.altKey && !e.shiftKey
) this.cancelDrag();
}
// Focus gained
onFocus(e) {
// The element receiving focus is self, or Close button from external
if (
e.target == this.element ||
e.target == this.close.element && !this.contains(e.relatedTarget)
) {
let elm = this.lastFocus;
if (!elm) {
elm = this.getFocusable();
elm = elm[Math.min(1, elm.length - 1)];
}
elm.focus({ preventScroll: true });
}
// The element receiving focus is not self
else if (e.target != this.close.element)
this.lastFocus = e.target;
// Bring the window to the front among its siblings
if (this.parent instanceof Toolkit.Desktop)
this.parent.bringToFront(this);
}
// Window key press
onKeyDown(e) {
// Take no action
if (e.altKey || e.ctrlKey || e.key != "Tab")
return;
// Move focus to the next element in the sequence
let focuses = this.getFocusable();
let nowIndex = focuses.indexOf(document.activeElement) || 0;
let nextIndex = nowIndex + focuses.length + (e.shiftKey ? -1 : 1);
let target = focuses[nextIndex % focuses.length];
Toolkit.handle(e);
target.focus();
}
// Title bar pointer down
onTitleDown(e) {
// Do not drag
if (e.button != 0 || this.app.drag != null)
return;
// Initiate dragging
this.drag = {
height: this.outerHeight,
left : this.left,
top : this.top,
width : this.outerWidth,
x : e.clientX,
y : e.clientY
};
// Configure event
this.focus();
this.app.drag = e;
Toolkit.handle(e);
}
// Title bar pointer move
onTitleMove(e) {
// Not dragging
if (this.app.drag != e.target)
return;
// Working variables
let bParent = this.parent.element.getBoundingClientRect();
// Move horizontally
let left = this.drag.left + e.clientX - this.drag.x;
left = Math.min(left, bParent.width - 16);
left = Math.max(left, 16 - this.drag.width);
this.element.style.left = left + "px";
// Move vertically
let top = this.drag.top + e.clientY - this.drag.y;
top = Math.min(top, bParent.height - this.minHeight);
top = Math.max(top, 0);
this.element.style.top = top + "px";
// Configure event
Toolkit.handle(e);
}
// Title bar pointer up
onTitleUp(e) {
// Not dragging
if (e.button != 0 || this.app.drag != e.target)
return;
// Configure instance fields
this.drag = null;
// Configure event
this.app.drag = null;
Toolkit.handle(e);
}
///////////////////////////// Public Methods //////////////////////////////
// Bring the window to the front among its siblings
bringToFront() {
if (this.parent != null)
this.parent.bringToFront(this);
}
// Set focus on the component
focus() {
if (!this.contains(document.activeElement))
(this.lastFocus || this.element).focus({ preventScroll: true });
if (this.parent instanceof Toolkit.Desktop)
this.parent.bringToFront(this);
}
// Height of client
get height() { return this.client.getBoundingClientRect().height; }
set height(height) {
this.element.style.height =
this.outerHeight - this.height + Math.max(height, 0) + "px";
}
// Position of window left edge
get left() {
return this.element.getBoundingClientRect().left - (
this.parent == null ? 0 :
this.parent.element.getBoundingClientRect().left
);
}
set left(left) {
if (this.parent != null) {
left = Math.min(left,
this.parent.element.getBoundingClientRect().width - 16);
}
left = Math.max(left, 16 - this.outerWidth);
this.element.style.left = left + "px";
}
// Height of entire window
get outerHeight() { return this.element.getBoundingClientRect().height; }
set outerHeight(height) {
height = Math.max(height, this.minHeight);
this.element.style.height = height + "px";
}
// Width of entire window
get outerWidth() { return this.element.getBoundingClientRect().width; }
set outerWidth(width) {
width = Math.max(width, 32);
this.element.style.width = width + "px";
let left = this.left;
if (left + width < 16)
this.element.style.left = 16 - width + "px";
}
// Specify the window title
setTitle(title, localize) {
this.setString("text", title, localize);
}
// Position of window top edge
get top() {
return this.element.getBoundingClientRect().top - (
this.parent == null ? 0 :
this.parent.element.getBoundingClientRect().top
);
}
set top(top) {
if (this.parent != null) {
top = Math.min(top, -this.minHeight +
this.parent.element.getBoundingClientRect().height);
}
top = Math.max(top, 0);
this.element.style.top = top + "px";
}
// Specify whether the element is visible
get visible() { return super.visible; }
set visible(visible) {
let prevSetting = super.visible;
let prevActual = this.isVisible();
super.visible = visible;
let nowActual = this.isVisible();
if (!nowActual && this.contains(document.activeElement))
document.activeElement.blur();
}
// Width of client
get width() { return this.client.getBoundingClientRect().width; }
set width(width) {
this.outerWidth = this.outerWidth - this.width + Math.max(width, 32);
}
///////////////////////////// Package Methods /////////////////////////////
// Add a child component to the primary client region of this component
append(element) {
this.client.append(element);
}
// Update localization strings
localize() {
this.localizeText(this.title);
if ((this.title.textContent || "") == "")
this.title.innerText = "\u00a0"; // &nbsp;
this.close.localize();
}
///////////////////////////// Private Methods /////////////////////////////
// Cancel a move or resize dragging operaiton
cancelDrag() {
this.app.drag = null;
this.element.style.height = this.drag.height + "px";
this.element.style.left = this.drag.left + "px";
this.element.style.top = this.drag.top + "px";
this.element.style.width = this.drag.width + "px";
this.drag = null;
}
// Minimum height of window
get minHeight() {
return (
this.client .getBoundingClientRect().top -
this.element.getBoundingClientRect().top +
parseFloat(this.style.borderBottomWidth)
);
}
}
export { register };