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 }; | ||||
							
								
								
									
										211
									
								
								core/bus.c
								
								
								
								
							
							
						
						
									
										211
									
								
								core/bus.c
								
								
								
								
							|  | @ -3,154 +3,121 @@ | |||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /********************************* Constants *********************************/ | ||||
| /***************************** Utility Functions *****************************/ | ||||
| 
 | ||||
| /* Memory access address masks by data type */ | ||||
| static const uint32_t TYPE_MASKS[] = { | ||||
|     0x07FFFFFF, /* S8  */ | ||||
|     0x07FFFFFF, /* U8  */ | ||||
|     0x07FFFFFE, /* S16 */ | ||||
|     0x07FFFFFE, /* U16 */ | ||||
|     0x07FFFFFC  /* S32 */ | ||||
| }; | ||||
| /* Read a data unit from a memory buffer */ | ||||
| static int32_t busReadBuffer(uint8_t *mem, int type) { | ||||
| 
 | ||||
|     /* Little-endian implementation */ | ||||
|     #ifdef VB_LITTLEENDIAN | ||||
|         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 */ | ||||
|     #else | ||||
|         switch (type) { | ||||
|             case VB_S8 : return (int8_t) *mem; | ||||
|             case VB_U8 : return          *mem; | ||||
|             case VB_S16: return (int16_t ) (int8_t) mem[1] <<  8 | mem[0]; | ||||
|             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 | ||||
| 
 | ||||
| /*************************** 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) { | ||||
| 
 | ||||
|         /* 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 | ||||
|             case VB_S8 : return *(int8_t   *) data; | ||||
|             case VB_U8 : return *             data; | ||||
|             case VB_S16: return *(int16_t  *) data; | ||||
|             case VB_U16: return *(uint16_t *) data; | ||||
|             case VB_S32: return *(int32_t  *) data; | ||||
|         #endif | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|     return 0; /* Unreachable */ | ||||
| } | ||||
| 
 | ||||
| /* Write a typed value to a buffer in host memory */ | ||||
| static void busWriteBuffer(uint8_t *data, int type, int32_t value) { | ||||
| /* Write a data unit to a memory buffer */ | ||||
| static void busWriteBuffer(uint8_t *mem, int type, int32_t value) { | ||||
| 
 | ||||
|     /* Processing by data type */ | ||||
|     switch (type) { | ||||
|     /* Little-endian implementation */ | ||||
|     #ifdef VB_LITTLEENDIAN | ||||
|         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 */ | ||||
|         #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 | ||||
|             case VB_S8 : /* Fallthrough */ | ||||
|             case VB_U8 : *             data = value; return; | ||||
|             case VB_S16: /* Fallthrough */ | ||||
|             case VB_U16: *(uint16_t *) data = value; return; | ||||
|             case VB_S32: *(int32_t  *) data = value; return; | ||||
|         #endif | ||||
| 
 | ||||
|     } | ||||
|     /* Generic implementation */ | ||||
|     #else | ||||
|         switch (type) { | ||||
|             case VB_S32: | ||||
|                 mem[3] = value >> 24; | ||||
|                 mem[2] = value >> 16; | ||||
|                 /* Fallthrough */ | ||||
|             case VB_S16: | ||||
|             case VB_U16: | ||||
|                 mem[1] = value >>  8; | ||||
|         } | ||||
|         mem[0] = value; | ||||
|     #endif | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /***************************** Library Functions *****************************/ | ||||
| /***************************** Module Functions ******************************/ | ||||
| 
 | ||||
| /* Read a typed value from the simulation bus */ | ||||
| static void busRead(VB *sim, uint32_t address, int type, int32_t *value) { | ||||
| /* Read a data unit from the bus */ | ||||
| static int32_t busRead(VB *sim, uint32_t address, int type, int debug) { | ||||
| 
 | ||||
|     /* Working variables */ | ||||
|     address &= TYPE_MASKS[type]; | ||||
|     *value   = 0; | ||||
|     /* Force address alignment */ | ||||
|     address &= ~((uint32_t) TYPE_SIZES[type] - 1); | ||||
| 
 | ||||
|     /* Process by address range */ | ||||
|     switch (address >> 24) { | ||||
|         case 0: break; /* VIP */ | ||||
|         case 1: break; /* VSU */ | ||||
|         case 2: break; /* Misc. I/O */ | ||||
|         case 3: break; /* Unmapped */ | ||||
|         case 4: break; /* Game Pak expansion */ | ||||
| 
 | ||||
|         case 5: /* WRAM */ | ||||
|             *value = busReadBuffer(&sim->wram[address & 0x0000FFFF], type); | ||||
|             break; | ||||
| 
 | ||||
|         case 6: /* Game Pak RAM */ | ||||
|             if (sim->cart.ram != NULL) { | ||||
|                 *value = busReadBuffer( | ||||
|                     &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; | ||||
| 
 | ||||
|     switch (address >> 24 & 7) { | ||||
|         case 0 : return 0; /* VIP */ | ||||
|         case 1 : return 0 * debug; /* VSU */ | ||||
|         case 2 : return 0; /* Miscellaneous hardware */ | ||||
|         case 3 : return 0; /* Unmapped */ | ||||
|         case 4 : return 0; /* Game pak expansion */ | ||||
|         case 5 : return /* WRAM */ | ||||
|             busReadBuffer(&sim->wram[address & 0xFFFF], type); | ||||
|         case 6 : return sim->cart.sram == NULL ? 0 : /* Game pak RAM */ | ||||
|             busReadBuffer(&sim->cart.sram | ||||
|                 [address & (sim->cart.sramSize - 1)], type); | ||||
|         default: return sim->cart.rom  == NULL ? 0 : /* Game pak ROM */ | ||||
|             busReadBuffer(&sim->cart.rom | ||||
|                 [address & (sim->cart.romSize  - 1)], type); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| /* Write a typed value to the simulation bus */ | ||||
| static void busWrite(VB*sim,uint32_t address,int type,int32_t value,int debug){ | ||||
| /* Write a data unit to the bus */ | ||||
| static void busWrite( | ||||
|     VB *sim, uint32_t address, int type, uint32_t value, int debug) { | ||||
| 
 | ||||
|     /* Working variables */ | ||||
|     address &= TYPE_MASKS[type]; | ||||
|     /* Force address alignment */ | ||||
|     address &= ~((uint32_t) TYPE_SIZES[type] - 1); | ||||
| 
 | ||||
|     /* Process by address range */ | ||||
|     switch (address >> 24) { | ||||
|         case 0: break; /* VIP */ | ||||
|         case 1: break; /* VSU */ | ||||
|         case 2: break; /* Misc. I/O */ | ||||
|         case 3: break; /* Unmapped */ | ||||
|         case 4: break; /* Game Pak expansion */ | ||||
| 
 | ||||
|         case 5: /* WRAM */ | ||||
|             busWriteBuffer(&sim->wram[address & 0x0000FFFF], type, value); | ||||
|             break; | ||||
| 
 | ||||
|         case 6: /* Game Pak RAM */ | ||||
|             if (sim->cart.ram != NULL) { | ||||
|                 busWriteBuffer( | ||||
|                     &sim->cart.ram[address & sim->cart.ramMask], type, value); | ||||
|             } | ||||
|             break; | ||||
| 
 | ||||
|         case 7: /* Game Pak ROM */ | ||||
|             if (debug && sim->cart.rom != NULL) { | ||||
|                 busWriteBuffer( | ||||
|                     &sim->cart.rom[address & sim->cart.romMask], type, value); | ||||
|             } | ||||
|             break; | ||||
| 
 | ||||
|     switch (address >> 24 & 7) { | ||||
|         case 0 : return; /* VIP */ | ||||
|         case 1 : return; /* VSU */ | ||||
|         case 2 : return; /* Miscellaneous hardware */ | ||||
|         case 3 : return; /* Unmapped */ | ||||
|         case 4 : return; /* Game pak expansion */ | ||||
|         case 5 : /* WRAM */ | ||||
|             busWriteBuffer(&sim->wram[address & 0xFFFF], type, value); | ||||
|             return; | ||||
|         case 6 : /* Cartridge RAM */ | ||||
|             if (sim->cart.sram != NULL) | ||||
|                 busWriteBuffer(&sim->cart.sram | ||||
|                     [address & (sim->cart.sramSize - 1)], type, value); | ||||
|             return; | ||||
|         default: /* Cartridge ROM */ | ||||
|             if (debug && sim->cart.rom != NULL) | ||||
|                 busWriteBuffer(&sim->cart.rom | ||||
|                     [address & (sim->cart.romSize - 1)], type, value); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| #endif /* VBAPI */ | ||||
|  |  | |||
							
								
								
									
										3272
									
								
								core/cpu.c
								
								
								
								
							
							
						
						
									
										3272
									
								
								core/cpu.c
								
								
								
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										553
									
								
								core/vb.c
								
								
								
								
							
							
						
						
									
										553
									
								
								core/vb.c
								
								
								
								
							|  | @ -1,138 +1,49 @@ | |||
| #ifndef VBAPI | ||||
| #define VBAPI | ||||
| #ifdef VB_EXPORT | ||||
|     #define VBAPI VB_EXPORT | ||||
| #else | ||||
|     #define VBAPI | ||||
| #endif | ||||
| 
 | ||||
| /* Header includes */ | ||||
| #include <float.h> | ||||
| #include <vb.h> | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /*********************************** Types ***********************************/ | ||||
| /********************************* Constants *********************************/ | ||||
| 
 | ||||
| /* Simulation state */ | ||||
| struct VB { | ||||
| 
 | ||||
|     /* 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 */ | ||||
| }; | ||||
| /* Type sizes */ | ||||
| static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 }; | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /***************************** Library Functions *****************************/ | ||||
| /********************************** Macros ***********************************/ | ||||
| 
 | ||||
| /* Sign-extend an integer of variable width */ | ||||
| static int32_t SignExtend(int32_t value, int32_t bits) { | ||||
|     #ifndef VB_SIGNED_PROPAGATE | ||||
|         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 | ||||
| } | ||||
| /* Sign-extend a value of some number of bits to 32 bits */ | ||||
| #define SignExtend(v,b) \ | ||||
|     ((v) | (((v) & (1 << ((b) - 1))) ? (uint32_t) 0xFFFFFFFF << (b) : 0)) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /******************************** Sub-Modules ********************************/ | ||||
| 
 | ||||
| /*************************** Subsystem Components ****************************/ | ||||
| 
 | ||||
| /* Component includes */ | ||||
| #include "bus.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) { | ||||
|     return | ||||
|         cpuEmulate(sim, clocks) | ||||
|     ; | ||||
|     int broke; | ||||
|     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) { | ||||
|     clocks = cpuUntil(sim, 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 */ | ||||
| VBAPI int vbEmulate(VB *sim, uint32_t *clocks) { | ||||
|     int      brk;   /* A callback requested a break */ | ||||
|     uint32_t until; /* Clocks guaranteed to process */ | ||||
|     while (*clocks != 0) { | ||||
|         until    = sysUntil(sim, *clocks); | ||||
|         brk      = sysEmulate(sim, until); | ||||
|         *clocks -= until; | ||||
|         if (brk) | ||||
|             return brk; /* TODO: return 1 */ | ||||
|     } | ||||
|     return 0; | ||||
| int vbEmulate(VB *sim, uint32_t *clocks) { | ||||
|     int      broke; /* The simulation requested an application break */ | ||||
|     uint32_t until; /* Maximum clocks before a break could happen */ | ||||
| 
 | ||||
|     /* Process the simulation until a break condition occurs */ | ||||
|     do { | ||||
|         until   = *clocks; | ||||
|         until   = sysUntil  (sim, until); | ||||
|         broke   = sysEmulate(sim, until); | ||||
|        *clocks -= until; | ||||
|     } while (!broke && *clocks > 0); | ||||
| 
 | ||||
|     return broke; | ||||
| } | ||||
| 
 | ||||
| /* Process multiple simulations */ | ||||
| VBAPI int vbEmulateEx(VB **sims, int count, uint32_t *clocks) { | ||||
|     int      brk;   /* A callback requested a break */ | ||||
|     uint32_t until; /* Clocks guaranteed to process */ | ||||
| int vbEmulateMulti(VB **sims, int count, uint32_t *clocks) { | ||||
|     int      broke; /* The simulation requested an application break */ | ||||
|     uint32_t until; /* Maximum clocks before a break could happen */ | ||||
|     int      x;     /* Iterator */ | ||||
|     while (*clocks != 0) { | ||||
| 
 | ||||
|     /* Process simulations until a break condition occurs */ | ||||
|     do { | ||||
|         broke = 0; | ||||
|         until = *clocks; | ||||
|         for (x = count - 1; x >= 0; x--) | ||||
|             until = sysUntil(sims[x], until); | ||||
| 
 | ||||
|         brk = 0; | ||||
|         for (x = count - 1; x >= 0; x--) | ||||
|             brk |= sysEmulate(sims[x], until); | ||||
| 
 | ||||
|         for (x = 0; x < count; x++) | ||||
|             until  = sysUntil  (sims[x], until); | ||||
|         for (x = 0; x < count; x++) | ||||
|             broke |= sysEmulate(sims[x], until); | ||||
|         *clocks -= until; | ||||
|         if (brk) | ||||
|             return brk; /* TODO: return 1 */ | ||||
|     } while (!broke && *clocks > 0); | ||||
| 
 | ||||
|     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; | ||||
| } | ||||
| 
 | ||||
| /* Retrieve the game pack RAM buffer */ | ||||
| VBAPI void* vbGetCartRAM(VB *sim, uint32_t *size) { | ||||
|     if (size != NULL) | ||||
|         *size = sim->cart.ram == NULL ? 0 : sim->cart.ramMask + 1; | ||||
|     return sim->cart.ram; | ||||
| } | ||||
| /* Prepare a simulation state instance for use */ | ||||
| void vbInit(VB *sim) { | ||||
| 
 | ||||
| /* Retrieve the game pack ROM buffer */ | ||||
| VBAPI void* vbGetCartROM(VB *sim, uint32_t *size) { | ||||
|     if (size != NULL) | ||||
|         *size = sim->cart.rom == NULL ? 0 : sim->cart.romMask + 1; | ||||
|     return sim->cart.rom; | ||||
| } | ||||
|     /* Breakpoint callbacks */ | ||||
|     sim->onException = NULL; | ||||
|     sim->onExecute   = NULL; | ||||
|     sim->onFetch     = NULL; | ||||
|     sim->onRead      = NULL; | ||||
|     sim->onWrite     = NULL; | ||||
| 
 | ||||
| /* Retrieve the exception callback handle */ | ||||
| VBAPI vbOnException vbGetExceptionCallback(VB *sim) { | ||||
|     return sim->onException; | ||||
| } | ||||
|     /* System */ | ||||
|     sim->peer = NULL; | ||||
| 
 | ||||
| /* 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; | ||||
|     /* Cartridge */ | ||||
|     sim->cart.rom      = NULL; | ||||
|     sim->onExecute     = NULL; | ||||
|     sim->onFetch       = NULL; | ||||
|     sim->onRead        = NULL; | ||||
|     sim->onWrite       = NULL; | ||||
|     sim->cart.romSize  = 0; | ||||
|     sim->cart.sram     = NULL; | ||||
|     sim->cart.sramSize = 0; | ||||
| 
 | ||||
|     /* Everything else */ | ||||
|     vbReset(sim); | ||||
|     return sim; | ||||
| } | ||||
| 
 | ||||
| /* Read a value from the memory bus */ | ||||
| VBAPI int32_t vbRead(VB *sim, uint32_t address, int type) { | ||||
|     int32_t value; | ||||
|     if (type < 0 || type > 4) | ||||
|         return 0; | ||||
|     busRead(sim, address, type, &value); | ||||
|     return value; | ||||
| /* Read a data unit from the bus */ | ||||
| int32_t vbRead(VB *sim, uint32_t address, int type, int debug) { | ||||
|     return type < 0 || type >= (int) sizeof TYPE_SIZES ? 0 : | ||||
|         busRead(sim, address, type, debug); | ||||
| } | ||||
| 
 | ||||
| /* Simulate a hardware reset */ | ||||
| VBAPI VB* vbReset(VB *sim) { | ||||
| void vbReset(VB *sim) { | ||||
|     uint32_t x; /* Iterator */ | ||||
| 
 | ||||
|     /* Subsystem components */ | ||||
|     cpuReset(sim); | ||||
| 
 | ||||
|     /* WRAM (the hardware does not do this) */ | ||||
|     for (x = 0; x < 0x10000; x++) | ||||
|         sim->wram[x] = 0x00; | ||||
| } | ||||
| 
 | ||||
|     /* CPU (normal) */ | ||||
|     sim->cpu.exception = 0; | ||||
|     sim->cpu.halt      = 0; | ||||
|     sim->cpu.irq       = 0; | ||||
|     sim->cpu.pc        = 0xFFFFFFF0; | ||||
|     cpuSetSystemRegister(sim, VB_ECR, 0x0000FFF0, 1); | ||||
|     cpuSetSystemRegister(sim, VB_PSW, 0x00008000, 1); | ||||
| /* Specify a breakpoint callback */ | ||||
| void* vbSetCallback(VB *sim, int type, void *callback) { | ||||
|     void **field; /* Pointer to field within simulation */ | ||||
|     void  *prev;  /* Previous value within field */ | ||||
| 
 | ||||
|     /* CPU (extra, hardware does not do this) */ | ||||
|     sim->cpu.adtre = 0x00000000; | ||||
|     sim->cpu.eipc  = 0x00000000; | ||||
|     sim->cpu.eipsw = 0x00000000; | ||||
|     sim->cpu.fepc  = 0x00000000; | ||||
|     sim->cpu.fepsw = 0x00000000; | ||||
|     sim->cpu.sr29  = 0x00000000; | ||||
|     sim->cpu.sr31  = 0x00000000; | ||||
|     cpuSetSystemRegister(sim, VB_CHCW, 0x00000000, 1); | ||||
|     for (x = 0; x < 32; x++) | ||||
|         sim->cpu.program[x] = 0x00000000; | ||||
|     /* 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; | ||||
|         return NULL; | ||||
|     } | ||||
| 
 | ||||
|     /* 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.nextPC    = 0xFFFFFFF0; | ||||
|     sim->cpu.operation = CPU_FETCH; | ||||
|     sim->cpu.step      = 0; | ||||
| 
 | ||||
|     return sim; | ||||
| } | ||||
| 
 | ||||
| /* 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; | ||||
|     sim->cpu.fetch     = 0; | ||||
|     sim->cpu.pc        = value; | ||||
|     sim->cpu.state     = CPU_FETCH; | ||||
|     sim->cpu.substring = 0; | ||||
|     return value; | ||||
| } | ||||
| 
 | ||||
| /* Specify a new value for a program register */ | ||||
| VBAPI int32_t vbSetProgramRegister(VB *sim, int index, int32_t value) { | ||||
|     return index < 1 || index > 31 ? 0 : (sim->cpu.program[index] = value); | ||||
| int32_t vbSetProgramRegister(VB *sim, int id, int32_t value) { | ||||
|     return id < 1 || id > 31 ? 0 : (sim->cpu.program[id] = value); | ||||
| } | ||||
| 
 | ||||
| /* Specify a new read callback handle */ | ||||
| VBAPI vbOnRead vbSetReadCallback(VB *sim, vbOnRead callback) { | ||||
|     vbOnRead prev = sim->onRead; | ||||
|     sim->onRead = callback; | ||||
|     return prev; | ||||
| /* Supply a ROM buffer */ | ||||
| int vbSetROM(VB *sim, void *rom, uint32_t size) { | ||||
| 
 | ||||
|     /* Check the buffer size */ | ||||
|     if (size < 1024 || size > 0x1000000 || ((size - 1) & size) != 0) | ||||
|         return 0; | ||||
| 
 | ||||
|     /* Configure the ROM buffer */ | ||||
|     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 */ | ||||
| VBAPI uint32_t vbSetSystemRegister(VB *sim, int index, uint32_t value) { | ||||
|     return index < 0 || index > 31 ? 0 : | ||||
|         cpuSetSystemRegister(sim, index, value, 1); | ||||
| uint32_t vbSetSystemRegister(VB *sim, int id, uint32_t value) { | ||||
|     return cpuSetSystemRegister(sim, id, value, 1); | ||||
| } | ||||
| 
 | ||||
| /* Specify a new write callback handle */ | ||||
| VBAPI vbOnWrite vbSetWriteCallback(VB *sim, vbOnWrite callback) { | ||||
|     vbOnWrite prev = sim->onWrite; | ||||
|     sim->onWrite = callback; | ||||
|     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); | ||||
| /* Write a data unit to the bus */ | ||||
| void vbWrite(VB *sim, uint32_t address, int type, int32_t value, int debug) { | ||||
|     if (type >= 0 && type < (int32_t) sizeof TYPE_SIZES) | ||||
|         busWrite(sim, address, type, value, debug); | ||||
| } | ||||
|  |  | |||
							
								
								
									
										203
									
								
								core/vb.h
								
								
								
								
							
							
						
						
									
										203
									
								
								core/vb.h
								
								
								
								
							|  | @ -1,5 +1,5 @@ | |||
| #ifndef VB_H_ | ||||
| #define VB_H_ | ||||
| #ifndef __VB_H__ | ||||
| #define __VB_H__ | ||||
| 
 | ||||
| #ifdef __cplusplus | ||||
| extern "C" { | ||||
|  | @ -17,15 +17,15 @@ extern "C" { | |||
| 
 | ||||
| /********************************* Constants *********************************/ | ||||
| 
 | ||||
| /* Callback IDs */ | ||||
| #define VB_EXCEPTION 0 | ||||
| #define VB_EXECUTE   1 | ||||
| #define VB_FETCH     2 | ||||
| #define VB_FRAME     3 | ||||
| #define VB_READ      4 | ||||
| #define VB_WRITE     5 | ||||
| /* Memory access types */ | ||||
| #define VB_CANCEL -1 | ||||
| #define VB_S8      0 | ||||
| #define VB_U8      1 | ||||
| #define VB_S16     2 | ||||
| #define VB_U16     3 | ||||
| #define VB_S32     4 | ||||
| 
 | ||||
| /* System registers */ | ||||
| /* System register IDs */ | ||||
| #define VB_ADTRE 25 | ||||
| #define VB_CHCW  24 | ||||
| #define VB_ECR    4 | ||||
|  | @ -37,62 +37,157 @@ extern "C" { | |||
| #define VB_PSW    5 | ||||
| #define VB_TKCW   7 | ||||
| 
 | ||||
| /* Memory access data types */ | ||||
| #define VB_S8  0 | ||||
| #define VB_U8  1 | ||||
| #define VB_S16 2 | ||||
| #define VB_U16 3 | ||||
| #define VB_S32 4 | ||||
| /* PC types */ | ||||
| #define VB_PC      0 | ||||
| #define VB_PC_FROM 1 | ||||
| #define VB_PC_TO   2 | ||||
| 
 | ||||
| /* Breakpoint callback types */ | ||||
| #define VB_ONEXCEPTION 0 | ||||
| #define VB_ONEXECUTE   1 | ||||
| #define VB_ONFETCH     2 | ||||
| #define VB_ONREAD      3 | ||||
| #define VB_ONWRITE     4 | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| /*********************************** Types ***********************************/ | ||||
| 
 | ||||
| /* Simulation state */ | ||||
| /* Forward references */ | ||||
| typedef struct VB VB; | ||||
| 
 | ||||
| /* Callbacks */ | ||||
| typedef int (*vbOnException)(VB *sim, uint16_t *cause); | ||||
| typedef int (*vbOnExecute  )(VB *sim, uint32_t address, const uint16_t *code, int length); | ||||
| typedef int (*vbOnFetch    )(VB *sim, int fetch, uint32_t address, int32_t *value, uint32_t *cycles); | ||||
| typedef int (*vbOnRead     )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles); | ||||
| typedef int (*vbOnWrite    )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles, int *cancel); | ||||
| /* Memory access */ | ||||
| typedef struct { | ||||
|     uint32_t address; /* Bus address being accessed */ | ||||
|     uint32_t clocks;  /* Number of clocks required to complete */ | ||||
|     int32_t  value;   /* Value read (callback's responsibility) or to write */ | ||||
|     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 ********************************/ | ||||
| 
 | ||||
| VBAPI int           vbEmulate             (VB *sim, uint32_t *clocks); | ||||
| VBAPI int           vbEmulateEx           (VB **sims, int count, uint32_t *clocks); | ||||
| VBAPI void*         vbGetCallback         (VB *sim, int id); | ||||
| 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 int32_t       vbGetProgramRegister  (VB *sim, int index); | ||||
| VBAPI vbOnRead      vbGetReadCallback     (VB *sim); | ||||
| VBAPI uint32_t      vbGetSystemRegister   (VB *sim, int index); | ||||
| VBAPI void*         vbGetUserData         (VB *sim); | ||||
| VBAPI vbOnWrite     vbGetWriteCallback    (VB *sim); | ||||
| VBAPI VB*           vbInit                (VB *sim); | ||||
| VBAPI int32_t       vbRead                (VB *sim, uint32_t address, int type); | ||||
| VBAPI VB*           vbReset               (VB *sim); | ||||
| 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 int32_t       vbSetProgramRegister  (VB *sim, int index, int32_t value); | ||||
| VBAPI vbOnRead      vbSetReadCallback     (VB *sim, vbOnRead callback); | ||||
| VBAPI uint32_t      vbSetSystemRegister   (VB *sim, int index, uint32_t value); | ||||
| VBAPI void*         vbSetUserData         (VB *sim, void *tag); | ||||
| VBAPI vbOnWrite     vbSetWriteCallback    (VB *sim, vbOnWrite callback); | ||||
| VBAPI size_t        vbSizeOf              (); | ||||
| VBAPI int32_t       vbWrite               (VB *sim, uint32_t address, int type, int32_t value); | ||||
| VBAPI void     vbConnect            (VB *sim1, VB *sim2); | ||||
| VBAPI int      vbEmulate            (VB *sim, uint32_t *clocks); | ||||
| VBAPI int      vbEmulateMulti       (VB **sims, int count, uint32_t *clocks); | ||||
| VBAPI void*    vbGetCallback        (VB *sim, int type); | ||||
| VBAPI uint32_t vbGetProgramCounter  (VB *sim); | ||||
| VBAPI int32_t  vbGetProgramRegister (VB *sim, int id); | ||||
| VBAPI void*    vbGetROM             (VB *sim, uint32_t *size); | ||||
| VBAPI void*    vbGetSRAM            (VB *sim, uint32_t *size); | ||||
| VBAPI uint32_t vbGetSystemRegister  (VB *sim, int id); | ||||
| VBAPI void     vbInit               (VB *sim); | ||||
| VBAPI int32_t  vbRead               (VB *sim, uint32_t address, int type, int debug); | ||||
| VBAPI void     vbReset              (VB *sim); | ||||
| VBAPI void*    vbSetCallback        (VB *sim, int type, void *callback); | ||||
| VBAPI uint32_t vbSetProgramCounter  (VB *sim, uint32_t value); | ||||
| VBAPI int32_t  vbSetProgramRegister (VB *sim, int id, int32_t value); | ||||
| VBAPI int      vbSetROM             (VB *sim, void *rom, uint32_t size); | ||||
| VBAPI int      vbSetSRAM            (VB *sim, void *sram, uint32_t size); | ||||
| VBAPI uint32_t vbSetSystemRegister  (VB *sim, int id, uint32_t value); | ||||
| VBAPI void     vbWrite              (VB *sim, uint32_t address, int type, int32_t value, int debug); | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -100,4 +195,4 @@ VBAPI int32_t       vbWrite               (VB *sim, uint32_t address, int type, | |||
| } | ||||
| #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 | ||||
| warranty.  In no event will the authors be held liable for any damages | ||||
|  |  | |||
							
								
								
									
										37
									
								
								makefile
								
								
								
								
							
							
						
						
									
										37
									
								
								makefile
								
								
								
								
							|  | @ -1,7 +1,7 @@ | |||
| .PHONY: help | ||||
| help: | ||||
| 	@echo | ||||
| 	@echo "Virtual Boy Emulator - October 10, 2024" | ||||
| 	@echo "Virtual Boy Emulator - April 20, 2022" | ||||
| 	@echo | ||||
| 	@echo "Target build environment is any Debian with the following packages:" | ||||
| 	@echo "  emscripten" | ||||
|  | @ -27,35 +27,28 @@ build: | |||
| 
 | ||||
| .PHONY: bundle | ||||
| bundle: | ||||
| 	@java web/Bundle.java vbemu | ||||
| 	@java app/Bundle.java vbemu | ||||
| 
 | ||||
| .PHONY: clean | ||||
| clean: | ||||
| 	@rm -f vbemu_*.html web/core/core.wasm | ||||
| 	@rm -f vbemu_*.html app/core/core.wasm | ||||
| 
 | ||||
| .PHONY: core | ||||
| core: | ||||
| #	GCC generic
 | ||||
| 	@gcc core/vb.c -I core -c -o /dev/null \
 | ||||
|         -Werror -std=c90 -Wall -Wextra -Wpedantic | ||||
| #	GCC compilation control
 | ||||
| 	@gcc 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 | ||||
| #	Clang generic
 | ||||
| 	@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 | ||||
| 	@gcc core/vb.c -I core \
 | ||||
|         -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90 | ||||
| 	@gcc core/vb.c -I core -D VB_LITTLEENDIAN \
 | ||||
|         -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90 | ||||
| 	@emcc core/vb.c -I core \
 | ||||
|         -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90 | ||||
| 	@emcc core/vb.c -I core -D VB_LITTLEENDIAN \
 | ||||
|         -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90 | ||||
| 
 | ||||
| .PHONY: wasm | ||||
| wasm: | ||||
| 	@emcc -o web/core/core.wasm web/core/wasm.c core/vb.c -Icore \
 | ||||
|         -D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE \
 | ||||
|         -D "VBAPI=__attribute__((used))" \
 | ||||
|         --no-entry -O2 -flto -s WASM=1 \
 | ||||
| 	@emcc -o app/core/core.wasm wasm/wasm.c core/vb.c -Icore \
 | ||||
|         -D VB_LITTLEENDIAN --no-entry -O2 -flto -s WASM=1 \
 | ||||
|         -D "VB_EXPORT=__attribute__((used))" \
 | ||||
|         -s EXPORTED_RUNTIME_METHODS=[] -s ALLOW_MEMORY_GROWTH \
 | ||||
|         -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing | ||||
| 	@rm -f 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