Compare commits
No commits in common. "master" and "old-web" have entirely different histories.
|
@ -0,0 +1,214 @@
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,308 @@
|
||||||
|
/*
|
||||||
|
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());
|
|
@ -0,0 +1,448 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { Disassembler } from /**/"./Disassembler.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// CPU //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// CPU register editor and disassembler
|
||||||
|
class CPU extends Toolkit.SplitPane {
|
||||||
|
|
||||||
|
///////////////////////// Initialization Methods //////////////////////////
|
||||||
|
|
||||||
|
constructor(app, sim) {
|
||||||
|
super(app, {
|
||||||
|
className: "tk tk-splitpane tk-cpu",
|
||||||
|
edge : Toolkit.SplitPane.RIGHT,
|
||||||
|
style : {
|
||||||
|
position: "relative"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.app = app;
|
||||||
|
this.sim = sim;
|
||||||
|
this.initDasm();
|
||||||
|
|
||||||
|
this.metrics = new Toolkit.Component(this.app, {
|
||||||
|
className: "tk tk-mono",
|
||||||
|
tagName : "div",
|
||||||
|
style : {
|
||||||
|
position : "absolute",
|
||||||
|
visibility: "hidden"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let text = "";
|
||||||
|
for (let x = 0; x < 16; x++) {
|
||||||
|
if (x) text += "\n";
|
||||||
|
let digit = x.toString(16);
|
||||||
|
text += digit + "\n" + digit.toUpperCase();
|
||||||
|
}
|
||||||
|
this.metrics.element.innerText = text;
|
||||||
|
this.splitter.append(this.metrics.element);
|
||||||
|
|
||||||
|
this.setView(1, this.regs = new Toolkit.SplitPane(app, {
|
||||||
|
className: "tk tk-splitpane",
|
||||||
|
edge : Toolkit.SplitPane.TOP
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.regs.setView(0, this.sysregs = new RegisterList(this, true ));
|
||||||
|
this.regs.setView(1, this.proregs = new RegisterList(this, false));
|
||||||
|
|
||||||
|
// Adjust split panes to the initial size of the System Registers pane
|
||||||
|
let resize;
|
||||||
|
let preshow = e=>this.onPreshow(resize);
|
||||||
|
resize = new ResizeObserver(preshow);
|
||||||
|
resize.observe(this.sysregs.viewport);
|
||||||
|
resize.observe(this.metrics.element);
|
||||||
|
|
||||||
|
this.metrics.addEventListener("resize", e=>this.metricsResize());
|
||||||
|
}
|
||||||
|
|
||||||
|
initDasm() {
|
||||||
|
this.dasm = new Disassembler(this.app, this.sim);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Event Handlers //////////////////////////////
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Resize handler prior to first visibility
|
||||||
|
onPreshow(resize) {
|
||||||
|
this.metricsResize();
|
||||||
|
|
||||||
|
// Once the list of registers is visible, stop listening
|
||||||
|
if (this.isVisible()) {
|
||||||
|
resize.disconnect();
|
||||||
|
this.sysregs.view.element.style.display = "grid";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the split panes
|
||||||
|
let sys = this.sysregs.view.element;
|
||||||
|
let pro = this.proregs.view.element;
|
||||||
|
this.setValue(
|
||||||
|
Math.max(sys.scrollWidth, pro.scrollWidth) +
|
||||||
|
this.sysregs.vertical.getBounds().width
|
||||||
|
);
|
||||||
|
this.regs.setValue(
|
||||||
|
this.sysregs[PSW].expansion.getBounds().bottom -
|
||||||
|
sys.getBoundingClientRect().top
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export { CPU };
|
|
@ -0,0 +1,187 @@
|
||||||
|
import { Disassembler } from /**/"./Disassembler.js";
|
||||||
|
import { Memory } from /**/"./Memory.js";
|
||||||
|
import { RegisterList } from /**/"./RegisterList.js";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class Debugger {
|
||||||
|
|
||||||
|
///////////////////////// Initialization Methods //////////////////////////
|
||||||
|
|
||||||
|
constructor(app, index, sim) {
|
||||||
|
|
||||||
|
// Configure instance fields
|
||||||
|
this.app = app;
|
||||||
|
this.index = index;
|
||||||
|
this.sim = sim;
|
||||||
|
|
||||||
|
// Configure components
|
||||||
|
this.disassembler = new Disassembler(this);
|
||||||
|
this.memory = new Memory (this);
|
||||||
|
this.programRegisters = new RegisterList(this, false);
|
||||||
|
this.systemRegisters = new RegisterList(this, true );
|
||||||
|
|
||||||
|
// Configure windows
|
||||||
|
this.initCPUWindow ();
|
||||||
|
this.initMemoryWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the CPU window
|
||||||
|
initCPUWindow() {
|
||||||
|
let app = this.app;
|
||||||
|
|
||||||
|
// Produce the window
|
||||||
|
let wnd = this.cpuWindow = new app.Toolkit.Window(app, {
|
||||||
|
width : 400,
|
||||||
|
height : 300,
|
||||||
|
className : "tk tk-window tk-cpu" + (this.index==0?"":" two"),
|
||||||
|
closeToolTip: "common.close",
|
||||||
|
title : "cpu._",
|
||||||
|
visible : false
|
||||||
|
});
|
||||||
|
wnd.setSubstitution("sim", this.index == 1 ? " 2" : "");
|
||||||
|
wnd.addEventListener("close", ()=>wnd.setVisible(false));
|
||||||
|
app.desktop.add(wnd);
|
||||||
|
|
||||||
|
// Visibility override
|
||||||
|
let that = this;
|
||||||
|
let setVisible = wnd.setVisible;
|
||||||
|
wnd.setVisible = function(visible) {
|
||||||
|
that.disassembler .setSubscribed(visible);
|
||||||
|
that.systemRegisters .setSubscribed(visible);
|
||||||
|
that.programRegisters.setSubscribed(visible);
|
||||||
|
setVisible.apply(wnd, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-seek the initial view
|
||||||
|
let onSeek = ()=>this.disassembler.seek(this.disassembler.pc, true);
|
||||||
|
Toolkit.addResizeListener(this.disassembler.element, onSeek);
|
||||||
|
wnd.addEventListener("firstshow", ()=>{
|
||||||
|
app.desktop.center(wnd);
|
||||||
|
Toolkit.removeResizeListener(this.disassembler.element, onSeek);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Window splitters
|
||||||
|
let sptMain = new Toolkit.SplitPane(this.app, {
|
||||||
|
className: "tk tk-splitpane tk-main",
|
||||||
|
edge : Toolkit.SplitPane.RIGHT
|
||||||
|
});
|
||||||
|
let sptRegs = new Toolkit.SplitPane(this.app, {
|
||||||
|
className: "tk tk-splitpane tk-registers",
|
||||||
|
edge : Toolkit.SplitPane.TOP
|
||||||
|
});
|
||||||
|
|
||||||
|
// Configure window splitter initial size
|
||||||
|
let resize = new ResizeObserver(()=>{
|
||||||
|
|
||||||
|
// Measure register lists
|
||||||
|
let sys = this.systemRegisters .getPreferredSize();
|
||||||
|
let pro = this.programRegisters.getPreferredSize();
|
||||||
|
let height = Math.ceil(Math.max(sys.height, pro.height));
|
||||||
|
let width = Math.ceil(Math.max(sys.width , pro.width )) +
|
||||||
|
this.systemRegisters.vertical.getBounds().width;
|
||||||
|
|
||||||
|
// Configure splitters
|
||||||
|
if (sptMain.getValue() != width)
|
||||||
|
sptMain.setValue(width);
|
||||||
|
if (sptRegs.getValue() != height)
|
||||||
|
sptRegs.setValue(height);
|
||||||
|
});
|
||||||
|
resize.observe(this.programRegisters.view.element);
|
||||||
|
resize.observe(this.systemRegisters .view.element);
|
||||||
|
|
||||||
|
// Stop monitoring splitter size when something receives focus
|
||||||
|
let onFocus = e=>{
|
||||||
|
resize.disconnect();
|
||||||
|
wnd.removeEventListener("focus", onFocus, true);
|
||||||
|
};
|
||||||
|
sptMain.addEventListener("focus", onFocus, true);
|
||||||
|
|
||||||
|
// Configure window layout
|
||||||
|
sptMain.setView(0, this.disassembler);
|
||||||
|
sptMain.setView(1, sptRegs);
|
||||||
|
sptRegs.setView(0, this.systemRegisters);
|
||||||
|
sptRegs.setView(1, this.programRegisters);
|
||||||
|
wnd.append(sptMain);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up the Memory window
|
||||||
|
initMemoryWindow() {
|
||||||
|
let app = this.app;
|
||||||
|
|
||||||
|
// Produce the window
|
||||||
|
let wnd = this.memoryWindow = new app.Toolkit.Window(app, {
|
||||||
|
width : 400,
|
||||||
|
height : 300,
|
||||||
|
className : "tk tk-window" + (this.index == 0 ? "" : " two"),
|
||||||
|
closeToolTip: "common.close",
|
||||||
|
title : "memory._",
|
||||||
|
visible : false
|
||||||
|
});
|
||||||
|
wnd.setSubstitution("sim", this.index == 1 ? " 2" : "");
|
||||||
|
wnd.addEventListener("close" , ()=>wnd.setVisible(false));
|
||||||
|
wnd.addEventListener("firstshow", ()=>app.desktop.center(wnd));
|
||||||
|
app.desktop.add(wnd);
|
||||||
|
|
||||||
|
// Visibility override
|
||||||
|
let that = this;
|
||||||
|
let setVisible = wnd.setVisible;
|
||||||
|
wnd.setVisible = function(visible) {
|
||||||
|
that.memory.setSubscribed(visible);
|
||||||
|
setVisible.apply(wnd, arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Configure window layout
|
||||||
|
wnd.append(this.memory);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Package Methods /////////////////////////////
|
||||||
|
|
||||||
|
// Retrieve the disassembler configuraiton
|
||||||
|
getDasmConfig() {
|
||||||
|
return this.disassembler.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to run until the next instruction
|
||||||
|
runNext() {
|
||||||
|
this.app.runNext(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the disassembler configuration
|
||||||
|
setDasmConfig(config) {
|
||||||
|
this.disassembler .setConfig(config);
|
||||||
|
this.memory .dasmChanged();
|
||||||
|
this.programRegisters.dasmChanged();
|
||||||
|
this.systemRegisters .dasmChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify whether dual sims mode is active
|
||||||
|
setDualSims(dualSims) {
|
||||||
|
|
||||||
|
// Update substitutions for sim 1
|
||||||
|
if (this.index == 0) {
|
||||||
|
let sub = dualSims ? " 1" : "";
|
||||||
|
this.cpuWindow .setSubstitution("sim", sub);
|
||||||
|
this.memoryWindow.setSubstitution("sim", sub);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide windows for sim 2
|
||||||
|
else if (!dualSims) {
|
||||||
|
this.cpuWindow .close(false);
|
||||||
|
this.memoryWindow.close(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute one instruction
|
||||||
|
singleStep() {
|
||||||
|
this.app.singleStep(this.index);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export { Debugger };
|
|
@ -0,0 +1,958 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,517 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,889 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,44 @@
|
||||||
|
let F32 = new Float32Array( 1);
|
||||||
|
let S32 = new Int32Array (F32.buffer, 0, 1);
|
||||||
|
let U32 = new Uint32Array (F32.buffer, 0, 1);
|
||||||
|
|
||||||
|
// Interpret a floating short as a 32-bit integer
|
||||||
|
function fromF32(x) {
|
||||||
|
F32[0] = x;
|
||||||
|
return S32[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Interpret a 32-bit integer as a floating short
|
||||||
|
function toF32(x) {
|
||||||
|
S32[0] = x;
|
||||||
|
return F32[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represent a value as a signed 32-bit integer
|
||||||
|
function s32(x) {
|
||||||
|
S32[0] = x;
|
||||||
|
return S32[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign-extend a value with a given number of bits
|
||||||
|
function signExtend(value, bits) {
|
||||||
|
bits = 32 - bits;
|
||||||
|
S32[0] = value << bits;
|
||||||
|
return S32[0] >> bits;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Represent a value as an unsigned 32-bit integer
|
||||||
|
function u32(x) {
|
||||||
|
U32[0] = x;
|
||||||
|
return U32[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export let Util = {
|
||||||
|
fromF32 : fromF32,
|
||||||
|
toF32 : toF32,
|
||||||
|
s32 : s32,
|
||||||
|
signExtend: signExtend,
|
||||||
|
u32 : u32
|
||||||
|
};
|
|
@ -0,0 +1,196 @@
|
||||||
|
import { Sim } from /**/"./Sim.js";
|
||||||
|
|
||||||
|
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
|
||||||
|
|
||||||
|
let RESTRICT = {};
|
||||||
|
let WASM_URL = url(/**/"./core.wasm" );
|
||||||
|
let WORKER_URL = url(/**/"./CoreWorker.js");
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Core //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// Environment manager for simulated Virtual Boys
|
||||||
|
class Core {
|
||||||
|
|
||||||
|
//////////////////////////////// Constants ////////////////////////////////
|
||||||
|
|
||||||
|
// States
|
||||||
|
static IDLE = 0;
|
||||||
|
static RUNNING = 1;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Static Methods //////////////////////////////
|
||||||
|
|
||||||
|
// Create a new instance of Core
|
||||||
|
static create(options) {
|
||||||
|
return new Core(RESTRICT).init(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////// Initialization Methods //////////////////////////
|
||||||
|
|
||||||
|
// Stub constructor
|
||||||
|
constructor(restrict) {
|
||||||
|
if (restrict != RESTRICT) {
|
||||||
|
throw "Cannot instantiate Core directly. " +
|
||||||
|
"Use Core.create() instead.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute constructor
|
||||||
|
async init(options = {}) {
|
||||||
|
|
||||||
|
// Configure instance fields
|
||||||
|
this.length = 0;
|
||||||
|
this.onsubscriptions = null;
|
||||||
|
this.resolutions = [];
|
||||||
|
this.state = Core.IDLE;
|
||||||
|
this.worker = new Worker(WORKER_URL);
|
||||||
|
this.worker.onmessage = e=>this.onMessage(e.data);
|
||||||
|
|
||||||
|
// Issue a create command
|
||||||
|
if ("sims" in options)
|
||||||
|
await this.create(options.sims, WASM_URL);
|
||||||
|
|
||||||
|
// Only initialize the WebAssembly module
|
||||||
|
else this.send("init", false, { wasm: WASM_URL });
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Event Handlers //////////////////////////////
|
||||||
|
|
||||||
|
// Worker message received
|
||||||
|
onMessage(data) {
|
||||||
|
|
||||||
|
// Process a promised response
|
||||||
|
if ("response" in data)
|
||||||
|
this.resolutions.shift()(data.response);
|
||||||
|
|
||||||
|
// Process subscriptions
|
||||||
|
if (this.onsubscriptions && data.subscriptions)
|
||||||
|
this.onsubscriptions(data.subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Public Methods //////////////////////////////
|
||||||
|
|
||||||
|
// Associate two simulations as peers, or remove an association
|
||||||
|
connect(a, b, options = {}) {
|
||||||
|
return this.send({
|
||||||
|
command: "connect",
|
||||||
|
respond: !("respond" in options) || !!options.respond,
|
||||||
|
sims : [ a, b ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and initialize new simulations
|
||||||
|
async create(sims, wasm) {
|
||||||
|
let numSims = sims===undefined ? 1 : Math.max(0, parseInt(sims) || 0);
|
||||||
|
|
||||||
|
// Execute the command in the core thread
|
||||||
|
let response = await this.send({
|
||||||
|
command: "create",
|
||||||
|
sims : numSims,
|
||||||
|
wasm : wasm
|
||||||
|
});
|
||||||
|
|
||||||
|
// Process the core thread's response
|
||||||
|
let ret = [];
|
||||||
|
for (let x = 0; x < numSims; x++, this.length++)
|
||||||
|
ret.push(this[this.length] =
|
||||||
|
new Sim(this, response[x], this.length));
|
||||||
|
return sims === undefined ? ret[0] : ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a simulation
|
||||||
|
destroy(sim, options = {}) {
|
||||||
|
|
||||||
|
// Configure simulation
|
||||||
|
sim = this[sim] || sim;
|
||||||
|
if (sim.core != this)
|
||||||
|
return;
|
||||||
|
let ptr = sim.destroy();
|
||||||
|
|
||||||
|
// State management
|
||||||
|
for (let x = sim.index + 1; x < this.length; x++)
|
||||||
|
(this[x - 1] = this[x]).index--;
|
||||||
|
delete this[--this.length];
|
||||||
|
|
||||||
|
// Execute the command on the core thread
|
||||||
|
return this.send({
|
||||||
|
command: "destroy",
|
||||||
|
respond: !("respond" in options) || !!options.respond,
|
||||||
|
sim : ptr
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to run until the next instruction
|
||||||
|
runNext(a, b, options = {}) {
|
||||||
|
return this.send({
|
||||||
|
command: "runNext",
|
||||||
|
refresh: !!options.refresh,
|
||||||
|
respond: !("respond" in options) || !!options.respond,
|
||||||
|
sims : [ a, b ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Execute one instruction
|
||||||
|
singleStep(a, b, options = {}) {
|
||||||
|
return this.send({
|
||||||
|
command: "singleStep",
|
||||||
|
refresh: !!options.refresh,
|
||||||
|
respond: !("respond" in options) || !!options.respond,
|
||||||
|
sims : [ a, b ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unsubscribe from frame data
|
||||||
|
unsubscribe(key, sim = 0) {
|
||||||
|
this.send({
|
||||||
|
command: "unsubscribe",
|
||||||
|
key : key,
|
||||||
|
respond: false,
|
||||||
|
sim : sim
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Private Methods /////////////////////////////
|
||||||
|
|
||||||
|
// Send a message to the Worker
|
||||||
|
send(data = {}, transfers = []) {
|
||||||
|
|
||||||
|
// Create the message object
|
||||||
|
Object.assign(data, {
|
||||||
|
respond: !("respond" in data) || !!data.respond,
|
||||||
|
run : !("run" in data) || !!data.run
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not wait on a response
|
||||||
|
if (!data.respond)
|
||||||
|
this.worker.postMessage(data, transfers);
|
||||||
|
|
||||||
|
// Wait for the response to come back
|
||||||
|
else return new Promise((resolve, reject)=>{
|
||||||
|
this.resolutions.push(response=>resolve(response));
|
||||||
|
this.worker.postMessage(data, transfers);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
export { Core };
|
|
@ -0,0 +1,377 @@
|
||||||
|
"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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}();
|
|
@ -0,0 +1,151 @@
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Sim //
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
// One simulated Virtual Boy
|
||||||
|
class Sim {
|
||||||
|
|
||||||
|
///////////////////////// Initialization Methods //////////////////////////
|
||||||
|
|
||||||
|
constructor(core, sim, index) {
|
||||||
|
this.core = core;
|
||||||
|
this.index = index;
|
||||||
|
this.sim = sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Public Methods //////////////////////////////
|
||||||
|
|
||||||
|
// Locate CPU instructions
|
||||||
|
disassemble(target, row, rows, scroll, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command : "disassemble",
|
||||||
|
row : row,
|
||||||
|
rows : rows,
|
||||||
|
scroll : scroll,
|
||||||
|
sim : this.sim,
|
||||||
|
subscribe: options.subscribe,
|
||||||
|
target : target
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all CPU program registers
|
||||||
|
getProgramRegisters(options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command : "getProgramRegisters",
|
||||||
|
sim : this.sim,
|
||||||
|
subscribe: options.subscribe
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve the value of a system register
|
||||||
|
getSystemRegister(id, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command : "getSystemRegister",
|
||||||
|
id : id,
|
||||||
|
sim : this.sim,
|
||||||
|
subscribe: options.subscribe
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve all CPU system registers (including PC)
|
||||||
|
getSystemRegisters(options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command : "getSystemRegisters",
|
||||||
|
sim : this.sim,
|
||||||
|
subscribe: options.subscribe
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read multiple bytes from the bus
|
||||||
|
read(address, length, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
address : address,
|
||||||
|
command : "read",
|
||||||
|
debug : !("debug" in options) || !!options.debug,
|
||||||
|
length : length,
|
||||||
|
sim : this.sim,
|
||||||
|
subscribe: options.subscribe
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify a new value for PC
|
||||||
|
setProgramCounter(value, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command: "setProgramCounter",
|
||||||
|
refresh: !!options.refresh,
|
||||||
|
sim : this.sim,
|
||||||
|
value : value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify a new value for a program register
|
||||||
|
setProgramRegister(id, value, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command: "setProgramRegister",
|
||||||
|
id : id,
|
||||||
|
refresh: !!options.refresh,
|
||||||
|
sim : this.sim,
|
||||||
|
value : value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify the current ROM buffer
|
||||||
|
setROM(rom, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command: "setROM",
|
||||||
|
rom : rom,
|
||||||
|
refresh: !!options.refresh,
|
||||||
|
sim : this.sim
|
||||||
|
}, [ rom.buffer ]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specify a new value for a system register
|
||||||
|
setSystemRegister(id, value, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
command: "setSystemRegister",
|
||||||
|
id : id,
|
||||||
|
refresh: !!options.refresh,
|
||||||
|
sim : this.sim,
|
||||||
|
value : value
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ubsubscribe from frame data
|
||||||
|
unsubscribe(key) {
|
||||||
|
return this.core.unsubscribe(key, this.sim);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write multiple bytes to the bus
|
||||||
|
write(address, bytes, options = {}) {
|
||||||
|
return this.core.send({
|
||||||
|
address : address,
|
||||||
|
command : "write",
|
||||||
|
bytes : bytes,
|
||||||
|
debug : !("debug" in options) || !!options.debug,
|
||||||
|
refresh : !!options.refresh,
|
||||||
|
sim : this.sim,
|
||||||
|
subscribe: options.subscribe
|
||||||
|
},
|
||||||
|
bytes instanceof ArrayBuffer ? [ bytes ] :
|
||||||
|
bytes.buffer instanceof ArrayBuffer ? [ bytes.buffer ] :
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
///////////////////////////// Package Methods /////////////////////////////
|
||||||
|
|
||||||
|
// The simulation has been destroyed
|
||||||
|
destroy() {
|
||||||
|
let sim = this.sim;
|
||||||
|
this.core = null;
|
||||||
|
this.sim = 0;
|
||||||
|
return sim;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Sim };
|
|
@ -0,0 +1,74 @@
|
||||||
|
{
|
||||||
|
"id": "en-US",
|
||||||
|
|
||||||
|
"common": {
|
||||||
|
"close" : "Close",
|
||||||
|
"collapse" : "Collapse",
|
||||||
|
"expand" : "Expand",
|
||||||
|
"gotoPrompt": "Enter the address to go to:"
|
||||||
|
},
|
||||||
|
|
||||||
|
"error": {
|
||||||
|
"fileRead": "An error occurred when reading the file.",
|
||||||
|
"romNotVB": "The selected file is not a Virtual Boy ROM."
|
||||||
|
},
|
||||||
|
|
||||||
|
"app": {
|
||||||
|
"title": "Virtual Boy Emulator",
|
||||||
|
|
||||||
|
"menu": {
|
||||||
|
"_": "Application menu bar",
|
||||||
|
|
||||||
|
"file": {
|
||||||
|
"_" : "File",
|
||||||
|
"loadROM" : "Load ROM{sim}...",
|
||||||
|
"debugMode": "Debug mode"
|
||||||
|
},
|
||||||
|
|
||||||
|
"emulation": {
|
||||||
|
"_" : "Emulation",
|
||||||
|
"run" : "Run",
|
||||||
|
"reset" : "Reset",
|
||||||
|
"dualSims": "Dual sims",
|
||||||
|
"linkSims": "Link sims"
|
||||||
|
},
|
||||||
|
|
||||||
|
"debug": {
|
||||||
|
"_" : "Debug{sim}",
|
||||||
|
"console" : "Console",
|
||||||
|
"memory" : "Memory",
|
||||||
|
"cpu" : "CPU",
|
||||||
|
"breakpoints" : "Breakpoints",
|
||||||
|
"palettes" : "Palettes",
|
||||||
|
"characters" : "Characters",
|
||||||
|
"bgMaps" : "BG maps",
|
||||||
|
"objects" : "Objects",
|
||||||
|
"worlds" : "Worlds",
|
||||||
|
"frameBuffers": "Frame buffers"
|
||||||
|
},
|
||||||
|
|
||||||
|
"theme": {
|
||||||
|
"_" : "Theme",
|
||||||
|
"light" : "Light",
|
||||||
|
"dark" : "Dark",
|
||||||
|
"virtual": "Virtual"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
"cpu": {
|
||||||
|
"_" : "CPU{sim}",
|
||||||
|
"float" : "Float",
|
||||||
|
"hex" : "Hex",
|
||||||
|
"signed" : "Signed",
|
||||||
|
"splitHorizontal": "Program and system registers splitter",
|
||||||
|
"splitVertical" : "Disassembler and registers splitter",
|
||||||
|
"unsigned" : "Unsigned"
|
||||||
|
},
|
||||||
|
|
||||||
|
"memory": {
|
||||||
|
"_": "Memory{sim}"
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// Global theme assets
|
||||||
|
Bundle["app/theme/kiosk.css"].installStylesheet(true);
|
||||||
|
await Bundle["app/theme/inconsolata.woff2"].installFont(
|
||||||
|
"Inconsolata SemiExpanded Medium");
|
||||||
|
await Bundle["app/theme/roboto.woff2"].installFont("Roboto");
|
||||||
|
Bundle["app/theme/check.svg" ].installImage("tk-check" , "check.svg" );
|
||||||
|
Bundle["app/theme/close.svg" ].installImage("tk-close" , "close.svg" );
|
||||||
|
Bundle["app/theme/collapse.svg"].installImage("tk-collapse", "collapse.svg");
|
||||||
|
Bundle["app/theme/expand.svg" ].installImage("tk-expand" , "expand.svg" );
|
||||||
|
Bundle["app/theme/radio.svg" ].installImage("tk-radio" , "radio.svg" );
|
||||||
|
Bundle["app/theme/scroll.svg" ].installImage("tk-scroll" , "scroll.svg" );
|
||||||
|
|
||||||
|
// Module imports
|
||||||
|
import { Core } from /**/"./core/Core.js";
|
||||||
|
import { Toolkit } from /**/"./toolkit/Toolkit.js";
|
||||||
|
import { App } from /**/"./app/App.js";
|
||||||
|
|
||||||
|
// Begin application
|
||||||
|
let dark = matchMedia("(prefers-color-scheme: dark)").matches;
|
||||||
|
let url = u=>u.startsWith("data:")?u:new URL(u,import.meta.url).toString();
|
||||||
|
new App({
|
||||||
|
core : await Core.create({ sims: 2 }),
|
||||||
|
locale : navigator.language,
|
||||||
|
standalone: true,
|
||||||
|
theme : dark ? "dark" : "light",
|
||||||
|
locales : [
|
||||||
|
await (await fetch(url(/**/"./locale/en-US.json"))).json()
|
||||||
|
],
|
||||||
|
themes : {
|
||||||
|
dark : Bundle["app/theme/dark.css" ].installStylesheet( dark),
|
||||||
|
light : Bundle["app/theme/light.css" ].installStylesheet(!dark),
|
||||||
|
virtual: Bundle["app/theme/virtual.css"].installStylesheet(false)
|
||||||
|
}
|
||||||
|
});
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458 2.6458" version="1.1">
|
||||||
|
<g>
|
||||||
|
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1.05832,1.653625 1.3229,-1.3229 v 0.66145 l -1.3229,1.3229 -0.79374,-0.79374 v -0.66145 z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 459 B |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
|
||||||
|
<g>
|
||||||
|
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 0.52916666,0.52916665 0.396875,0 0.52916664,0.52916665 0.5291667,-0.52916665 0.396875,0 0,0.396875 L 1.8520833,1.4552083 2.38125,1.984375 l 0,0.396875 -0.396875,0 L 1.4552083,1.8520834 0.92604166,2.38125 l -0.396875,0 0,-0.396875 L 1.0583333,1.4552083 0.52916666,0.92604165 Z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 652 B |
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
|
||||||
|
<g>
|
||||||
|
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1,26 @@
|
||||||
|
: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;
|
||||||
|
}
|
|
@ -0,0 +1,7 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
|
||||||
|
<g>
|
||||||
|
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="1.3229167" y="0.79375005" />
|
||||||
|
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 522 B |
Binary file not shown.
|
@ -0,0 +1,761 @@
|
||||||
|
: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);
|
||||||
|
}
|
|
@ -0,0 +1,26 @@
|
||||||
|
: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;
|
||||||
|
}
|
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458332 2.6458332" version="1.1">
|
||||||
|
<g>
|
||||||
|
<circle style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" cx="1.3229166" cy="1.3229166" r="0.66145831" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 361 B |
Binary file not shown.
|
@ -0,0 +1,6 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
|
||||||
|
<g>
|
||||||
|
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 1.4552083,0.66145833 0.52916666,1.5874999 V 2.2489583 L 1.4552083,1.3229166 2.38125,2.2489583 V 1.5874999 Z" />
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 484 B |
|
@ -0,0 +1,70 @@
|
||||||
|
: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;
|
||||||
|
}
|
|
@ -0,0 +1,387 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,312 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,125 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,748 @@
|
||||||
|
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
|
@ -0,0 +1,145 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,337 @@
|
||||||
|
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 };
|
|
@ -0,0 +1,699 @@
|
||||||
|
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 };
|
195
core/bus.c
195
core/bus.c
|
@ -3,154 +3,121 @@
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/********************************* Constants *********************************/
|
/***************************** Utility Functions *****************************/
|
||||||
|
|
||||||
/* Memory access address masks by data type */
|
/* Read a data unit from a memory buffer */
|
||||||
static const uint32_t TYPE_MASKS[] = {
|
static int32_t busReadBuffer(uint8_t *mem, int type) {
|
||||||
0x07FFFFFF, /* S8 */
|
|
||||||
0x07FFFFFF, /* U8 */
|
|
||||||
0x07FFFFFE, /* S16 */
|
|
||||||
0x07FFFFFE, /* U16 */
|
|
||||||
0x07FFFFFC /* S32 */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/* Little-endian implementation */
|
||||||
|
#ifdef VB_LITTLEENDIAN
|
||||||
/*************************** Sub-Module Functions ****************************/
|
|
||||||
|
|
||||||
/* Read a typed value from a buffer in host memory */
|
|
||||||
static int32_t busReadBuffer(uint8_t *data, int type) {
|
|
||||||
|
|
||||||
/* Processing by data type */
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case VB_S8 : return *(int8_t *)mem;
|
||||||
|
case VB_U8 : return * mem;
|
||||||
|
case VB_S16: return *(int16_t *)mem;
|
||||||
|
case VB_U16: return *(uint16_t *)mem;
|
||||||
|
}
|
||||||
|
return *(int32_t *)mem;
|
||||||
|
|
||||||
/* Generic implementation */
|
/* Generic implementation */
|
||||||
#ifndef VB_LITTLE_ENDIAN
|
|
||||||
case VB_S8 : return ((int8_t *)data)[0];
|
|
||||||
case VB_U8 : return data [0];
|
|
||||||
case VB_S16: return (int32_t) ((int8_t *)data)[1] << 8 | data[0];
|
|
||||||
case VB_U16: return (int32_t) data [1] << 8 | data[0];
|
|
||||||
case VB_S32: return
|
|
||||||
(int32_t) data[3] << 24 | (int32_t) data[2] << 16 |
|
|
||||||
(int32_t) data[1] << 8 | data[0];
|
|
||||||
|
|
||||||
/* Little-endian host */
|
|
||||||
#else
|
#else
|
||||||
case VB_S8 : return *(int8_t *) data;
|
switch (type) {
|
||||||
case VB_U8 : return * data;
|
case VB_S8 : return (int8_t) *mem;
|
||||||
case VB_S16: return *(int16_t *) data;
|
case VB_U8 : return *mem;
|
||||||
case VB_U16: return *(uint16_t *) data;
|
case VB_S16: return (int16_t ) (int8_t) mem[1] << 8 | mem[0];
|
||||||
case VB_S32: return *(int32_t *) data;
|
case VB_U16: return (uint16_t) mem[1] << 8 | mem[0];
|
||||||
|
}
|
||||||
|
return (int32_t ) mem[3] << 24 | (uint32_t) mem[2] << 16 |
|
||||||
|
(uint32_t) mem[1] << 8 | mem[0];
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0; /* Unreachable */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Write a typed value to a buffer in host memory */
|
/* Write a data unit to a memory buffer */
|
||||||
static void busWriteBuffer(uint8_t *data, int type, int32_t value) {
|
static void busWriteBuffer(uint8_t *mem, int type, int32_t value) {
|
||||||
|
|
||||||
/* Processing by data type */
|
/* Little-endian implementation */
|
||||||
|
#ifdef VB_LITTLEENDIAN
|
||||||
switch (type) {
|
switch (type) {
|
||||||
|
case VB_S16: case VB_U16: *(uint16_t *)mem = value; return;
|
||||||
|
case VB_S8 : case VB_U8 : * mem = value; return;
|
||||||
|
}
|
||||||
|
*(int32_t *)mem = value;
|
||||||
|
|
||||||
/* Generic implementation */
|
/* Generic implementation */
|
||||||
#ifndef VB_LITTLE_ENDIAN
|
|
||||||
case VB_S32: data[3] = value >> 24;
|
|
||||||
data[2] = value >> 16; /* Fallthrough */
|
|
||||||
case VB_S16: /* Fallthrough */
|
|
||||||
case VB_U16: data[1] = value >> 8; /* Fallthrough */
|
|
||||||
case VB_S8 : /* Fallthrough */
|
|
||||||
case VB_U8 : data[0] = value;
|
|
||||||
|
|
||||||
/* Little-endian host */
|
|
||||||
#else
|
#else
|
||||||
case VB_S8 : /* Fallthrough */
|
switch (type) {
|
||||||
case VB_U8 : * data = value; return;
|
case VB_S32:
|
||||||
case VB_S16: /* Fallthrough */
|
mem[3] = value >> 24;
|
||||||
case VB_U16: *(uint16_t *) data = value; return;
|
mem[2] = value >> 16;
|
||||||
case VB_S32: *(int32_t *) data = value; return;
|
/* Fallthrough */
|
||||||
|
case VB_S16:
|
||||||
|
case VB_U16:
|
||||||
|
mem[1] = value >> 8;
|
||||||
|
}
|
||||||
|
mem[0] = value;
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************** Library Functions *****************************/
|
/***************************** Module Functions ******************************/
|
||||||
|
|
||||||
/* Read a typed value from the simulation bus */
|
/* Read a data unit from the bus */
|
||||||
static void busRead(VB *sim, uint32_t address, int type, int32_t *value) {
|
static int32_t busRead(VB *sim, uint32_t address, int type, int debug) {
|
||||||
|
|
||||||
/* Working variables */
|
/* Force address alignment */
|
||||||
address &= TYPE_MASKS[type];
|
address &= ~((uint32_t) TYPE_SIZES[type] - 1);
|
||||||
*value = 0;
|
|
||||||
|
|
||||||
/* Process by address range */
|
/* Process by address range */
|
||||||
switch (address >> 24) {
|
switch (address >> 24 & 7) {
|
||||||
case 0: break; /* VIP */
|
case 0 : return 0; /* VIP */
|
||||||
case 1: break; /* VSU */
|
case 1 : return 0 * debug; /* VSU */
|
||||||
case 2: break; /* Misc. I/O */
|
case 2 : return 0; /* Miscellaneous hardware */
|
||||||
case 3: break; /* Unmapped */
|
case 3 : return 0; /* Unmapped */
|
||||||
case 4: break; /* Game Pak expansion */
|
case 4 : return 0; /* Game pak expansion */
|
||||||
|
case 5 : return /* WRAM */
|
||||||
case 5: /* WRAM */
|
busReadBuffer(&sim->wram[address & 0xFFFF], type);
|
||||||
*value = busReadBuffer(&sim->wram[address & 0x0000FFFF], type);
|
case 6 : return sim->cart.sram == NULL ? 0 : /* Game pak RAM */
|
||||||
break;
|
busReadBuffer(&sim->cart.sram
|
||||||
|
[address & (sim->cart.sramSize - 1)], type);
|
||||||
case 6: /* Game Pak RAM */
|
default: return sim->cart.rom == NULL ? 0 : /* Game pak ROM */
|
||||||
if (sim->cart.ram != NULL) {
|
busReadBuffer(&sim->cart.rom
|
||||||
*value = busReadBuffer(
|
[address & (sim->cart.romSize - 1)], type);
|
||||||
&sim->cart.ram[address & sim->cart.ramMask], type);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 7: /* Game Pak ROM */
|
|
||||||
if (sim->cart.rom != NULL) {
|
|
||||||
*value = busReadBuffer(
|
|
||||||
&sim->cart.rom[address & sim->cart.romMask], type);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Write a typed value to the simulation bus */
|
/* Write a data unit to the bus */
|
||||||
static void busWrite(VB*sim,uint32_t address,int type,int32_t value,int debug){
|
static void busWrite(
|
||||||
|
VB *sim, uint32_t address, int type, uint32_t value, int debug) {
|
||||||
|
|
||||||
/* Working variables */
|
/* Force address alignment */
|
||||||
address &= TYPE_MASKS[type];
|
address &= ~((uint32_t) TYPE_SIZES[type] - 1);
|
||||||
|
|
||||||
/* Process by address range */
|
/* Process by address range */
|
||||||
switch (address >> 24) {
|
switch (address >> 24 & 7) {
|
||||||
case 0: break; /* VIP */
|
case 0 : return; /* VIP */
|
||||||
case 1: break; /* VSU */
|
case 1 : return; /* VSU */
|
||||||
case 2: break; /* Misc. I/O */
|
case 2 : return; /* Miscellaneous hardware */
|
||||||
case 3: break; /* Unmapped */
|
case 3 : return; /* Unmapped */
|
||||||
case 4: break; /* Game Pak expansion */
|
case 4 : return; /* Game pak expansion */
|
||||||
|
case 5 : /* WRAM */
|
||||||
case 5: /* WRAM */
|
busWriteBuffer(&sim->wram[address & 0xFFFF], type, value);
|
||||||
busWriteBuffer(&sim->wram[address & 0x0000FFFF], type, value);
|
return;
|
||||||
break;
|
case 6 : /* Cartridge RAM */
|
||||||
|
if (sim->cart.sram != NULL)
|
||||||
case 6: /* Game Pak RAM */
|
busWriteBuffer(&sim->cart.sram
|
||||||
if (sim->cart.ram != NULL) {
|
[address & (sim->cart.sramSize - 1)], type, value);
|
||||||
busWriteBuffer(
|
return;
|
||||||
&sim->cart.ram[address & sim->cart.ramMask], type, value);
|
default: /* Cartridge ROM */
|
||||||
}
|
if (debug && sim->cart.rom != NULL)
|
||||||
break;
|
busWriteBuffer(&sim->cart.rom
|
||||||
|
[address & (sim->cart.romSize - 1)], type, value);
|
||||||
case 7: /* Game Pak ROM */
|
|
||||||
if (debug && sim->cart.rom != NULL) {
|
|
||||||
busWriteBuffer(
|
|
||||||
&sim->cart.rom[address & sim->cart.romMask], type, value);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#endif /* VBAPI */
|
#endif /* VBAPI */
|
||||||
|
|
3214
core/cpu.c
3214
core/cpu.c
File diff suppressed because it is too large
Load Diff
549
core/vb.c
549
core/vb.c
|
@ -1,138 +1,49 @@
|
||||||
#ifndef VBAPI
|
#ifdef VB_EXPORT
|
||||||
#define VBAPI
|
#define VBAPI VB_EXPORT
|
||||||
|
#else
|
||||||
|
#define VBAPI
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
/* Header includes */
|
||||||
#include <float.h>
|
#include <float.h>
|
||||||
#include <vb.h>
|
#include <vb.h>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************** Types ***********************************/
|
/********************************* Constants *********************************/
|
||||||
|
|
||||||
/* Simulation state */
|
/* Type sizes */
|
||||||
struct VB {
|
static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 };
|
||||||
|
|
||||||
/* Game Pak */
|
|
||||||
struct {
|
|
||||||
uint8_t *ram; /* Save RAM */
|
|
||||||
uint8_t *rom; /* Program ROM */
|
|
||||||
uint32_t ramMask; /* Size of SRAM - 1 */
|
|
||||||
uint32_t romMask; /* Size of ROM - 1 */
|
|
||||||
} cart;
|
|
||||||
|
|
||||||
/* CPU */
|
|
||||||
struct {
|
|
||||||
|
|
||||||
/* Cache Control Word */
|
|
||||||
struct {
|
|
||||||
uint8_t ice; /* Instruction Cache Enable */
|
|
||||||
} chcw;
|
|
||||||
|
|
||||||
/* Exception Cause Register */
|
|
||||||
struct {
|
|
||||||
uint16_t eicc; /* Exception/Interrupt Cause Code */
|
|
||||||
uint16_t fecc; /* Fatal Error Cause Code */
|
|
||||||
} ecr;
|
|
||||||
|
|
||||||
/* Program Status Word */
|
|
||||||
struct {
|
|
||||||
uint8_t ae; /* Address Trap Enable */
|
|
||||||
uint8_t cy; /* Carry */
|
|
||||||
uint8_t ep; /* Exception Pending */
|
|
||||||
uint8_t fiv; /* Floating Invalid */
|
|
||||||
uint8_t fov; /* Floating Overflow */
|
|
||||||
uint8_t fpr; /* Floading Precision */
|
|
||||||
uint8_t fro; /* Floating Reserved Operand */
|
|
||||||
uint8_t fud; /* Floading Underflow */
|
|
||||||
uint8_t fzd; /* Floating Zero Divide */
|
|
||||||
uint8_t i; /* Interrupt Level */
|
|
||||||
uint8_t id; /* Interrupt Disable */
|
|
||||||
uint8_t np; /* NMI Pending */
|
|
||||||
uint8_t ov; /* Overflow */
|
|
||||||
uint8_t s; /* Sign */
|
|
||||||
uint8_t z; /* Zero */
|
|
||||||
} psw;
|
|
||||||
|
|
||||||
/* Other registers */
|
|
||||||
uint32_t adtre; /* Address Trap Register for Execution */
|
|
||||||
uint32_t eipc; /* Exception/Interrupt PC */
|
|
||||||
uint32_t eipsw; /* Exception/Interrupt PSW */
|
|
||||||
uint32_t fepc; /* Fatal Error PC */
|
|
||||||
uint32_t fepsw; /* Fatal Error PSW */
|
|
||||||
uint32_t pc; /* Program Counter */
|
|
||||||
int32_t program[32]; /* Program registers */
|
|
||||||
uint32_t sr29; /* System register 29 */
|
|
||||||
uint32_t sr31; /* System register 31 */
|
|
||||||
|
|
||||||
/* Working data */
|
|
||||||
union {
|
|
||||||
struct {
|
|
||||||
uint32_t dest;
|
|
||||||
uint64_t src;
|
|
||||||
} bs; /* Arithmetic bit strings */
|
|
||||||
struct {
|
|
||||||
uint32_t address;
|
|
||||||
int32_t value;
|
|
||||||
} data; /* Data accesses */
|
|
||||||
} aux;
|
|
||||||
|
|
||||||
/* Other state */
|
|
||||||
uint32_t clocks; /* Master clocks to wait */
|
|
||||||
uint16_t code[2]; /* Instruction code units */
|
|
||||||
uint16_t exception; /* Exception cause code */
|
|
||||||
int halt; /* CPU is halting */
|
|
||||||
uint16_t irq; /* Interrupt request lines */
|
|
||||||
int length; /* Instruction code length */
|
|
||||||
uint32_t nextPC; /* Address of next instruction */
|
|
||||||
int operation; /* Current operation ID */
|
|
||||||
int step; /* Operation sub-task ID */
|
|
||||||
} cpu;
|
|
||||||
|
|
||||||
/* Other system state */
|
|
||||||
uint8_t wram[0x10000]; /* System RAM */
|
|
||||||
|
|
||||||
/* Application data */
|
|
||||||
vbOnException onException; /* CPU exception */
|
|
||||||
vbOnExecute onExecute; /* CPU instruction execute */
|
|
||||||
vbOnFetch onFetch; /* CPU instruction fetch */
|
|
||||||
vbOnRead onRead; /* CPU instruction read */
|
|
||||||
vbOnWrite onWrite; /* CPU instruction write */
|
|
||||||
void *tag; /* User data */
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************** Library Functions *****************************/
|
/********************************** Macros ***********************************/
|
||||||
|
|
||||||
/* Sign-extend an integer of variable width */
|
/* Sign-extend a value of some number of bits to 32 bits */
|
||||||
static int32_t SignExtend(int32_t value, int32_t bits) {
|
#define SignExtend(v,b) \
|
||||||
#ifndef VB_SIGNED_PROPAGATE
|
((v) | (((v) & (1 << ((b) - 1))) ? (uint32_t) 0xFFFFFFFF << (b) : 0))
|
||||||
value &= ~((uint32_t) 0xFFFFFFFF << bits);
|
|
||||||
bits = (int32_t) 1 << (bits - (int32_t) 1);
|
|
||||||
return (value ^ bits) - bits;
|
|
||||||
#else
|
|
||||||
return value << (32 - bits) >> (32 - bits);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/******************************** Sub-Modules ********************************/
|
|
||||||
|
|
||||||
|
/*************************** Subsystem Components ****************************/
|
||||||
|
|
||||||
|
/* Component includes */
|
||||||
#include "bus.c"
|
#include "bus.c"
|
||||||
#include "cpu.c"
|
#include "cpu.c"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/***************************** Library Functions *****************************/
|
/***************************** Module Functions ******************************/
|
||||||
|
|
||||||
/* Process a simulation for a given number of clocks */
|
/* Process a simulation for some number of clocks */
|
||||||
static int sysEmulate(VB *sim, uint32_t clocks) {
|
static int sysEmulate(VB *sim, uint32_t clocks) {
|
||||||
return
|
int broke;
|
||||||
cpuEmulate(sim, clocks)
|
broke = cpuEmulate(sim, clocks);
|
||||||
;
|
return broke;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Determine how many clocks are guaranteed to process */
|
/* Determine the number of clocks before a break condititon could occur */
|
||||||
static uint32_t sysUntil(VB *sim, uint32_t clocks) {
|
static uint32_t sysUntil(VB *sim, uint32_t clocks) {
|
||||||
clocks = cpuUntil(sim, clocks);
|
clocks = cpuUntil(sim, clocks);
|
||||||
return clocks;
|
return clocks;
|
||||||
|
@ -140,252 +51,258 @@ static uint32_t sysUntil(VB *sim, uint32_t clocks) {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/******************************* API Commands ********************************/
|
/******************************* API Functions *******************************/
|
||||||
|
|
||||||
|
/* Associate two simulations as peers, or remove an association */
|
||||||
|
void vbConnect(VB *sim1, VB *sim2) {
|
||||||
|
|
||||||
|
/* Disconnect */
|
||||||
|
if (sim2 == NULL) {
|
||||||
|
if (sim1->peer != NULL)
|
||||||
|
sim1->peer->peer = NULL;
|
||||||
|
sim1->peer = NULL;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disconnect any existing link associations */
|
||||||
|
if (sim1->peer != NULL && sim1->peer != sim2)
|
||||||
|
sim1->peer->peer = NULL;
|
||||||
|
if (sim2->peer != NULL && sim2->peer != sim1)
|
||||||
|
sim2->peer->peer = NULL;
|
||||||
|
|
||||||
|
/* Link the two simulations */
|
||||||
|
sim1->peer = sim2;
|
||||||
|
sim2->peer = sim1;
|
||||||
|
}
|
||||||
|
|
||||||
/* Process one simulation */
|
/* Process one simulation */
|
||||||
VBAPI int vbEmulate(VB *sim, uint32_t *clocks) {
|
int vbEmulate(VB *sim, uint32_t *clocks) {
|
||||||
int brk; /* A callback requested a break */
|
int broke; /* The simulation requested an application break */
|
||||||
uint32_t until; /* Clocks guaranteed to process */
|
uint32_t until; /* Maximum clocks before a break could happen */
|
||||||
while (*clocks != 0) {
|
|
||||||
until = sysUntil(sim, *clocks);
|
/* Process the simulation until a break condition occurs */
|
||||||
brk = sysEmulate(sim, until);
|
do {
|
||||||
|
until = *clocks;
|
||||||
|
until = sysUntil (sim, until);
|
||||||
|
broke = sysEmulate(sim, until);
|
||||||
*clocks -= until;
|
*clocks -= until;
|
||||||
if (brk)
|
} while (!broke && *clocks > 0);
|
||||||
return brk; /* TODO: return 1 */
|
|
||||||
}
|
return broke;
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Process multiple simulations */
|
/* Process multiple simulations */
|
||||||
VBAPI int vbEmulateEx(VB **sims, int count, uint32_t *clocks) {
|
int vbEmulateMulti(VB **sims, int count, uint32_t *clocks) {
|
||||||
int brk; /* A callback requested a break */
|
int broke; /* The simulation requested an application break */
|
||||||
uint32_t until; /* Clocks guaranteed to process */
|
uint32_t until; /* Maximum clocks before a break could happen */
|
||||||
int x; /* Iterator */
|
int x; /* Iterator */
|
||||||
while (*clocks != 0) {
|
|
||||||
|
/* Process simulations until a break condition occurs */
|
||||||
|
do {
|
||||||
|
broke = 0;
|
||||||
until = *clocks;
|
until = *clocks;
|
||||||
for (x = count - 1; x >= 0; x--)
|
for (x = 0; x < count; x++)
|
||||||
until = sysUntil(sims[x], until);
|
until = sysUntil (sims[x], until);
|
||||||
|
for (x = 0; x < count; x++)
|
||||||
brk = 0;
|
broke |= sysEmulate(sims[x], until);
|
||||||
for (x = count - 1; x >= 0; x--)
|
|
||||||
brk |= sysEmulate(sims[x], until);
|
|
||||||
|
|
||||||
*clocks -= until;
|
*clocks -= until;
|
||||||
if (brk)
|
} while (!broke && *clocks > 0);
|
||||||
return brk; /* TODO: return 1 */
|
|
||||||
|
return broke;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve a current breakpoint callback */
|
||||||
|
void* vbGetCallback(VB *sim, int type) {
|
||||||
|
void **field; /* Pointer to field within simulation */
|
||||||
|
|
||||||
|
/* Select the field to update */
|
||||||
|
switch (type) {
|
||||||
|
case VB_ONEXCEPTION: field = (void *) &sim->onException; break;
|
||||||
|
case VB_ONEXECUTE : field = (void *) &sim->onExecute ; break;
|
||||||
|
case VB_ONFETCH : field = (void *) &sim->onFetch ; break;
|
||||||
|
case VB_ONREAD : field = (void *) &sim->onRead ; break;
|
||||||
|
case VB_ONWRITE : field = (void *) &sim->onWrite ; break;
|
||||||
|
default: return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve the simulation field */
|
||||||
|
return *field;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve the value of PC */
|
||||||
|
uint32_t vbGetProgramCounter(VB *sim) {
|
||||||
|
return sim->cpu.pc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve the value of a program register */
|
||||||
|
int32_t vbGetProgramRegister(VB *sim, int id) {
|
||||||
|
return id < 1 || id > 31 ? 0 : sim->cpu.program[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve the ROM buffer */
|
||||||
|
void* vbGetROM(VB *sim, uint32_t *size) {
|
||||||
|
if (size != NULL)
|
||||||
|
*size = sim->cart.romSize;
|
||||||
|
return sim->cart.rom;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve the SRAM buffer */
|
||||||
|
void* vbGetSRAM(VB *sim, uint32_t *size) {
|
||||||
|
if (size != NULL)
|
||||||
|
*size = sim->cart.sramSize;
|
||||||
|
return sim->cart.sram;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retrieve the value of a system register */
|
||||||
|
uint32_t vbGetSystemRegister(VB *sim, int id) {
|
||||||
|
switch (id) {
|
||||||
|
case VB_ADTRE: return sim->cpu.adtre;
|
||||||
|
case VB_CHCW : return sim->cpu.chcw.ice << 1;
|
||||||
|
case VB_EIPC : return sim->cpu.eipc;
|
||||||
|
case VB_EIPSW: return sim->cpu.eipsw;
|
||||||
|
case VB_FEPC : return sim->cpu.fepc;
|
||||||
|
case VB_FEPSW: return sim->cpu.fepsw;
|
||||||
|
case VB_PIR : return 0x00005346;
|
||||||
|
case VB_TKCW : return 0x000000E0;
|
||||||
|
case 29 : return sim->cpu.sr29;
|
||||||
|
case 30 : return 0x00000004;
|
||||||
|
case 31 : return sim->cpu.sr31;
|
||||||
|
case VB_ECR : return
|
||||||
|
(uint32_t) sim->cpu.ecr.fecc << 16 | sim->cpu.ecr.eicc;
|
||||||
|
case VB_PSW : return
|
||||||
|
(uint32_t) sim->cpu.psw.i << 16 |
|
||||||
|
(uint32_t) sim->cpu.psw.np << 15 |
|
||||||
|
(uint32_t) sim->cpu.psw.ep << 14 |
|
||||||
|
(uint32_t) sim->cpu.psw.ae << 13 |
|
||||||
|
(uint32_t) sim->cpu.psw.id << 12 |
|
||||||
|
(uint32_t) sim->cpu.psw.fro << 9 |
|
||||||
|
(uint32_t) sim->cpu.psw.fiv << 8 |
|
||||||
|
(uint32_t) sim->cpu.psw.fzd << 7 |
|
||||||
|
(uint32_t) sim->cpu.psw.fov << 6 |
|
||||||
|
(uint32_t) sim->cpu.psw.fud << 5 |
|
||||||
|
(uint32_t) sim->cpu.psw.fpr << 4 |
|
||||||
|
(uint32_t) sim->cpu.psw.cy << 3 |
|
||||||
|
(uint32_t) sim->cpu.psw.ov << 2 |
|
||||||
|
(uint32_t) sim->cpu.psw.s << 1 |
|
||||||
|
(uint32_t) sim->cpu.psw.z
|
||||||
|
;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Retrieve the game pack RAM buffer */
|
/* Prepare a simulation state instance for use */
|
||||||
VBAPI void* vbGetCartRAM(VB *sim, uint32_t *size) {
|
void vbInit(VB *sim) {
|
||||||
if (size != NULL)
|
|
||||||
*size = sim->cart.ram == NULL ? 0 : sim->cart.ramMask + 1;
|
|
||||||
return sim->cart.ram;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the game pack ROM buffer */
|
/* Breakpoint callbacks */
|
||||||
VBAPI void* vbGetCartROM(VB *sim, uint32_t *size) {
|
sim->onException = NULL;
|
||||||
if (size != NULL)
|
|
||||||
*size = sim->cart.rom == NULL ? 0 : sim->cart.romMask + 1;
|
|
||||||
return sim->cart.rom;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the exception callback handle */
|
|
||||||
VBAPI vbOnException vbGetExceptionCallback(VB *sim) {
|
|
||||||
return sim->onException;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the execute callback handle */
|
|
||||||
VBAPI vbOnExecute vbGetExecuteCallback(VB *sim) {
|
|
||||||
return sim->onExecute;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the fetch callback handle */
|
|
||||||
VBAPI vbOnFetch vbGetFetchCallback(VB *sim) {
|
|
||||||
return sim->onFetch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the value of the program counter */
|
|
||||||
VBAPI uint32_t vbGetProgramCounter(VB *sim) {
|
|
||||||
return sim->cpu.pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the value in a program register */
|
|
||||||
VBAPI int32_t vbGetProgramRegister(VB *sim, int index) {
|
|
||||||
return index < 1 || index > 31 ? 0 : sim->cpu.program[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the read callback handle */
|
|
||||||
VBAPI vbOnRead vbGetReadCallback(VB *sim) {
|
|
||||||
return sim->onRead;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the value in a system register */
|
|
||||||
VBAPI uint32_t vbGetSystemRegister(VB *sim, int index) {
|
|
||||||
return index < 0 || index > 31 ? 0 : cpuGetSystemRegister(sim, index);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve a simulation's userdata pointer */
|
|
||||||
VBAPI void* vbGetUserData(VB *sim) {
|
|
||||||
return sim->tag;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Retrieve the write callback handle */
|
|
||||||
VBAPI vbOnWrite vbGetWriteCallback(VB *sim) {
|
|
||||||
return sim->onWrite;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Initialize a simulation instance */
|
|
||||||
VBAPI VB* vbInit(VB *sim) {
|
|
||||||
sim->cart.ram = NULL;
|
|
||||||
sim->cart.rom = NULL;
|
|
||||||
sim->onExecute = NULL;
|
sim->onExecute = NULL;
|
||||||
sim->onFetch = NULL;
|
sim->onFetch = NULL;
|
||||||
sim->onRead = NULL;
|
sim->onRead = NULL;
|
||||||
sim->onWrite = NULL;
|
sim->onWrite = NULL;
|
||||||
|
|
||||||
|
/* System */
|
||||||
|
sim->peer = NULL;
|
||||||
|
|
||||||
|
/* Cartridge */
|
||||||
|
sim->cart.rom = NULL;
|
||||||
|
sim->cart.romSize = 0;
|
||||||
|
sim->cart.sram = NULL;
|
||||||
|
sim->cart.sramSize = 0;
|
||||||
|
|
||||||
|
/* Everything else */
|
||||||
vbReset(sim);
|
vbReset(sim);
|
||||||
return sim;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Read a value from the memory bus */
|
/* Read a data unit from the bus */
|
||||||
VBAPI int32_t vbRead(VB *sim, uint32_t address, int type) {
|
int32_t vbRead(VB *sim, uint32_t address, int type, int debug) {
|
||||||
int32_t value;
|
return type < 0 || type >= (int) sizeof TYPE_SIZES ? 0 :
|
||||||
if (type < 0 || type > 4)
|
busRead(sim, address, type, debug);
|
||||||
return 0;
|
|
||||||
busRead(sim, address, type, &value);
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Simulate a hardware reset */
|
/* Simulate a hardware reset */
|
||||||
VBAPI VB* vbReset(VB *sim) {
|
void vbReset(VB *sim) {
|
||||||
uint32_t x; /* Iterator */
|
uint32_t x; /* Iterator */
|
||||||
|
|
||||||
|
/* Subsystem components */
|
||||||
|
cpuReset(sim);
|
||||||
|
|
||||||
/* WRAM (the hardware does not do this) */
|
/* WRAM (the hardware does not do this) */
|
||||||
for (x = 0; x < 0x10000; x++)
|
for (x = 0; x < 0x10000; x++)
|
||||||
sim->wram[x] = 0x00;
|
sim->wram[x] = 0x00;
|
||||||
|
}
|
||||||
|
|
||||||
/* CPU (normal) */
|
/* Specify a breakpoint callback */
|
||||||
sim->cpu.exception = 0;
|
void* vbSetCallback(VB *sim, int type, void *callback) {
|
||||||
sim->cpu.halt = 0;
|
void **field; /* Pointer to field within simulation */
|
||||||
sim->cpu.irq = 0;
|
void *prev; /* Previous value within field */
|
||||||
sim->cpu.pc = 0xFFFFFFF0;
|
|
||||||
cpuSetSystemRegister(sim, VB_ECR, 0x0000FFF0, 1);
|
|
||||||
cpuSetSystemRegister(sim, VB_PSW, 0x00008000, 1);
|
|
||||||
|
|
||||||
/* CPU (extra, hardware does not do this) */
|
/* Select the field to update */
|
||||||
sim->cpu.adtre = 0x00000000;
|
switch (type) {
|
||||||
sim->cpu.eipc = 0x00000000;
|
case VB_ONEXCEPTION: field = (void *) &sim->onException; break;
|
||||||
sim->cpu.eipsw = 0x00000000;
|
case VB_ONEXECUTE : field = (void *) &sim->onExecute ; break;
|
||||||
sim->cpu.fepc = 0x00000000;
|
case VB_ONFETCH : field = (void *) &sim->onFetch ; break;
|
||||||
sim->cpu.fepsw = 0x00000000;
|
case VB_ONREAD : field = (void *) &sim->onRead ; break;
|
||||||
sim->cpu.sr29 = 0x00000000;
|
case VB_ONWRITE : field = (void *) &sim->onWrite ; break;
|
||||||
sim->cpu.sr31 = 0x00000000;
|
return NULL;
|
||||||
cpuSetSystemRegister(sim, VB_CHCW, 0x00000000, 1);
|
}
|
||||||
for (x = 0; x < 32; x++)
|
|
||||||
sim->cpu.program[x] = 0x00000000;
|
|
||||||
|
|
||||||
/* CPU (other) */
|
/* Update the simulation field */
|
||||||
|
prev = *field;
|
||||||
|
*field = callback;
|
||||||
|
return prev;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Specify a new value for PC */
|
||||||
|
uint32_t vbSetProgramCounter(VB *sim, uint32_t value) {
|
||||||
|
value &= 0xFFFFFFFE;
|
||||||
|
sim->cpu.busWait = 0;
|
||||||
|
sim->cpu.causeCode = 0;
|
||||||
sim->cpu.clocks = 0;
|
sim->cpu.clocks = 0;
|
||||||
sim->cpu.nextPC = 0xFFFFFFF0;
|
sim->cpu.fetch = 0;
|
||||||
sim->cpu.operation = CPU_FETCH;
|
sim->cpu.pc = value;
|
||||||
sim->cpu.step = 0;
|
sim->cpu.state = CPU_FETCH;
|
||||||
|
sim->cpu.substring = 0;
|
||||||
return sim;
|
return value;
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a game pak RAM buffer */
|
|
||||||
VBAPI int vbSetCartRAM(VB *sim, void *sram, uint32_t size) {
|
|
||||||
if (sram != NULL) {
|
|
||||||
if (size < 16 || size > 0x1000000 || (size & (size - 1)) != 0)
|
|
||||||
return 1;
|
|
||||||
sim->cart.ramMask = size - 1;
|
|
||||||
}
|
|
||||||
sim->cart.ram = sram;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a game pak ROM buffer */
|
|
||||||
VBAPI int vbSetCartROM(VB *sim, void *rom, uint32_t size) {
|
|
||||||
if (rom != NULL) {
|
|
||||||
if (size < 16 || size > 0x1000000 || (size & (size - 1)) != 0)
|
|
||||||
return 1;
|
|
||||||
sim->cart.romMask = size - 1;
|
|
||||||
}
|
|
||||||
sim->cart.rom = rom;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a new exception callback handle */
|
|
||||||
VBAPI vbOnException vbSetExceptionCallback(VB *sim, vbOnException callback) {
|
|
||||||
vbOnException prev = sim->onException;
|
|
||||||
sim->onException = callback;
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a new execute callback handle */
|
|
||||||
VBAPI vbOnExecute vbSetExecuteCallback(VB *sim, vbOnExecute callback) {
|
|
||||||
vbOnExecute prev = sim->onExecute;
|
|
||||||
sim->onExecute = callback;
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a new fetch callback handle */
|
|
||||||
VBAPI vbOnFetch vbSetFetchCallback(VB *sim, vbOnFetch callback) {
|
|
||||||
vbOnFetch prev = sim->onFetch;
|
|
||||||
sim->onFetch = callback;
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a new value for the program counter */
|
|
||||||
VBAPI uint32_t vbSetProgramCounter(VB *sim, uint32_t value) {
|
|
||||||
sim->cpu.operation = CPU_FETCH;
|
|
||||||
sim->cpu.pc = sim->cpu.nextPC = value & 0xFFFFFFFE;
|
|
||||||
sim->cpu.step = 0;
|
|
||||||
return sim->cpu.pc;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specify a new value for a program register */
|
/* Specify a new value for a program register */
|
||||||
VBAPI int32_t vbSetProgramRegister(VB *sim, int index, int32_t value) {
|
int32_t vbSetProgramRegister(VB *sim, int id, int32_t value) {
|
||||||
return index < 1 || index > 31 ? 0 : (sim->cpu.program[index] = value);
|
return id < 1 || id > 31 ? 0 : (sim->cpu.program[id] = value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specify a new read callback handle */
|
/* Supply a ROM buffer */
|
||||||
VBAPI vbOnRead vbSetReadCallback(VB *sim, vbOnRead callback) {
|
int vbSetROM(VB *sim, void *rom, uint32_t size) {
|
||||||
vbOnRead prev = sim->onRead;
|
|
||||||
sim->onRead = callback;
|
/* Check the buffer size */
|
||||||
return prev;
|
if (size < 1024 || size > 0x1000000 || ((size - 1) & size) != 0)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
/* Configure the ROM buffer */
|
||||||
|
sim->cart.rom = (uint8_t *) rom;
|
||||||
|
sim->cart.romSize = size;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 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 */
|
/* Specify a new value for a system register */
|
||||||
VBAPI uint32_t vbSetSystemRegister(VB *sim, int index, uint32_t value) {
|
uint32_t vbSetSystemRegister(VB *sim, int id, uint32_t value) {
|
||||||
return index < 0 || index > 31 ? 0 :
|
return cpuSetSystemRegister(sim, id, value, 1);
|
||||||
cpuSetSystemRegister(sim, index, value, 1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Specify a new write callback handle */
|
/* Write a data unit to the bus */
|
||||||
VBAPI vbOnWrite vbSetWriteCallback(VB *sim, vbOnWrite callback) {
|
void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) {
|
||||||
vbOnWrite prev = sim->onWrite;
|
if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES)
|
||||||
sim->onWrite = callback;
|
busWrite(sim, address, type, value, debug);
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Determine the size of a simulation instance */
|
|
||||||
VBAPI size_t vbSizeOf() {
|
|
||||||
return sizeof (VB);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Specify a simulation's userdata pointer */
|
|
||||||
VBAPI void* vbSetUserData(VB *sim, void *tag) {
|
|
||||||
void *prev = sim->tag;
|
|
||||||
sim->tag = tag;
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Write a value to the memory bus */
|
|
||||||
VBAPI int32_t vbWrite(VB *sim, uint32_t address, int type, int32_t value) {
|
|
||||||
if (type < 0 || type > 4)
|
|
||||||
return 0;
|
|
||||||
busWrite(sim, address, type, value, 1);
|
|
||||||
return vbRead(sim, address, type);
|
|
||||||
}
|
}
|
||||||
|
|
197
core/vb.h
197
core/vb.h
|
@ -1,5 +1,5 @@
|
||||||
#ifndef VB_H_
|
#ifndef __VB_H__
|
||||||
#define VB_H_
|
#define __VB_H__
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
extern "C" {
|
extern "C" {
|
||||||
|
@ -17,15 +17,15 @@ extern "C" {
|
||||||
|
|
||||||
/********************************* Constants *********************************/
|
/********************************* Constants *********************************/
|
||||||
|
|
||||||
/* Callback IDs */
|
/* Memory access types */
|
||||||
#define VB_EXCEPTION 0
|
#define VB_CANCEL -1
|
||||||
#define VB_EXECUTE 1
|
#define VB_S8 0
|
||||||
#define VB_FETCH 2
|
#define VB_U8 1
|
||||||
#define VB_FRAME 3
|
#define VB_S16 2
|
||||||
#define VB_READ 4
|
#define VB_U16 3
|
||||||
#define VB_WRITE 5
|
#define VB_S32 4
|
||||||
|
|
||||||
/* System registers */
|
/* System register IDs */
|
||||||
#define VB_ADTRE 25
|
#define VB_ADTRE 25
|
||||||
#define VB_CHCW 24
|
#define VB_CHCW 24
|
||||||
#define VB_ECR 4
|
#define VB_ECR 4
|
||||||
|
@ -37,62 +37,157 @@ extern "C" {
|
||||||
#define VB_PSW 5
|
#define VB_PSW 5
|
||||||
#define VB_TKCW 7
|
#define VB_TKCW 7
|
||||||
|
|
||||||
/* Memory access data types */
|
/* PC types */
|
||||||
#define VB_S8 0
|
#define VB_PC 0
|
||||||
#define VB_U8 1
|
#define VB_PC_FROM 1
|
||||||
#define VB_S16 2
|
#define VB_PC_TO 2
|
||||||
#define VB_U16 3
|
|
||||||
#define VB_S32 4
|
/* Breakpoint callback types */
|
||||||
|
#define VB_ONEXCEPTION 0
|
||||||
|
#define VB_ONEXECUTE 1
|
||||||
|
#define VB_ONFETCH 2
|
||||||
|
#define VB_ONREAD 3
|
||||||
|
#define VB_ONWRITE 4
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*********************************** Types ***********************************/
|
/*********************************** Types ***********************************/
|
||||||
|
|
||||||
/* Simulation state */
|
/* Forward references */
|
||||||
typedef struct VB VB;
|
typedef struct VB VB;
|
||||||
|
|
||||||
/* Callbacks */
|
/* Memory access */
|
||||||
typedef int (*vbOnException)(VB *sim, uint16_t *cause);
|
typedef struct {
|
||||||
typedef int (*vbOnExecute )(VB *sim, uint32_t address, const uint16_t *code, int length);
|
uint32_t address; /* Bus address being accessed */
|
||||||
typedef int (*vbOnFetch )(VB *sim, int fetch, uint32_t address, int32_t *value, uint32_t *cycles);
|
uint32_t clocks; /* Number of clocks required to complete */
|
||||||
typedef int (*vbOnRead )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles);
|
int32_t value; /* Value read (callback's responsibility) or to write */
|
||||||
typedef int (*vbOnWrite )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles, int *cancel);
|
int8_t type; /* Data type of value */
|
||||||
|
} VB_ACCESS;
|
||||||
|
|
||||||
|
/* CPU instruction */
|
||||||
|
typedef struct {
|
||||||
|
|
||||||
|
/* Public fields */
|
||||||
|
uint32_t address; /* Bus address */
|
||||||
|
uint16_t bits[2]; /* Binary instruction code */
|
||||||
|
uint8_t size; /* Size in bytes of the instruction */
|
||||||
|
|
||||||
|
/* Implementation fields */
|
||||||
|
int32_t aux[2]; /* Auxiliary storage for CAXI and bit strings */
|
||||||
|
uint8_t id; /* Internal operation ID */
|
||||||
|
} VB_INSTRUCTION;
|
||||||
|
|
||||||
|
/* Breakpoint callbacks */
|
||||||
|
typedef int (*VB_EXCEPTIONPROC)(VB *, uint16_t);
|
||||||
|
typedef int (*VB_EXECUTEPROC )(VB *, VB_INSTRUCTION *);
|
||||||
|
typedef int (*VB_FETCHPROC )(VB *, int, VB_ACCESS *);
|
||||||
|
typedef int (*VB_READPROC )(VB *, VB_ACCESS *);
|
||||||
|
typedef int (*VB_WRITEPROC )(VB *, VB_ACCESS *);
|
||||||
|
|
||||||
|
/* Simulation state */
|
||||||
|
struct VB {
|
||||||
|
|
||||||
|
/* Game pak */
|
||||||
|
struct {
|
||||||
|
uint8_t *rom; /* Active ROM buffer */
|
||||||
|
uint32_t romSize; /* Size of ROM data */
|
||||||
|
uint8_t *sram; /* Active SRAM buffer */
|
||||||
|
uint32_t sramSize; /* Size of SRAM data */
|
||||||
|
} cart;
|
||||||
|
|
||||||
|
/* CPU */
|
||||||
|
struct {
|
||||||
|
|
||||||
|
/* System registers */
|
||||||
|
uint32_t adtre; /* Address trap register for execution */
|
||||||
|
uint32_t eipc; /* Exception/Interrupt PC */
|
||||||
|
uint32_t eipsw; /* Exception/Interrupt PSW */
|
||||||
|
uint32_t fepc; /* Fatal error PC */
|
||||||
|
uint32_t fepsw; /* Fatal error PSW */
|
||||||
|
uint32_t sr29; /* Unknown system register */
|
||||||
|
uint32_t sr31; /* Unknown system register */
|
||||||
|
|
||||||
|
/* Cache control word */
|
||||||
|
struct {
|
||||||
|
int8_t ice; /* Instruction cache enable */
|
||||||
|
} chcw;
|
||||||
|
|
||||||
|
/* Exception cause register */
|
||||||
|
struct {
|
||||||
|
uint16_t eicc; /* Exception/interrupt cause code */
|
||||||
|
uint16_t fecc; /* Fatal error cause code */
|
||||||
|
} ecr;
|
||||||
|
|
||||||
|
/* Program status word */
|
||||||
|
struct {
|
||||||
|
int8_t ae; /* Address trap enable */
|
||||||
|
int8_t cy; /* Carry */
|
||||||
|
int8_t ep; /* Exception pending */
|
||||||
|
int8_t fiv; /* Floating invalid */
|
||||||
|
int8_t fov; /* Floating overflow */
|
||||||
|
int8_t fpr; /* Floating precision */
|
||||||
|
int8_t fro; /* Floating reserved operand */
|
||||||
|
int8_t fud; /* Floating underflow */
|
||||||
|
int8_t fzd; /* Floating zero divide */
|
||||||
|
int8_t i; /* Interrupt level */
|
||||||
|
int8_t id; /* Interrupt disable */
|
||||||
|
int8_t np; /* NMI pending */
|
||||||
|
int8_t ov; /* Overflow */
|
||||||
|
int8_t s; /* Sign */
|
||||||
|
int8_t z; /* Zero */
|
||||||
|
} psw;
|
||||||
|
|
||||||
|
/* Other registers */
|
||||||
|
uint32_t pc; /* Program counter */
|
||||||
|
int32_t program[32]; /* program registers */
|
||||||
|
|
||||||
|
/* Other fields */
|
||||||
|
VB_ACCESS access; /* Memory access descriptor */
|
||||||
|
VB_INSTRUCTION inst; /* Instruction descriptor */
|
||||||
|
uint8_t irq[5]; /* Interrupt request lines */
|
||||||
|
uint8_t busWait; /* Memory access counter */
|
||||||
|
uint16_t causeCode; /* Exception cause code */
|
||||||
|
uint32_t clocks; /* Clocks until next action */
|
||||||
|
int16_t fetch; /* Index of fetch unit */
|
||||||
|
uint8_t state; /* Operations state */
|
||||||
|
uint8_t substring; /* A bit string operation is in progress */
|
||||||
|
} cpu;
|
||||||
|
|
||||||
|
/* Breakpoint callbacks */
|
||||||
|
VB_EXCEPTIONPROC onException; /* CPU exception */
|
||||||
|
VB_EXECUTEPROC onExecute; /* Instruction execute */
|
||||||
|
VB_FETCHPROC onFetch; /* Instruction fetch */
|
||||||
|
VB_READPROC onRead; /* Memory read */
|
||||||
|
VB_WRITEPROC onWrite; /* Memory write */
|
||||||
|
|
||||||
|
/* Other fields */
|
||||||
|
VB *peer; /* Communications peer */
|
||||||
|
uint8_t wram[0x10000]; /* Main memory */
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/******************************* API Commands ********************************/
|
/******************************* API Commands ********************************/
|
||||||
|
|
||||||
|
VBAPI void vbConnect (VB *sim1, VB *sim2);
|
||||||
VBAPI int vbEmulate (VB *sim, uint32_t *clocks);
|
VBAPI int vbEmulate (VB *sim, uint32_t *clocks);
|
||||||
VBAPI int vbEmulateEx (VB **sims, int count, uint32_t *clocks);
|
VBAPI int vbEmulateMulti (VB **sims, int count, uint32_t *clocks);
|
||||||
VBAPI void* vbGetCallback (VB *sim, int id);
|
VBAPI void* vbGetCallback (VB *sim, int type);
|
||||||
VBAPI void* vbGetCartRAM (VB *sim, uint32_t *size);
|
|
||||||
VBAPI void* vbGetCartROM (VB *sim, uint32_t *size);
|
|
||||||
VBAPI vbOnException vbGetExceptionCallback(VB *sim);
|
|
||||||
VBAPI vbOnExecute vbGetExecuteCallback (VB *sim);
|
|
||||||
VBAPI vbOnFetch vbGetFetchCallback (VB *sim);
|
|
||||||
VBAPI uint32_t vbGetProgramCounter (VB *sim);
|
VBAPI uint32_t vbGetProgramCounter (VB *sim);
|
||||||
VBAPI int32_t vbGetProgramRegister (VB *sim, int index);
|
VBAPI int32_t vbGetProgramRegister (VB *sim, int id);
|
||||||
VBAPI vbOnRead vbGetReadCallback (VB *sim);
|
VBAPI void* vbGetROM (VB *sim, uint32_t *size);
|
||||||
VBAPI uint32_t vbGetSystemRegister (VB *sim, int index);
|
VBAPI void* vbGetSRAM (VB *sim, uint32_t *size);
|
||||||
VBAPI void* vbGetUserData (VB *sim);
|
VBAPI uint32_t vbGetSystemRegister (VB *sim, int id);
|
||||||
VBAPI vbOnWrite vbGetWriteCallback (VB *sim);
|
VBAPI void vbInit (VB *sim);
|
||||||
VBAPI VB* vbInit (VB *sim);
|
VBAPI int32_t vbRead (VB *sim, uint32_t address, int type, int debug);
|
||||||
VBAPI int32_t vbRead (VB *sim, uint32_t address, int type);
|
VBAPI void vbReset (VB *sim);
|
||||||
VBAPI VB* vbReset (VB *sim);
|
VBAPI void* vbSetCallback (VB *sim, int type, void *callback);
|
||||||
VBAPI int vbSetCartRAM (VB *sim, void *sram, uint32_t size);
|
|
||||||
VBAPI int vbSetCartROM (VB *sim, void *rom, uint32_t size);
|
|
||||||
VBAPI vbOnException vbSetExceptionCallback(VB *sim, vbOnException callback);
|
|
||||||
VBAPI vbOnExecute vbSetExecuteCallback (VB *sim, vbOnExecute callback);
|
|
||||||
VBAPI vbOnFetch vbSetFetchCallback (VB *sim, vbOnFetch callback);
|
|
||||||
VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
|
VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
|
||||||
VBAPI int32_t vbSetProgramRegister (VB *sim, int index, int32_t value);
|
VBAPI int32_t vbSetProgramRegister (VB *sim, int id, int32_t value);
|
||||||
VBAPI vbOnRead vbSetReadCallback (VB *sim, vbOnRead callback);
|
VBAPI int vbSetROM (VB *sim, void *rom, uint32_t size);
|
||||||
VBAPI uint32_t vbSetSystemRegister (VB *sim, int index, uint32_t value);
|
VBAPI int vbSetSRAM (VB *sim, void *sram, uint32_t size);
|
||||||
VBAPI void* vbSetUserData (VB *sim, void *tag);
|
VBAPI uint32_t vbSetSystemRegister (VB *sim, int id, uint32_t value);
|
||||||
VBAPI vbOnWrite vbSetWriteCallback (VB *sim, vbOnWrite callback);
|
VBAPI void vbWrite (VB *sim, uint32_t address, int type, int32_t value, int debug);
|
||||||
VBAPI size_t vbSizeOf ();
|
|
||||||
VBAPI int32_t vbWrite (VB *sim, uint32_t address, int type, int32_t value);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -100,4 +195,4 @@ VBAPI int32_t vbWrite (VB *sim, uint32_t address, int type,
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#endif /* VB_H_ */
|
#endif /* __VB_H__ */
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Copyright (C) 2024 Guy Perfect
|
Copyright (C) 2022 Guy Perfect
|
||||||
|
|
||||||
This software is provided 'as-is', without any express or implied
|
This software is provided 'as-is', without any express or implied
|
||||||
warranty. In no event will the authors be held liable for any damages
|
warranty. In no event will the authors be held liable for any damages
|
||||||
|
|
37
makefile
37
makefile
|
@ -1,7 +1,7 @@
|
||||||
.PHONY: help
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@echo
|
@echo
|
||||||
@echo "Virtual Boy Emulator - October 10, 2024"
|
@echo "Virtual Boy Emulator - April 20, 2022"
|
||||||
@echo
|
@echo
|
||||||
@echo "Target build environment is any Debian with the following packages:"
|
@echo "Target build environment is any Debian with the following packages:"
|
||||||
@echo " emscripten"
|
@echo " emscripten"
|
||||||
|
@ -27,35 +27,28 @@ build:
|
||||||
|
|
||||||
.PHONY: bundle
|
.PHONY: bundle
|
||||||
bundle:
|
bundle:
|
||||||
@java web/Bundle.java vbemu
|
@java app/Bundle.java vbemu
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean:
|
clean:
|
||||||
@rm -f vbemu_*.html web/core/core.wasm
|
@rm -f vbemu_*.html app/core/core.wasm
|
||||||
|
|
||||||
.PHONY: core
|
.PHONY: core
|
||||||
core:
|
core:
|
||||||
# GCC generic
|
@gcc core/vb.c -I core \
|
||||||
@gcc core/vb.c -I core -c -o /dev/null \
|
-fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
|
||||||
-Werror -std=c90 -Wall -Wextra -Wpedantic
|
@gcc core/vb.c -I core -D VB_LITTLEENDIAN \
|
||||||
# GCC compilation control
|
-fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
|
||||||
@gcc core/vb.c -I core -c -o /dev/null \
|
@emcc core/vb.c -I core \
|
||||||
-Werror -std=c90 -Wall -Wextra -Wpedantic \
|
-fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
|
||||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE -D VB_DIV_GENERIC
|
@emcc core/vb.c -I core -D VB_LITTLEENDIAN \
|
||||||
# Clang generic
|
-fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
|
||||||
@emcc core/vb.c -I core -c -o /dev/null \
|
|
||||||
-Werror -std=c90 -Wall -Wextra -Wpedantic
|
|
||||||
# Clang compilation control
|
|
||||||
@emcc core/vb.c -I core -c -o /dev/null \
|
|
||||||
-Werror -std=c90 -Wall -Wextra -Wpedantic \
|
|
||||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE -D VB_DIV_GENERIC
|
|
||||||
|
|
||||||
.PHONY: wasm
|
.PHONY: wasm
|
||||||
wasm:
|
wasm:
|
||||||
@emcc -o web/core/core.wasm web/core/wasm.c core/vb.c -Icore \
|
@emcc -o app/core/core.wasm wasm/wasm.c core/vb.c -Icore \
|
||||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE \
|
-D VB_LITTLEENDIAN --no-entry -O2 -flto -s WASM=1 \
|
||||||
-D "VBAPI=__attribute__((used))" \
|
-D "VB_EXPORT=__attribute__((used))" \
|
||||||
--no-entry -O2 -flto -s WASM=1 \
|
|
||||||
-s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \
|
-s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \
|
||||||
-s MAXIMUM_MEMORY=4GB -fno-strict-aliasing
|
-s MAXIMUM_MEMORY=4GB -fno-strict-aliasing
|
||||||
@rm -f web/core/*.wasm.tmp*
|
@rm -f app/core/*.wasm.tmp*
|
||||||
|
|
|
@ -0,0 +1,126 @@
|
||||||
|
#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);
|
||||||
|
}
|
Loading…
Reference in New Issue