Adding registers panes of CPU window

This commit is contained in:
Guy Perfect 2021-09-02 00:16:22 +00:00
parent 3dc8b93f94
commit ff07e1907f
32 changed files with 2437 additions and 444 deletions

1
.gitattributes vendored
View File

@ -9,3 +9,4 @@
*.class binary
*.dll binary
*.wasm binary
*.woff2 binary

View File

@ -90,6 +90,9 @@ globalThis.App = class App {
item = menu.newMenuItem({ text: "{memory._}" });
item.addClickListener(
()=>this.debuggers[0].memory.setVisible(true, true));
item = menu.newMenuItem({ text: "{cpu._}" });
item.addClickListener(
()=>this.debuggers[0].cpu.setVisible(true, true));
// Theme menu
menu = this.mainMenu.newMenu({ text: "{menu.theme._}" });
@ -159,11 +162,12 @@ globalThis.App = class App {
return;
}
// Send the ROM to the WebAssembly core module
this.core.postMessage({
command: "SetROM",
rom : file,
sim : 0
});
}, file);
}
// Specify the current color theme

View File

@ -12,16 +12,25 @@ globalThis.Debugger = class Debugger {
this.gui = app.gui;
this.sim = sim;
// Configure Memory window
// Memory window
this.memory = new MemoryWindow(this, {
title : "{sim}{memory._}",
center : true,
height : 300,
visible: false,
width : 400
});
this.memory.addCloseListener(e=>this.memory.setVisible(false));
app.desktop.add(this.memory);
// CPU window
this.cpu = new CPUWindow(this, {
title : "{sim}{cpu._}",
height : 300,
visible: false,
width : 400
});
this.cpu.addCloseListener(e=>this.cpu.setVisible(false));
app.desktop.add(this.cpu);
}
@ -31,12 +40,14 @@ globalThis.Debugger = class Debugger {
// Message received from emulation thread
message(msg) {
switch (msg.debug) {
case "CPU" : this.cpu .message(msg); break;
case "Memory": this.memory.message(msg); break;
}
}
// Reload all output
refresh() {
this.cpu .refresh();
this.memory.refresh();
}

View File

@ -25,12 +25,38 @@
// Message received
onmessage(msg) {
switch (msg.command) {
case "Init" : this.init (msg); break;
case "ReadBuffer": this.readBuffer(msg); break;
case "SetROM" : this.setROM (msg); break;
case "GetRegisters": this.getRegisters(msg); break;
case "Init" : this.init (msg); break;
case "ReadBuffer" : this.readBuffer (msg); break;
case "SetRegister" : this.setRegister (msg); break;
case "SetROM" : this.setROM (msg); break;
}
}
// Retrieve the values of all the CPU registers
getRegisters(msg) {
msg.pc = this.core.GetProgramCounter(msg.sim, 0);
msg.pcFrom = this.core.GetProgramCounter(msg.sim, 1);
msg.pcTo = this.core.GetProgramCounter(msg.sim, 2);
msg.adtre = this.core.GetSystemRegister(msg.sim, 25);
msg.chcw = this.core.GetSystemRegister(msg.sim, 24);
msg.ecr = this.core.GetSystemRegister(msg.sim, 4);
msg.eipc = this.core.GetSystemRegister(msg.sim, 0);
msg.eipsw = this.core.GetSystemRegister(msg.sim, 1);
msg.fepc = this.core.GetSystemRegister(msg.sim, 2);
msg.fepsw = this.core.GetSystemRegister(msg.sim, 3);
msg.pir = this.core.GetSystemRegister(msg.sim, 6);
msg.psw = this.core.GetSystemRegister(msg.sim, 5);
msg.tkcw = this.core.GetSystemRegister(msg.sim, 7);
msg.sr29 = this.core.GetSystemRegister(msg.sim, 29);
msg.sr30 = this.core.GetSystemRegister(msg.sim, 30);
msg.sr31 = this.core.GetSystemRegister(msg.sim, 31);
msg.program = new Array(32);
for (let x = 0; x <= 31; x++)
msg.program[x] = this.core.GetProgramRegister(msg.sim, x);
postMessage(msg);
}
// Initialize the WebAssembly core module
async init(msg) {
@ -48,13 +74,29 @@
// Read multiple data units from the bus
readBuffer(msg) {
let buffer = this.malloc(Uint8Array, msg.size);
this.core.ReadBuffer(msg.sim, buffer.pointer, msg.address, msg.size);
this.core.ReadBuffer(msg.sim, buffer.pointer,
msg.address, msg.size, msg.debug ? 1 : 0);
msg.buffer = this.core.memory.buffer.slice(
buffer.pointer, buffer.pointer + msg.size);
this.free(buffer);
postMessage(msg, msg.buffer);
}
// Specify a new value for a register
setRegister(msg) {
switch (msg.type) {
case "pc" : msg.value =
this.core.SetProgramCounter (msg.sim, msg.value);
break;
case "program": msg.value =
this.core.SetProgramRegister(msg.sim, msg.id, msg.value);
break;
case "system" : msg.value =
this.core.SetSystemRegister (msg.sim, msg.id, msg.value);
}
postMessage(msg);
}
// Supply a ROM buffer
setROM(msg) {
let rom = new Uint8Array(msg.rom);

View File

@ -52,14 +52,15 @@ globalThis.Bundle = class BundledFile {
// Detect the MIME type
this.mime =
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
name.endsWith(".frag") ? "text/plain;charset=UTF-8" :
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
name.endsWith(".png" ) ? "image/png" :
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".vert") ? "text/plain;charset=UTF-8" :
name.endsWith(".wasm") ? "application/wasm" :
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
name.endsWith(".frag" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
name.endsWith(".png" ) ? "image/png" :
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".vert" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".wasm" ) ? "application/wasm" :
name.endsWith(".woff2") ? "font/woff2" :
"application/octet-stream"
;
@ -304,11 +305,18 @@ let run = async function() {
await Bundle.run("app/toolkit/Panel.js");
await Bundle.run("app/toolkit/Application.js");
await Bundle.run("app/toolkit/Button.js");
await Bundle.run("app/toolkit/ButtonGroup.js");
await Bundle.run("app/toolkit/CheckBox.js");
await Bundle.run("app/toolkit/Label.js");
await Bundle.run("app/toolkit/MenuBar.js");
await Bundle.run("app/toolkit/MenuItem.js");
await Bundle.run("app/toolkit/Menu.js");
await Bundle.run("app/toolkit/RadioButton.js");
await Bundle.run("app/toolkit/Splitter.js");
await Bundle.run("app/toolkit/TextBox.js");
await Bundle.run("app/toolkit/Window.js");
await Bundle.run("app/windows/CPUWindow.js");
await Bundle.run("app/windows/Register.js");
await Bundle.run("app/windows/MemoryWindow.js");
await App.create();
};

View File

@ -9,8 +9,21 @@
romNotVB : "The selected file is not a Virtual Boy ROM.",
readFileError: "Unable to read the selected file."
},
cpu: {
_ : "CPU",
disassembler: "Disassembler",
float_ : "Float",
hex : "Hex",
pcFrom : "From",
pcTo : "To",
mainSplit : "Disassembler and registers splitter",
regsSplit : "System registers and program registers splitter",
signed : "Signed",
unsigned : "Unsigned"
},
memory: {
_: "Memory"
_ : "Memory",
hexEditor: "Hex viewer"
},
menu: {
_ : "Main application menu",

Binary file not shown.

View File

@ -1,17 +1,23 @@
:root {
--control : #222222;
--control-focus : #444444;
--control-shadow : #999999;
--control-text : #cccccc;
--desktop : #111111;
--window-blur : #555555;
--window-blur-text : #cccccc;
--window-close-blur : #998080;
--window-close-blur-border : #999999;
--window-close-blur-text : #ffffff;
--window-close-focus : #ee9999;
--window-close-focus-border: #999999;
--window-close-focus-text : #ffffff;
--window-focus : #007ACC;
--window-focus-text : #ffffff;
--close : #ee9999;
--close-blur : #998080;
--close-blur-border: #999999;
--close-blur-text : #ffffff;
--close-border : #999999;
--close-text : #ffffff;
--control : #333333;
--control-disabled : #999999;
--control-focus : #555555;
--control-shadow : #999999;
--control-text : #cccccc;
--desktop : #111111;
--splitter-focus : #0099ff99;
--title : #007ACC;
--title-blur : #555555;
--title-blur-text : #cccccc;
--title-text : #ffffff;
--window : #222222;
--window-border : #cccccc;
--window-disabled : #888888;
--window-text : #cccccc;
}

View File

@ -2,7 +2,7 @@
:root {
--font-dialog: "Roboto-Regular", sans-serif;
--font-hex : "Inconsolata_SemiExpanded-Regular", monospace;
--font-hex : "Inconsolata_SemiExpanded-Medium", monospace;
--font-size : 12px;
}
@ -13,8 +13,15 @@
}
body {
margin : 0;
overflow: hidden;
background: var(--window);
overflow : hidden;
}
* {
background: transparent;
border : none;
margin : 0;
padding : 0;
}
*:focus {
@ -48,6 +55,117 @@ body {
}
/********************************* Checkbox **********************************/
[role="checkbox"] {
column-gap: 2px;
}
[role="checkbox"] [name="check"] {
background : var(--window);
border : 1px solid var(--control-shadow);
color : var(--window-text);
font-size : calc(var(--font-size) - 1px);
line-height: calc(var(--font-size) - 2px);
height : calc(var(--font-size) - 2px);
overflow : hidden;
position : relative;
width : calc(var(--font-size) - 2px);
}
[role="checkbox"] [name="check"]:after {
bottom : 0;
left : 0;
line-height: 100%;
position : absolute;
right : 0;
text-align : center;
top : 0;
}
[role="checkbox"][active] [name="check"]:after,
[role="checkbox"][aria-checked="true"] [name="check"]:after {
content: "\2713";
}
[role="checkbox"][active] [name="check"]:after {
color: var(--window);
}
[role="checkbox"]:focus [name="check"] {
background: var(--control-focus);
}
[role="checkbox"][aria-disabled="true"] [name="check"] {
border-color: var(--window-disabled);
color : var(--window-disabled);
}
[role="checkbox"] [name="label"] {
color: var(--control-text);
}
[role="checkbox"][aria-disabled="true"] [name="label"] {
color: var(--control-disabled);
}
/********************************* Checkbox **********************************/
[role="radio"] {
column-gap: 2px;
}
[role="radio"] [name="check"] {
background : var(--window);
border : 1px solid var(--control-shadow);
border-radius: 50%;
color : var(--window-text);
height : calc(var(--font-size) - 2px);
overflow : hidden;
position : relative;
width : calc(var(--font-size) - 2px);
}
[role="radio"] [name="check"]:after {
background : currentColor;
border-radius: 50%;
bottom : 0;
left : 0;
position : absolute;
margin : 30%;
right : 0;
top : 0;
}
[role="radio"][active] [name="check"]:after,
[role="radio"][aria-checked="true"] [name="check"]:after {
content: "";
}
[role="radio"][active] [name="check"]:after {
color: var(--window);
}
[role="radio"]:focus [name="check"] {
background: var(--control-focus);
}
[role="radio"][aria-disabled="true"] [name="check"] {
border-color: var(--window-disabled);
color : var(--window-disabled);
}
[role="radio"] [name="label"] {
color: var(--control-text);
}
[role="radio"][aria-disabled="true"] [name="label"] {
color: var(--control-disabled);
}
/********************************** MenuBar **********************************/
@ -94,132 +212,265 @@ body {
/********************************* Splitter **********************************/
[role="separator"][tabindex] {
z-index: 1;
}
[role="separator"][tabindex]:focus {
background: var(--splitter-focus);
}
/********************************** TextBox **********************************/
input[type="text"] {
background : var(--window);
border : 1px solid var(--control-shadow);
color : var(--window-text);
font-size : var(--font-size);
line-height: var(--font-size);
}
/********************************** Window ***********************************/
[role="dialog"] {
/*background: #cc0000;*/
padding: 3px;
}
[role="dialog"] [name="body"] {
background: var(--control);
box-shadow:
0 0 0 1px var(--control),
0 0 0 2px var(--control-text),
1px 1px 0 2px var(--control-text)
0 0 0 2px var(--window-border),
1px 1px 0 2px var(--window-border)
;
row-gap : 3px;
}
[role="dialog"] [name="title-bar"] {
min-height: calc(1em + 5px);
}
[role="dialog"][focus="true"] [name="title-bar"] {
box-shadow:
0 0 0 1px var(--window-focus),
0 1px 0 1px var(--control-shadow)
align-items: center;
background : var(--title);
column-gap : 1px;
padding : 0 0 1px 0;
margin : 0 0 2px 0;
box-shadow :
-0.5px -0.5px 0 0.5px var(--title ),
0.5px -0.5px 0 0.5px var(--title ),
0 0 0 1px var(--control-shadow)
;
background : var(--window-focus);
}
[role="dialog"][focus="false"] [name="title-bar"] {
background: var(--title-blur);
box-shadow:
0 0 0 1px var(--window-blur),
0 1px 0 1px var(--control-shadow)
-0.5px -0.5px 0 0.5px var(--title-blur ),
0.5px -0.5px 0 0.5px var(--title-blur ),
0 0 0 1px var(--control-shadow)
;
background: var(--window-blur);
}
[role="dialog"] [name="title-icon"],
[role="dialog"] [name="title-close-box"] {
align-self : stretch;
min-width : calc(1em + 5px);
width : calc(1em + 5px);
[role="dialog"] [name="title-bar"] [name="icon"] {
height: 15px;
width : 15px;
}
[role="dialog"] [name="title"] {
color : var(--window-focus-text);
font-weight : bold;
line-height : calc(1em + 1px);
overflow : hidden;
padding : 2px;
text-align : center;
text-overflow: ellipsis;
white-space : nowrap;
[role="dialog"] [name="title-bar"] [name="title"] {
color : var(--title-text);
font-size : var(--font-size);
font-weight: bold;
text-align : center;
}
[role="dialog"][focus="false"] [name="title"] {
color: var(--window-blur-text);
[role="dialog"][focus="false"] [name="title-bar"] [name="title"] {
color: var(--title-blur-text);
}
[role="dialog"] [name="title-close-box"] {
align-items : center;
display : flex;
justify-content: center;
}
[role="dialog"] [name="title-close"] {
align-items : center;
background : var(--window-close-focus);
box-shadow : 0 0 0 1px var(--window-close-focus-border);
color : var(--window-close-focus-text);
display : flex;
height : 13px;
justify-content: center;
overflow : hidden;
padding : 0;
width : 13px;
}
[role="dialog"][focus="false"] [name="title-close"] {
background: var(--window-close-blur);
box-shadow: 0 0 0 1px var(--window-close-blur-border);
color : var(--window-close-blur-text);
}
[role="dialog"] [name="title-close"][active] {
box-shadow: 0 0 0 1px var(--window-close-focus-border);
margin : 0;
}
[role="dialog"] [name="title-close"]:after {
content : '\00d7';
[role="dialog"] [name="title-bar"] [name="close"] {
background : var(--close);
border : 1px solid var(--close-border);
box-sizing : border-box;
box-shadow : none;
color : var(--close-text);
font-size : 13px;
font-weight: bold;
height : 15px;
line-height: 13px;
margin : 0;
padding : 0;
position : relative;
text-align : center;
width : 15px;
}
[role="dialog"] [name="title-close"][active]:after {
margin: 1px -1px -1px 1px;
[role="dialog"][focus="false"] [name="title-bar"] [name="close"] {
background : var(--close-blur);
border-color: var(--close-blur-border);
color : var(--close-blur-text);
}
[role="dialog"] [name="title-bar"] [name="close"]:after {
content : "\00d7";
position: absolute;
inset : 0 0 0 0;
}
[role="dialog"] [name="title-bar"] [name="close"][active]:after {
inset: 1px -1px -1px 1px;
}
[role="dialog"] [name="client"] {
background: var(--control);
box-shadow: 0 0 0 1px var(--control);
}
/******************************* Memory Window *******************************/
[window="memory"] [name="client"] {
align-items: start;
column-gap : calc(var(--font-size) / 2);
[role="dialog"][window="memory"] [name="wrap-hex"] {
background : var(--window);
box-shadow : 0 0 0 1px var(--control-shadow);
font-family: var(--font-hex);
margin : 1px;
padding : 1px;
}
[window="memory"] [name="client"] > *:not(:nth-child(1n+1)) {
text-align: center;
[role="dialog"][window="memory"] [name="hex"] [role="row"] {
column-gap: calc(var(--font-size) / 2);
}
[window="memory"] [name="client"] > *:nth-child(17n+2),
[window="memory"] [name="client"] > *:nth-child(17n+10) {
[role="dialog"][window="memory"] [name="hex"] [role="row"] > *:nth-child( 2),
[role="dialog"][window="memory"] [name="hex"] [role="row"] > *:nth-child(10) {
margin-left: calc(var(--font-size) / 2);
}
[window="memory"] [name="address"],
[window="memory"] [name="byte"] {
font-family: var(--font-hex);
line-height: 1em;
/******************************** CPU Window *********************************/
[role="dialog"][window="cpu"] [name="wrap-disassembler"],
[role="dialog"][window="cpu"] [name="wrap-system-registers"],
[role="dialog"][window="cpu"] [name="wrap-program-registers"] {
background: var(--window);
color : var(--window-text);
box-shadow: 0 0 0 1px var(--control-shadow);
}
[window="memory"] [name="address"] {
align-self: start;
[role="dialog"][window="cpu"] [name="wrap-main"] {
box-shadow : 0 0 0 1px var(--control-shadow);
margin-left: 1px;
}
[role="dialog"][window="cpu"] [name="wrap-disassembler"] {
margin: 1px 0;
}
[role="dialog"][window="cpu"] [name="split-main"] {
margin-right: -1px;
}
[role="dialog"][window="cpu"] [name="wrap-registers"] {
box-shadow:
0 -1px 0 0 var(--control-shadow),
0 1px 0 0 var(--control-shadow)
;
margin: 1px 0;
width : 140px;
}
[role="dialog"][window="cpu"] [name="wrap-system-registers"] {
height: 143px;
margin: 0 1px;
}
[role="dialog"][window="cpu"] [name="wrap-program-registers"] {
margin: 0 1px;
}
[role="dialog"][window="cpu"] [name="disassembler"],
[role="dialog"][window="cpu"] [name="system-registers"],
[role="dialog"][window="cpu"] [name="program-registers"] {
padding: 1px;
}
[role="dialog"][window="cpu"] [name="expand"] {
column-gap: 0;
}
[role="dialog"][window="cpu"] [name="expand"] [name="check"] {
border: none;
}
[role="dialog"][window="cpu"] [name="expand"][aria-checked="false"]
[name="check"]:after {
content: "+";
}
[role="dialog"][window="cpu"] [name="expand"][aria-checked="true"]
[name="check"]:after {
content: "-";
}
[role="dialog"][window="cpu"] [name="wrap-registers"] input[type="text"] {
border : none;
font-family : var(--font-hex);
margin-right: 1px;
text-align : right;
width : 58px;
}
[role="dialog"][window="cpu"] [name="wrap-registers"]
[format="float"] [name="value"],
[role="dialog"][window="cpu"] [name="wrap-registers"]
[format="signed"] [name="value"],
[role="dialog"][window="cpu"] [name="wrap-registers"] [format="unsigned"]
[name="value"] {
font-family: var(--dialog-font);
width : 80px;
}
[role="dialog"][window="cpu"] [name="wrap-registers"] [name="expansion"] {
margin-left: calc(var(--font-size) * 1.5);
}
[role="dialog"][window="cpu"] [register="psw" ],
[role="dialog"][window="cpu"] [register="tkcw"] {
column-gap: var(--font-size);
}
[role="dialog"][window="cpu"] [register="ecr" ],
[role="dialog"][window="cpu"] [register="pir" ],
[role="dialog"][window="cpu"] [register="psw" ] [name="I" ],
[role="dialog"][window="cpu"] [register="tkcw"] [name="RD"] {
column-gap: 4px;
}
[role="dialog"][window="cpu"] [register="psw" ] [name="I" ] input[type="text"],
[role="dialog"][window="cpu"] [register="tkcw"] [name="RD"] input[type="text"]{
font-family: var(--font-dialog);
text-align : left;
width : 20px;
}
[role="dialog"][window="cpu"] [register="ecr"] input[type="text"],
[role="dialog"][window="cpu"] [register="pir"] input[type="text"] {
width: 32px;
}
[role="dialog"][window="cpu"] [name="expand"][aria-disabled="true"]
[name="check"]:after {
content: "";
}
[role="dialog"][window="cpu"] [aria-disabled="true"] * {
color: var(--window-text);
}
[role="dialog"][window="cpu"] [aria-disabled="true"] [name="check"] {
border-color: var(--control-shadow);
color : var(--window-text);
}

View File

@ -1,17 +1,23 @@
:root {
--control : #eeeeee;
--control-focus : #cccccc;
--control-shadow : #999999;
--control-text : #000000;
--desktop : #cccccc;
--window-blur : #cccccc;
--window-blur-text : #444444;
--window-close-blur : #d4c4c4;
--window-close-blur-border : #999999;
--window-close-blur-text : #ffffff;
--window-close-focus : #ee9999;
--window-close-focus-border: #999999;
--window-close-focus-text : #ffffff;
--window-focus : #80ccff;
--window-focus-text : #000000;
--close : #ee9999;
--close-blur : #d4c4c4;
--close-blur-border: #999999;
--close-blur-text : #ffffff;
--close-border : #999999;
--close-text : #ffffff;
--control : #eeeeee;
--control-disabled : #888888;
--control-focus : #cccccc;
--control-shadow : #999999;
--control-text : #000000;
--desktop : #cccccc;
--splitter-focus : #0099ff99;
--title : #80ccff;
--title-blur : #cccccc;
--title-blur-text : #444444;
--title-text : #000000;
--window : #ffffff;
--window-border : #000000;
--window-disabled : #aaaaaa;
--window-text : #000000;
}

View File

@ -1,21 +1,27 @@
:root {
--control : #000000;
--control-focus : #550000;
--control-shadow : #aa0000;
--control-text : #ff0000;
--desktop : #000000;
--window-blur : #000000;
--window-blur-text : #aa0000;
--window-close-blur : #550000;
--window-close-blur-border : #aa0000;
--window-close-blur-text : #aa0000;
--window-close-focus : #aa0000;
--window-close-focus-border: #ff0000;
--window-close-focus-text : #ff0000;
--window-focus : #550000;
--window-focus-text : #ff0000;
--close : #aa0000;
--close-blur : #550000;
--close-blur-border: #aa0000;
--close-blur-text : #aa0000;
--close-border : #ff0000;
--close-text : #ff0000;
--control : #000000;
--control-disabled : #aa0000;
--control-focus : #550000;
--control-shadow : #aa0000;
--control-text : #ff0000;
--desktop : #000000;
--splitter-focus : #ff000099;
--title : #550000;
--title-blur : #000000;
--title-blur-text : #aa0000;
--title-text : #ff0000;
--window : #000000;
--window-border : #ff0000;
--window-disabled : #aa0000;
--window-text : #ff0000;
}
[filter="true"] {
[filter] {
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
}

View File

@ -200,7 +200,6 @@ Toolkit.Application = class Application extends Toolkit.Panel {
// A pointer or mouse down even has propagated
onpropagation(e) {
e.preventDefault();
e.stopPropagation();
for (let listener of this.propagationListeners)
listener(e, this);

View File

@ -45,43 +45,51 @@ Toolkit.Button = class Button extends Toolkit.Component {
this.clickListeners.push(listener);
}
// The button was activated
click(e) {
if (!this.enabled)
return;
for (let listener of this.clickListeners)
listener(e);
}
// Request focus on the appropriate element
focus() {
this.element.focus();
}
// Retrieve the button's accessible name
// Retrieve the component's accessible name
getName() {
return this.name;
}
// Retrieve the button's display text
// Retrieve the component's display text
getText() {
return this.text;
}
// Retrieve the button's tool tip text
// Retrieve the component's tool tip text
getToolTip() {
return this.toolTip;
}
// Determine whether the button is enabled
// Determine whether the component is enabled
isEnabled() {
return this.enabled;
}
// Determine whether the button is focusable
// Determine whether the component is focusable
isFocusable() {
return this.focusable;
}
// Specify whether the button is enabled
// Specify whether the component is enabled
setEnabled(enabled) {
this.enabled = enabled = !!enabled;
this.element.setAttribute("aria-disabled", !enabled);
}
// Specify whether the button can receive focus
// Specify whether the component can receive focus
setFocusable(focusable) {
this.focusable = focusable = !!focusable;
if (focusable)
@ -89,19 +97,19 @@ Toolkit.Button = class Button extends Toolkit.Component {
else this.element.removeAttribute("tabindex");
}
// Specify the button's accessible name
// Specify the component's accessible name
setName(name) {
this.name = name || "";
this.localize();
}
// Specify the button's display text
// Specify the component's display text
setText(text) {
this.text = text || "";
this.localize();
}
// Specify the button's tool tip text
// Specify the component's tool tip text
setToolTip(toolTip) {
this.toolTip = toolTip || "";
this.localize();
@ -133,14 +141,6 @@ Toolkit.Button = class Button extends Toolkit.Component {
///////////////////////////// Private Methods /////////////////////////////
// The button was activated
activate(e) {
if (!this.enabled)
return;
for (let listener of this.clickListeners)
listener(e, this);
}
// Key down event handler
onkeydown(e) {
@ -152,7 +152,7 @@ Toolkit.Button = class Button extends Toolkit.Component {
switch (e.key) {
case " ":
case "Enter":
this.activate(e);
this.click(e);
break;
default: return;
}
@ -166,7 +166,6 @@ Toolkit.Button = class Button extends Toolkit.Component {
onpointerdown(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Configure focus
@ -221,11 +220,11 @@ Toolkit.Button = class Button extends Toolkit.Component {
// Configure element
this.element.releasePointerCapture(e.pointerId);
// Activate the menu item if it is active
// Activate the component if it is active
if (!this.element.hasAttribute("active"))
return;
this.element.removeAttribute("active");
this.activate(e);
this.click(e);
}
};

View File

@ -0,0 +1,44 @@
"use strict";
// Grouping manager for mutually-exclusive controls
Toolkit.ButtonGroup = class ButtonGroup {
// Object constructor
constructor() {
this.components = [];
}
///////////////////////////// Public Methods //////////////////////////////
// Add a component to the group
add(component) {
if (this.components.indexOf(component) != -1)
return component;
this.components.push(component);
if ("setGroup" in component)
component.setGroup(this);
return component;
}
// Select only one button in the group
setChecked(component) {
for (let comp of this.components) {
if ("setChecked" in comp)
comp.setChecked(comp == component, this);
}
}
// Remove a component from the group
remove(component) {
let index = this.components.indexOf(component);
if (index == -1)
return false;
this.components.splice(index, 1);
if ("setGroup" in component)
component.setGroup(null);
return true;
}
};

190
app/toolkit/CheckBox.js Normal file
View File

@ -0,0 +1,190 @@
"use strict";
// On/off toggle checkbox
Toolkit.CheckBox = class CheckBox extends Toolkit.Panel {
// Object constructor
constructor(application, options) {
super(application, options);
options = options || {};
// Configure instance fields
this.changeListeners = [];
this.checked = false;
this.enabled = "enabled" in options ? !!options.enabled : true;
this.text = options.text || "";
// Configure element
this.setLayout("grid", {
columns : "max-content max-content"
});
this.setDisplay("inline-grid");
this.setHollow(false);
this.setOverflow("visible", "visible");
this.element.setAttribute("tabindex", "0");
this.element.setAttribute("role", "checkbox");
this.element.setAttribute("aria-checked", "false");
this.element.style.alignItems = "center";
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
// Configure check box
this.check = this.add(this.newLabel());
this.check.element.setAttribute("name", "check");
this.check.element.setAttribute("aria-hidden", "true");
// Configure label
this.label = this.add(this.newLabel({ localized: true }));
this.label.element.setAttribute("name", "label");
this.element.setAttribute("aria-labelledby", this.label.id);
// Configure properties
this.setChecked(options.checked);
this.setEnabled(this.enabled);
this.setText (this.text );
}
///////////////////////////// Public Methods //////////////////////////////
// Add a callback for change events
addChangeListener(listener) {
if (this.changeListeners.indexOf(listener) == -1)
this.changeListeners.push(listener);
}
// Request focus on the appropriate element
focus() {
this.element.focus();
}
// Determine whether the component is checked
isChecked() {
return this.checked;
}
// Determine whether the component is enabled
isEnabled() {
return this.enabled;
}
// Specify whether the component is checked
setChecked(checked, e) {
checked = !!checked;
if (checked == this.checked)
return;
this.checked = checked;
this.element.setAttribute("aria-checked", checked);
if (e === undefined)
return;
for (let listener of this.changeListeners)
listener(e);
}
// Specify whether the component is enabled
setEnabled(enabled) {
this.enabled = enabled = !!enabled;
this.element.setAttribute("aria-disabled", !enabled);
if (enabled)
this.element.setAttribute("tabindex", "0");
else this.element.removeAttribute("tabindex");
}
// Specify the component's display text
setText(text) {
this.text = text = text || "";
this.label.setText(text);
}
///////////////////////////// Private Methods /////////////////////////////
// Key down event handler
onkeydown(e) {
// Error checking
if (!this.enabled)
return;
// Ignore the key
if (e.key != " ")
return;
// Configure event
e.preventDefault();
e.stopPropagation();
// Toggle the checked state
this.setChecked(!this.checked, e);
}
// Pointer down event handler
onpointerdown(e) {
// Configure event
//e.preventDefault();
e.stopPropagation();
// Configure focus
if (this.enabled)
this.focus();
else return;
// Error checking
if (e.button != 0 || this.element.hasPointerCapture(e.captureId))
return;
// Configure element
this.element.setPointerCapture(e.pointerId);
this.element.setAttribute("active", "");
}
// Pointer move event handler
onpointermove(e) {
// Error checking
if (!this.element.hasPointerCapture(e))
return;
// Configure event
e.preventDefault();
e.stopPropagation();
// Working variables
let bounds = this.getBounds();
let active =
e.x >= bounds.x && e.x < bounds.x + bounds.width &&
e.y >= bounds.y && e.y < bounds.y + bounds.height
;
// Configure element
if (active)
this.element.setAttribute("active", "");
else this.element.removeAttribute("active");
}
// Pointer up event handler
onpointerup(e) {
// Configure event
e.stopPropagation();
// Error checking
if (!this.element.hasPointerCapture(e.pointerId))
return;
// Configure element
this.element.releasePointerCapture(e.pointerId);
// Activate the component if it is active
if (!this.element.hasAttribute("active"))
return;
this.element.removeAttribute("active");
this.setChecked(!this.checked, e);
}
};

View File

@ -21,6 +21,10 @@ Toolkit.Component = class Component {
// Configure component
this.element.component = this;
this.setSize(
"width" in options ? options.width : null,
"height" in options ? options.height : null
);
this.setVisible(this.visible);
}
@ -58,11 +62,11 @@ Toolkit.Component = class Component {
}
// Specify the location and size of the component
setBounds(left, top, width, height) {
setBounds(left, top, width, height, minimum) {
this.setLeft (left );
this.setTop (top );
this.setWidth (width );
this.setHeight(height);
this.setWidth (width , minimum);
this.setHeight(height, minimum);
}
// Specify the display CSS property of the visible element
@ -71,12 +75,19 @@ Toolkit.Component = class Component {
this.setVisible(this.visible);
}
// Specify the height of the element
setHeight(height) {
if (height === null)
// Specify the height of the component
setHeight(height, minimum) {
if (height === null) {
this.element.style.removeProperty("min-height");
this.element.style.removeProperty("height");
else this.element.style.height =
typeof height == "number" ? height + "px" : height
} else {
height = typeof height == "number" ?
Math.max(0, height) + "px" : height;
this.element.style.height = height;
if (minimum)
this.element.style.minHeight = height;
else this.element.style.removeProperty("min-height");
}
}
// Specify the horizontal position of the component
@ -100,9 +111,9 @@ Toolkit.Component = class Component {
}
// Specify both the width and the height of the component
setSize(width, height) {
this.setHeight(height);
this.setWidth (width );
setSize(width, height, minimum) {
this.setHeight(height, minimum);
this.setWidth (width , minimum);
}
// Specify the vertical position of the component
@ -123,12 +134,19 @@ Toolkit.Component = class Component {
} else this.element.style.display = "none";
}
// Specify the width of the element
setWidth(width) {
if (width === null)
// Specify the width of the component
setWidth(width, minimum) {
if (width === null) {
this.element.style.removeProperty("min-width");
this.element.style.removeProperty("width");
else this.element.style.width =
typeof width == "number" ? width + "px" : width ;
} else {
width = typeof width == "number" ?
Math.max(0, width) + "px" : width;
this.element.style.width = width;
if (minimum)
this.element.style.minWidth = width;
else this.element.style.removeProperty("min-width");
}
}

View File

@ -5,10 +5,11 @@ Toolkit.Label = class Label extends Toolkit.Component {
// Object constructor
constructor(application, options) {
super(application, "div", options);
super(application, options&&options.label ? "label" : "div", options);
options = options || {};
// Configure instance fields
this.focusable = "focusable" in options ? !!options.focusable : false;
this.localized = "localized" in options ? !!options.localized : false;
this.text = options.text || "";
@ -17,7 +18,8 @@ Toolkit.Label = class Label extends Toolkit.Component {
this.element.style.userSelect = "none";
// Configure properties
this.setText(this.text);
this.setFocusable(this.focusable);
this.setText (this.text);
if (this.localized)
this.application.addComponent(this);
}
@ -26,17 +28,43 @@ Toolkit.Label = class Label extends Toolkit.Component {
///////////////////////////// Public Methods //////////////////////////////
// Request focus on the appropriate element
focus() {
if (this.focusable)
this.element.focus();
}
// Retrieve the label's display text
getText() {
return this.text;
}
// Determine whether the component is focusable
isFocusable() {
return this.focusable;
}
// Specify the label's display text
setText(text) {
this.text = text || "";
this.localize();
}
// Specify whether the component is focusable
setFocusable(focusable) {
this.focusable = focusable = !!focusable;
if (focusable) {
this.element.setAttribute("tabindex", "0");
this.localized && this.application &&
this.application.addComponent(this);
} else {
this.element.removeAttribute("aria-label");
this.element.removeAttribute("tabindex");
this.localized && this.application &&
this.application.removeComponent(this);
}
}
///////////////////////////// Package Methods /////////////////////////////

View File

@ -15,7 +15,7 @@ Toolkit.MenuBar = class MenuBar extends Toolkit.Panel {
// Configure element
this.element.style.position = "relative";
this.element.style.zIndex = "1";
this.element.style.zIndex = "2";
this.element.setAttribute("role", "menubar");
this.setLayout("flex", {
direction: "row",

View File

@ -15,22 +15,23 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
this.children = [];
this.columns = null;
this.direction = "row";
this.focusable = "focusable" in options ? !!options.focusable:false;
this.hollow = "hollow" in options ? !!options.hollow : true;
this.layout = null;
this.name = options.name || "";
this.overflowX = options.overflowX || "hidden";
this.overflowY = options.overflowY || "hidden";
this.rows = null;
this.wrap = false;
// Configure element
if ("noShrink" in options ? !options.noShrink : true) {
this.element.style.minHeight = "0";
this.element.style.minWidth = "0";
}
this.setOverflow(this.overflowX, this.overflowY);
// Configure layout
options = options || {};
this.setLayout(options.layout || null, options);
// Configure properties
this.setFocusable(this.focusable);
this.setHollow (this.hollow);
this.setLayout (options.layout || null, options);
this.setName (this.name);
this.setOverflow (this.overflowX, this.overflowY);
if (this.application && this.focusable)
this.application.addComponent(this);
}
@ -54,11 +55,37 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
return component;
}
// Request focus on the appropriate element
focus() {
if (this.focusable)
this.element.focus();
}
// Retrieve the component's accessible name
getName() {
return this.name;
}
// Determine whether the component is focusable
isFocusable() {
return this.focusable;
}
// Determine whether the component is hollow
isHollow() {
return this.hollow;
}
// Create a Button and associate it with the application
newButton(options) {
return new Toolkit.Button(this.application, options);
}
// Create a CheckBox and associate it with the application
newCheckBox(options) {
return new Toolkit.CheckBox(this.application, options);
}
// Create a Label and associate it with the application
newLabel(options) {
return new Toolkit.Label(this.application, options);
@ -74,6 +101,21 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
return new Toolkit.Panel(this.application, options);
}
// Create a RadioButton and associate it with the application
newRadioButton(options) {
return new Toolkit.RadioButton(this.application, options);
}
// Create a Splitter and associate it with the application
newSplitter(options) {
return new Toolkit.Splitter(this.application, options);
}
// Create a TextBox and associate it with the application
newTextBox(options) {
return new Toolkit.TextBox(this.application, options);
}
// Create a Window and associate it with the application
newWindow(options) {
return new Toolkit.Window(this.application, options);
@ -115,6 +157,31 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
this.children.splice(index, 1);
}
// Specify whether the component is focusable
setFocusable(focusable) {
this.focusable = focusable = !!focusable;
if (focusable) {
this.element.setAttribute("tabindex", "0");
this.application && this.application.addComponent(this);
} else {
this.element.removeAttribute("aria-label");
this.element.removeAttribute("tabindex");
this.application && this.application.removeComponent(this);
}
}
// Specify whether the component is hollow
setHollow(hollow) {
this.hollow = hollow = !!hollow;
if (hollow) {
this.element.style.minHeight = "0";
this.element.style.minWidth = "0";
} else {
this.element.style.removeProperty("min-height");
this.element.style.removeProperty("min-width" );
}
}
// Configure the element's layout
setLayout(layout, options) {
@ -122,6 +189,7 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
this.layout = layout;
// Processing by layout
options = options || {};
switch (layout) {
case "block" : this.setBlockLayout (options); break;
case "desktop": this.setDesktopLayout(options); break;
@ -132,11 +200,17 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
}
// Specify the component's accessible name
setName(name) {
this.name = name || "";
if (this.focusable)
this.localize();
}
// Configure the panel's overflow scrolling behavior
setOverflow(x, y) {
this.overflowX = x || "hidden";
this.overflowY = y || "hidden";
this.element.style.overflow = this.overflowX + " " + this.overflowY;
this.element.style.overflowX = this.overflowX = x || "hidden";
this.element.style.overflowY = this.overflowY = y || this.overflowX;
}
// Specify the semantic role of the panel
@ -153,7 +227,15 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
// Move a window to the foreground
bringToFront(wnd) {
for (let child of this.children)
child.element.style.zIndex = child == wnd ? "0" : "1";
child.element.style.zIndex = child == wnd ? "1" : "0";
}
// Update display text with localized strings
localize() {
let name = this.name;
if (this.application)
name = this.application.translate(name, this);
this.element.setAttribute("aria-label", name);
}

View File

@ -0,0 +1,59 @@
"use strict";
// Select-only radio button
Toolkit.RadioButton = class RadioButton extends Toolkit.CheckBox {
// Object constructor
constructor(application, options) {
super(application, options);
options = options || {};
// Configure instance fields
this.group = null;
// Configure element
this.element.setAttribute("role", "radio");
// Configure properties
this.setGroup(options.group || null);
}
///////////////////////////// Public Methods //////////////////////////////
// Retrieve the enclosing ButtonGroup
getGroup() {
return this.group;
}
// Specify whether the component is checked (overrides superclass)
setChecked(checked, e) {
checked = !!checked;
if (e instanceof Event && !checked || checked == this.checked)
return;
this.checked = checked;
this.element.setAttribute("aria-checked", checked);
if (this.group != null && e != this.group)
this.group.setChecked(this);
if (e === undefined)
return;
for (let listener of this.changeListeners)
listener(e);
}
// Specify the enclosing ButtonGroup
setGroup(group) {
group = group || null;
if (group == this.group)
return;
if (this.group != null)
this.group.remove(this);
this.group = group;
if (group != null)
group.add(this);
}
};

301
app/toolkit/Splitter.js Normal file
View File

@ -0,0 +1,301 @@
"use strict";
// Interactive splitter
Toolkit.Splitter = class Splitter extends Toolkit.Component {
// Object constructor
constructor(application, options) {
super(application, "div", options);
options = options || {};
// Configure instance fields
this.component = options.component || null;
this.dragPointer = null;
this.dragPos = 0;
this.dragSize = 0;
this.orientation = options.orientation || "horizontal";
this.name = options.name || "";
this.edge = options.edge ||
(this.orientation == "horizontal" ? "top" : "left");
// Configure element
this.element.setAttribute("role" , "separator");
this.element.setAttribute("tabindex" , "0");
this.element.setAttribute("aria-valuemin", "0");
this.element.addEventListener("keydown" , e=>this.onkeydown (e));
this.element.addEventListener("pointerdown", e=>this.onpointerdown(e));
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
// Configure properties
this.setComponent (this.component );
this.setName (this.name );
this.setOrientation(this.orientation);
this.application.addComponent(this);
}
///////////////////////////// Public Methods //////////////////////////////
// Request focus on the appropriate element
focus() {
this.element.focus();
}
// Retrieve the component managed by this Splitter
getComponent() {
return this.component;
}
// Retrieve the component's accessible name
getName() {
return this.name;
}
// Retrieve the component's orientation
getOrientation() {
return this.orientation;
}
// Determine the current and maximum separator values
measure() {
let max = 0;
let now = 0;
// Mesure the component
if (this.component != null && this.parent != null) {
let component = this.component.getBounds();
let bounds = this.getBounds();
let panel = this.parent.getBounds();
// Horizontal Splitter
if (this.orientation == "horizontal") {
max = panel.height - bounds.height;
now = Math.max(0, Math.min(max, component.height));
this.component.setSize(null, now);
}
// Vertical Splitter
else {
max = panel.width - bounds.width;
now = Math.max(0, Math.min(max, component.width));
this.component.setSize(now, null);
}
}
// Configure element
this.element.setAttribute("aria-valuemax", max);
this.element.setAttribute("aria-valuenow", now);
}
// Specify the component managed by this Splitter
setComponent(component) {
this.component = component = component || null;
this.element.setAttribute("aria-controls",
component == null ? "" : component.id);
this.measure();
}
// Specify the component's accessible name
setName(name) {
this.name = name || "";
this.localize();
}
// Specify the component's orientation
setOrientation(orientation) {
switch (orientation) {
case "horizontal":
this.orientation = "horizontal";
this.setSize(null, 3, true);
this.element.setAttribute("aria-orientation", "horizontal");
this.element.style.cursor = "ew-resize";
break;
case "vertical":
this.orientation = "vertical";
this.setSize(3, null, true);
this.element.setAttribute("aria-orientation", "vertical");
this.element.style.cursor = "ns-resize";
}
this.measure();
}
///////////////////////////// Package Methods /////////////////////////////
// Update display text with localized strings
localize() {
let name = this.name;
if (this.application) {
name = this.application.translate(name, this);
}
this.element.setAttribute("aria-label", name);
}
///////////////////////////// Private Methods /////////////////////////////
// Key press event handler
onkeydown(e) {
// Error checking
if (this.component == null)
return;
let pos = this.component.getBounds();
let size = this .getBounds();
let max = this.parent .getBounds();
if (this.orientation == "horizontal") {
max = max .height;
pos = pos .height;
size = size.height;
} else {
max = max .width;
pos = pos .width;
size = size.width;
}
if (this.edge == "top" || this.edge == "left")
max -= size;
// Processing by key
if (this.component != null) switch (e.key) {
case "ArrowDown":
switch (this.edge) {
case "bottom":
this.component.setSize(null, Math.min(max, pos - 6));
break;
case "top":
this.component.setSize(null, Math.min(max, pos + 6));
}
break;
case "ArrowLeft":
switch (this.edge) {
case "left":
this.component.setSize(Math.min(max, pos - 6), null);
break;
case "right":
this.component.setSize(Math.min(max, pos + 6), null);
}
break;
case "ArrowRight":
switch (this.edge) {
case "left":
this.component.setSize(Math.min(max, pos + 6), null);
break;
case "right":
this.component.setSize(Math.min(max, pos - 6), null);
}
break;
case "ArrowUp":
switch (this.edge) {
case "bottom":
this.component.setSize(null, Math.min(max, pos + 6));
break;
case "top":
this.component.setSize(null, Math.min(max, pos - 6));
}
break;
case "Escape":
if (this.dragPointer === null)
return;
this.element.releasePointerCapture(this.dragPointer);
this.dragPointer = null;
if (this.orientation == "horizontal")
this.component.setHeight(null, this.dragSize);
else this.component.setWidth(this.dragSize, null);
break;
default: return;
}
// Configure event
e.preventDefault();
e.stopPropagation();
}
// Pointer down event handler
onpointerdown(e) {
// Request focus
this.focus();
// Configure event
e.stopPropagation();
// Error checking
if (
this.component == null ||
e.button != 0 ||
this.element.hasPointerCapture(e.pointerId)
) return;
// Capture the pointer
this.element.setPointerCapture(e.pointerId);
this.dragPointer = e.pointerId;
let bounds = this.component.getBounds();
if (this.orientation == "horizontal") {
this.dragPos = e.y;
this.dragSize = bounds.height;
} else {
this.dragPos = e.x;
this.dragSize = bounds.width;
}
}
// Pointer move event handler
onpointermove(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Error checking
if (
this.component == null ||
!this.element.hasPointerCapture(e.pointerId)
) return;
// Resize the component
let bounds = this.getBounds();
let panel = this.parent.getBounds();
switch (this.edge) {
case "bottom":
this.component.setSize(null, Math.max(0, Math.min(
this.dragSize - e.y + this.dragPos,
panel.height - bounds.height
)));
break;
case "left":
this.component.setSize(Math.max(0, Math.min(
this.dragSize + e.x - this.dragPos,
panel.width - bounds.width
)), null);
break;
case "right":
this.component.setSize(Math.max(0, Math.min(
this.dragSize - e.x + this.dragPos,
panel.width - bounds.width
)), null);
break;
case "top":
this.component.setSize(null, Math.max(0, Math.min(
this.dragSize + e.y - this.dragPos,
panel.height - bounds.height
)));
break;
}
this.measure();
}
// Pointer up event handler
onpointerup(e) {
e.preventDefault();
e.stopPropagation();
this.element.releasePointerCapture(e.pointerId);
this.dragPointer = null;
}
};

138
app/toolkit/TextBox.js Normal file
View File

@ -0,0 +1,138 @@
"use strict";
Toolkit.TextBox = class TextBox extends Toolkit.Component {
constructor(application, options) {
super(application, "input", options);
options = options || {};
// Configure instance fields
this.changeListeners = [];
this.commitListeners = [];
this.enabled = "enabled" in options ? !!options.enabled : true;
this.name = options.name || "";
this.lastCommit = "";
// Configure element
this.element.size = 1;
this.element.type = "text";
this.element.addEventListener("blur" , e=>this.commit (e));
this.element.addEventListener("input" , e=>this.onchange (e));
this.element.addEventListener("keydown", e=>this.onkeydown(e));
// Configure properties
this.setEnabled(this.enabled);
this.setName (this.name );
this.setText (options.text || "");
this.application.addComponent(this);
}
///////////////////////////// Public Methods //////////////////////////////
// Add a callback for change events
addChangeListener(listener) {
if (this.changeListeners.indexOf(listener) == -1)
this.changeListeners.push(listener);
}
// Add a callback for commit events
addCommitListener(listener) {
if (this.commitListeners.indexOf(listener) == -1)
this.commitListeners.push(listener);
}
// Request focus on the appropriate element
focus() {
this.element.focus();
}
// Retrieve the component's accessible name
getName() {
return this.name;
}
// Retrieve the component's display text
getText() {
return this.element.value;
}
// Determine whether the component is enabled
isEnabled() {
return this.enabled;
}
// Specify whether the component is enabled
setEnabled(enabled) {
this.enabled = enabled = !!enabled;
this.element.setAttribute("aria-disabled", !enabled);
if (enabled)
this.element.removeAttribute("disabled");
else this.element.setAttribute("disabled", "");
}
// Specify the component's accessible name
setName(name) {
this.name = name || "";
this.localize();
}
// Specify the component's display text
setText(text) {
text = !text && text !== 0 ? "" : "" + text;
this.lastCommit = text;
this.element.value = text;
}
///////////////////////////// Package Methods /////////////////////////////
// Update display text with localized strings
localize() {
let name = this.name;
if (this.application)
name = this.application.translate(name, this);
this.element.setAttribute("aria-label", name);
}
///////////////////////////// Private Methods /////////////////////////////
// Input finalized
commit(e) {
let text = this.element.value || "";
if (!this.enabled || text == this.lastCommit)
return;
this.lastCommit = text;
for (let listener of this.commitListeners)
listener(e, this);
}
// Text changed event handler
onchange(e) {
e.stopPropagation();
if (!this.enabled)
return;
for (let listener of this.changeListeners)
listener(e, this);
}
// Key press event handler
onkeydown(e) {
// Configure event
e.stopPropagation();
// Error checking
if (!this.enabled)
return;
// The Enter key was pressed
if (e.key == "Enter")
this.commit(e);
}
};

View File

@ -11,28 +11,22 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Configure instance fields
this.closeListeners = [];
this.dragBounds = null;
this.dragClient = null;
this.dragCursor = { x: 0, y: 0 };
this.dragEdge = null;
this.dragPointer = null;
this.initialCenter = "center" in options ? !!options.center : false;
this.initialHeight = options.height || 64;
this.initialWidth = options.width || 64;
this.lastFocus = this.element;
this.shown = this.visible;
this.title = options.title || "";
// Configure element
this.setLayout("flex", {
alignCross: "stretch",
direction : "column",
overflowX : "visible",
overflowY : "visible"
});
this.setLayout("grid", { columns: "auto" });
this.setRole("dialog");
this.setBounds(0, 0, 64, 64);
this.setLocation(0, 0);
this.element.style.position = "absolute";
this.element.setAttribute("aria-modal", "false");
this.element.setAttribute("focus" , "false");
this.element.setAttribute("tabindex" , "-1" );
this.element.setAttribute("tabindex" , "0" );
this.element.addEventListener(
"blur" , e=>this.onblur (e), { capture: true });
this.element.addEventListener(
@ -42,69 +36,48 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.element.addEventListener("pointermove", e=>this.onpointermove(e));
this.element.addEventListener("pointerup" , e=>this.onpointerup (e));
// Configure body container
// Primary visible container
this.body = this.add(this.newPanel({
layout : "flex",
alignCross: "stretch",
direction : "column",
overflowX : "visible",
overflowY : "visible"
layout : "grid",
overflowX: "visible",
overflowY: "visible",
rows : "max-content auto"
}));
this.body.element.style.flexGrow = "1";
this.body.element.style.margin = "3px";
this.body.element.setAttribute("name", "body");
// Configure title bar
// Title bar
this.titleBar = this.body.add(this.newPanel({
layout : "flex",
alignCross: "center",
direction : "row",
noShrink : true,
overflowX : "visible",
overflowY : "visible"
layout : "grid",
columns: "max-content auto max-content",
hollow : false
}));
this.titleBar.element.setAttribute("name", "title-bar");
// Configure title icon element
this.titleIcon = this.titleBar.add(this.newPanel({}));
this.titleIcon.element.setAttribute("name", "title-icon");
this.titleIcon.element.style.removeProperty("min-width");
// Title bar icon
this.titleIcon = this.titleBar.add(this.newPanel());
this.titleIcon.element.setAttribute("name", "icon");
this.titleIcon.element.setAttribute("aria-hidden", "true");
// Configure title text element
this.titleElement = this.titleBar.add(this.newLabel({}));
this.titleElement.element.setAttribute("name", "title");
this.titleElement.element.style.cursor = "default";
this.titleElement.element.style.flexGrow = "1";
this.titleElement.element.style.userSelect = "none";
this.element.setAttribute("aria-labelledby", this.titleElement.id);
// Configure title close element
this.titleCloseBox = this.titleBar.add(this.newPanel({}));
this.titleCloseBox.element.setAttribute("name", "title-close-box");
this.titleCloseBox.element.style.removeProperty("min-width");
this.titleClose = this.titleCloseBox.add(this.newButton({
focusable: false,
name : "{app.close}",
toolTip : "{app.close}"
// Title bar title
this.title = this.titleBar.add(this.newLabel({
localized: true,
text : options.title
}));
this.titleClose.element.setAttribute("name", "title-close");
this.title.element.setAttribute("name", "title");
this.title.setProperty("sim", "");
// Title bar close
this.titleClose = this.titleBar.add(this.newButton({
toolTip: "{app.close}"
}));
this.titleClose.element.setAttribute("name", "close");
this.titleClose.element.setAttribute("aria-hidden", "true");
this.titleClose.addClickListener(e=>this.onclose(e));
// Configure client area
this.client = this.body.add(this.newPanel({
overflowX: "hidden",
overflowY: "hidden"
}));
this.client.element.style.flexGrow = "1";
// Client area
this.client = this.body.add(this.newPanel());
this.client.element.setAttribute("name", "client");
this.client.element.addEventListener(
"pointerdown", e=>this.onclientdown(e));
// Configure properties
this.setTitle(this.title);
if (this.shown)
this.setClientSize(this.initialHeight, this.initialWidth);
application.addComponent(this);
this.setSize(options.width, options.height);
}
@ -117,20 +90,19 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.closeListeners.push(listener);
}
// Specify the size of the client rectangle in pixels
setClientSize(width, height) {
let bounds = this.getBounds();
let client = this.client.getBounds();
this.setSize(
width + bounds.width - client.width,
height + bounds.height - client.height
);
// Retrieve the window's title text
getTitle() {
return this.title.getText();
}
// Specify the height of the component
setHeight(height, minimum) {
this.client && this.client.setHeight(height, minimum);
}
// Specify the window's title text
setTitle(title) {
this.title = title || "";
this.localize();
this.title.setText(title);
}
// Specify whether the component is visible
@ -146,6 +118,12 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.firstShow();
}
// Specify the width of the component
setWidth(width, minimum) {
this.client && this.client.setWidth(
Math.max(64, width || 0), minimum);
}
///////////////////////////// Package Methods /////////////////////////////
@ -155,13 +133,23 @@ Toolkit.Window = class Window extends Toolkit.Panel {
if (this.lastFocus != this)
this.lastFocus.focus();
else this.element.focus();
this.parent.bringToFront(this);
}
///////////////////////////// Private Methods /////////////////////////////
// Position the window in the center of the desktop
center() {
let bounds = this.getBounds();
let desktop = this.parent.getBounds();
this.contain(
Math.floor((desktop.width - bounds.width ) / 2),
Math.ceil ((desktop.height - bounds.height) / 2),
desktop, bounds
);
}
// Position the window using a tentative location in the desktop
contain(x, y, desktop, bounds, client) {
desktop = desktop || this.parent.getBounds();
@ -213,28 +201,6 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// The window is being displayed for the first time
firstShow() {
this.shown = true;
// Configure the initial size of the window
this.setClientSize(this.initialWidth, this.initialHeight);
// Configure the initial position of the window
if (!this.initialCenter)
return;
let bounds = this.getBounds();
let desktop = this.parent.getBounds();
this.contain(
Math.floor((desktop.width - bounds.width ) / 2),
Math.ceil ((desktop.height - bounds.height) / 2),
desktop, bounds
);
}
// Update display text with localized strings
localize() {
let title = this.title;
if (this.application)
title = this.application.translate(title, this);
this.titleElement.element.innerText = title;
}
// Focus lost event capture
@ -243,13 +209,6 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.element.setAttribute("focus", "false");
}
// Client pointer down event handler
onclientdown(e) {
e.preventDefault();
e.stopPropagation();
this.focus();
}
// Window close
onclose(e) {
for (let listener of this.closeListeners)
@ -258,52 +217,45 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Focus gained event capture
onfocus(e) {
// Configure element
this.element.setAttribute("focus", "true");
// Working variables
let target = e.target;
// Delegate focus to the most recently focused component
if (target == this.element)
target = this.lastFocus;
// The component is not visible: focus on self instead
if ("component" in target && !target.component.isVisible())
target = this.element;
// Configure instance fields
this.lastFocus = target;
// Transfer focus to the correct component
if (target != e.target)
target.focus();
this.parent.bringToFront(this);
}
// Key down event handler
// Key pressed event handler
onkeydown(e) {
// Processing by key
switch (e.key) {
default: return;
}
// Only listening for Escape while dragging
if (e.key != "Escape" || this.dragPointer === null)
return;
// Configure event
e.preventDefault();
e.stopPropagation();
// Restore the window's position
let desktop = this.parent.getBounds();
this.setLocation(
this.dragBounds.x - desktop.x,
this.dragBounds.y - desktop.y
);
// Restore the window's size
if (this.dragEdge != null)
this.setSize(this.dragClient.width, this.dragClient.height);
// Configure instance fields
this.element.releasePointerCapture(this.dragPointer);
this.dragPointer = null;
}
// Pointer down event handler
onpointerdown(e) {
// Configure event
e.preventDefault();
e.stopPropagation();
// Configure element
this.focus();
// Error checking
if (
e.button != 0 ||
@ -312,12 +264,19 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Configure instance fields
this.dragBounds = this.getBounds();
this.dragEdge = this.edge(e, this.dragBounds);
this.dragClient = this.client.getBounds();
this.dragCursor.x = e.x;
this.dragCursor.y = e.y;
this.dragEdge = this.edge(e, this.dragBounds);
// Don't perform a move if the cursor isn't in the title bar
let title = this.titleBar.getBounds();
if (this.dragEdge == null && e.y >= title.y + title.height)
return;
// Configure element
this.element.setPointerCapture(e.pointerId);
this.dragPointer = e.pointerId;
}
// Pointer move event handler
@ -337,12 +296,11 @@ Toolkit.Window = class Window extends Toolkit.Panel {
}
// Working variables
let rX = e.x - this.dragCursor.x;
let rY = e.y - this.dragCursor.y;
let bounds = this.getBounds();
let desktop = this.parent.getBounds();
let client = this.client.getBounds();
let minHeight = bounds.height - client.height;
let rX = e.x - this.dragCursor.x;
let rY = e.y - this.dragCursor.y;
let bounds = this.getBounds();
let desktop = this.parent.getBounds();
let client = this.client.getBounds();
// Move the window
if (this.dragEdge == null) {
@ -358,7 +316,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
if (this.dragEdge.startsWith("n")) {
let maxTop = desktop.height - client.y + bounds.y;
let top = this.dragBounds.y - desktop.y + rY;
let height = this.dragBounds.height - rY;
let height = this.dragClient.height - rY;
// Restrict window bounds
if (top > maxTop) {
@ -369,9 +327,9 @@ Toolkit.Window = class Window extends Toolkit.Panel {
height += top;
top = 0;
}
if (height < minHeight) {
top -= minHeight - height;
height = minHeight;
if (height < 0) {
top += height;
height = 0;
}
// Configure element
@ -383,7 +341,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
if (this.dragEdge.endsWith("w")) {
let maxLeft = desktop.width - 16;
let left = this.dragBounds.x - desktop.x + rX;
let width = this.dragBounds.width - rX;
let width = this.dragClient.width - rX;
// Restrict window bounds
if (left > maxLeft) {
@ -402,11 +360,11 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Resizing on the east edge
if (this.dragEdge.endsWith("e")) {
let width = this.dragBounds.width + rX;
let width = this.dragClient.width + rX;
// Restrict window bounds
width = Math.max(64, width);
width = Math.max(width, -this.dragBounds.x + 16);
width = Math.max(width, -this.dragClient.x + 16);
// Configure element
this.setWidth(width);
@ -414,10 +372,10 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Resizing on the south edge
if (this.dragEdge.startsWith("s")) {
let height = this.dragBounds.height + rY;
let height = this.dragClient.height + rY;
// Restrict window bounds
height = Math.max(minHeight, height);
height = Math.max(0, height);
// Configure element
this.setHeight(height);
@ -438,6 +396,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Configure element
this.element.releasePointerCapture(e.pointerId);
this.dragPointer = null;
}
};

292
app/windows/CPUWindow.js Normal file
View File

@ -0,0 +1,292 @@
"use strict";
// CPU register and disassembler display
(globalThis.CPUWindow = class CPUWindow extends Toolkit.Window {
// Static initializer
static initializer() {
// System register IDs
this.ADTRE = 25;
this.CHCW = 24;
this.ECR = 4;
this.EIPC = 0;
this.EIPSW = 1;
this.FEPC = 2;
this.FEPSW = 3;
this.PC = -1;
this.PIR = 6;
this.PSW = 5;
this.TKCW = 7;
// Program register names
this.PROGRAM = {
[ 2]: "hp",
[ 3]: "sp",
[ 4]: "gp",
[ 5]: "tp",
[31]: "lp"
};
}
// Object constructor
constructor(debug, options) {
super(debug.gui, options);
// Configure instance fields
this.address = 0xFFFFFFF0;
this.debug = debug;
// Configure properties
this.setProperty("sim", "");
// Configure elements
this.initDisassembler();
this.initSystemRegisters();
this.initProgramRegisters();
this.initWindow();
// Layout components
// Disassembler on the left
this.mainWrap.add(this.dasmWrap);
// Registers on the right
this.regs = this.newPanel({
layout: "grid",
rows : "max-content max-content auto"
});
this.regs.element.setAttribute("name", "wrap-registers");
// Splitter between disassembler and registers
this.mainSplit = this.newSplitter({
component : this.regs,
orientation: "vertical",
edge : "right",
name : "{cpu.mainSplit}"
});
this.mainSplit.element.setAttribute("name", "split-main");
this.mainSplit.element.style.width = "3px";
this.mainSplit.element.style.minWidth = "3px";
this.mainSplit.element.style.cursor = "ew-resize";
this.mainWrap.add(this.mainSplit);
// Registers on the right
this.mainWrap.add(this.regs);
// System registers on top
this.regs.add(this.sysWrap);
// Splitter between system registers and program registers
this.regsSplit = this.regs.add(this.newSplitter({
component : this.sysWrap,
orientation: "horizontal",
edge : "top",
name : "{cpu.regsSplit}"
}));
this.regsSplit.element.style.height = "3px";
this.regsSplit.element.style.minHeight = "3px";
this.regsSplit.element.style.cursor = "ns-resize";
// Program registers on the bottom
this.regs.add(this.proWrap);
}
// Initialize disassembler pane
initDisassembler() {
// Wrapping element to hide overflowing scrollbar
this.dasmWrap = this.newPanel({
layout : "grid",
overflowX: "hidden",
overflowY: "hidden"
});
this.dasmWrap.element.setAttribute("name", "wrap-disassembler");
// Main element
this.dasm = this.dasmWrap.add(this.newPanel({
focusable: true,
name : "{cpu.disassembler}",
overflowX: "auto",
overflowY: "hidden"
}));
this.dasm.element.setAttribute("name", "disassembler");
}
// Initialize program registers pane
initProgramRegisters() {
// Wrapping element to hide overflowing scrollbar
this.proWrap = this.newPanel({
layout : "grid",
overflow: "hidden"
});
this.proWrap.element.setAttribute("name", "wrap-program-registers");
this.proWrap.element.style.flexGrow = "1";
// Main element
this.proRegs = this.proWrap.add(this.newPanel({
overflowX: "auto",
overflowY: "scroll"
}));
this.proRegs.element.setAttribute("name", "program-registers");
// List of registers
this.proRegs.registers = {};
for (let x = 0; x <= 31; x++)
this.addRegister(false, x, CPUWindow.PROGRAM[x] || "r" + x);
}
// Initialize system registers pane
initSystemRegisters() {
// Wrapping element to hide overflowing scrollbar
this.sysWrap = this.newPanel({
layout : "grid",
overflow: "hidden"
});
this.sysWrap.element.setAttribute("name", "wrap-system-registers");
// Main element
this.sysRegs = this.sysWrap.add(this.newPanel({
overflowX: "auto",
overflowY: "scroll"
}));
this.sysRegs.element.setAttribute("name", "system-registers");
// List of registers
this.sysRegs.registers = {};
this.addRegister(true, CPUWindow.PC , "PC" );
this.addRegister(true, CPUWindow.PSW , "PSW" );
this.addRegister(true, CPUWindow.ADTRE, "ADTRE");
this.addRegister(true, CPUWindow.CHCW , "CHCW" );
this.addRegister(true, CPUWindow.ECR , "ECR" );
this.addRegister(true, CPUWindow.EIPC , "EIPC" );
this.addRegister(true, CPUWindow.EIPSW, "EIPSW");
this.addRegister(true, CPUWindow.FEPC , "FEPC" );
this.addRegister(true, CPUWindow.FEPSW, "FEPSW");
this.addRegister(true, CPUWindow.PIR , "PIR" );
this.addRegister(true, CPUWindow.TKCW , "TKCW" );
this.addRegister(true, 29 , "29" );
this.addRegister(true, 30 , "30" );
this.addRegister(true, 31 , "31" );
this.sysRegs.registers[CPUWindow.PSW].setExpanded(true);
}
// Initialize window and client
initWindow() {
// Configure element
this.element.setAttribute("window", "cpu");
// Configure body
this.body.element.setAttribute("filter", "");
// Configure client
this.client.setLayout("grid", {
columns: "auto"
});
this.client.addResizeListener(b=>this.onresize(b));
// Configure main wrapper
this.mainWrap = this.client.add(this.newPanel({
layout : "grid",
columns: "auto max-content max-content"
}));
this.mainWrap.element.setAttribute("name", "wrap-main");
}
///////////////////////////// Public Methods //////////////////////////////
// The window is being displayed for the first time
firstShow() {
super.firstShow();
this.center();
this.mainSplit.measure();
this.regsSplit.measure();
this.refresh();
}
///////////////////////////// Package Methods /////////////////////////////
// Update the display with current emulation data
refresh(clientHeight, lineHeight, registers) {
if (!registers) {
this.debug.core.postMessage({
command: "GetRegisters",
debug : "CPU",
sim : this.debug.sim
});
}
}
///////////////////////////// Message Methods /////////////////////////////
// Message received
message(msg) {
switch (msg.command) {
case "GetRegisters": this.getRegisters(msg); break;
case "SetRegister" : this.setRegister (msg); break;
}
}
// Retrieved all register values
getRegisters(msg) {
this.sysRegs.registers[CPUWindow.PC ]
.setValue(msg.pc, msg.pcFrom, msg.pcTo);
this.sysRegs.registers[CPUWindow.PSW ].setValue(msg.psw );
this.sysRegs.registers[CPUWindow.ADTRE].setValue(msg.adtre);
this.sysRegs.registers[CPUWindow.CHCW ].setValue(msg.chcw );
this.sysRegs.registers[CPUWindow.ECR ].setValue(msg.ecr );
this.sysRegs.registers[CPUWindow.EIPC ].setValue(msg.eipc );
this.sysRegs.registers[CPUWindow.EIPSW].setValue(msg.eipsw);
this.sysRegs.registers[CPUWindow.FEPC ].setValue(msg.fepc );
this.sysRegs.registers[CPUWindow.FEPSW].setValue(msg.fepsw);
this.sysRegs.registers[CPUWindow.PIR ].setValue(msg.pir );
this.sysRegs.registers[CPUWindow.TKCW ].setValue(msg.tkcw );
this.sysRegs.registers[29 ].setValue(msg.sr29 );
this.sysRegs.registers[30 ].setValue(msg.sr30 );
this.sysRegs.registers[31 ].setValue(msg.sr31 );
for (let x = 0; x <= 31; x++)
this.proRegs.registers[x].setValue(msg.program[x]);
}
// Modified a register value
setRegister(msg) {
(msg.type == "program" ? this.proRegs : this.sysRegs)
.registers[msg.id].setValue(msg.value);
}
///////////////////////////// Private Methods /////////////////////////////
// Insert a register control to a register list
addRegister(system, id, name) {
let list = system ? this.sysRegs : this.proRegs;
let reg = new CPUWindow.Register(this.debug, list, system, id, name);
list.registers[id] = reg;
list.add(reg);
if (reg.expands)
list.add(reg.expansion);
}
// Resize event handler
onresize(bounds) {
if (!this.isVisible())
return;
//this.regs.setHeight(bounds.height);
this.mainSplit.measure();
this.regsSplit.measure();
}
}).initializer();

View File

@ -8,47 +8,68 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
super(debug.gui, options);
// Configure instance fields
this.address = 0xFFFFFFF0;
this.address = 0x05000000;
this.debug = debug;
this.rows = [new MemoryWindow.Row(this.client)];
this.rows = [];
// Configure element
this.element.setAttribute("window", "memory");
this.element.addEventListener("wheel", e=>this.onwheel(e));
// Configure body
this.body.element.setAttribute("filter", "");
// Configure client
this.client.setLayout("grid", { columns: "repeat(17, max-content)" });
this.client.element.style.gridAutoRows = "max-content";
this.client.setOverflow("auto", "hidden");
this.client.setLayout("grid");
this.client.addResizeListener(b=>this.refresh(b.height));
// Wrapping element to hide overflowing scrollbar
this.hexWrap = this.client.add(this.newPanel({
layout : "grid",
columns: "auto"
}));
this.hexWrap.element.setAttribute("name", "wrap-hex");
// Configure hex viewer
this.hex = this.hexWrap.add(this.client.newPanel({
focusable: true,
layout : "block",
hollow : false,
overflowX: "auto",
overflowY: "hidden"
}));
this.hex.element.setAttribute("role", "grid");
this.hex.element.setAttribute("name", "hex");
this.hex.element.addEventListener("wheel", e=>this.onwheel(e));
// Configure properties
this.setProperty("sim", "");
}
///////////////////////////// Public Methods //////////////////////////////
// The window is being displayed for the first time
firstShow() {
super.firstShow();
this.seek(this.address, Math.floor(this.lines(true) / 3));
this.rows.push(this.hex.add(new MemoryWindow.Row(this.hex)));
this.application.addComponent(this);
}
///////////////////////////// Package Methods /////////////////////////////
// Update display text with localized strings
localize() {
let hex = "";
if (this.application)
hex = this.application.translate("{memory.hexEditor}");
this.hex.element.setAttribute("aria-label", hex);
}
// Update the display with current emulation data
refresh(clientHeight, lineHeight) {
clientHeight = clientHeight || this.client.getBounds().height;
lineHeight = lineHeight || this.lineHeight();
let rowCount = this.lines(false, clientHeight, lineHeight);
// Showing for the first time
if (this.address < 0)
this.seek(-this.address, Math.floor(rowCount / 3));
refresh(gridHeight, lineHeight) {
// Do nothing while closed
if (!this.isVisible())
return;
// Working variables
gridHeight = gridHeight || this.hex.getBounds().height;
lineHeight = lineHeight || this.lineHeight();
let rowCount = this.lines(false, gridHeight, lineHeight);
// Request bus data from the WebAssembly core
this.debug.core.postMessage({
@ -61,9 +82,9 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
// Configure elements
for (let y = this.rows.length; y < rowCount; y++)
this.rows[y] = new MemoryWindow.Row(this.client);
this.rows[y] = this.hex.add(new MemoryWindow.Row(this.hex));
for (let y = rowCount; y < this.rows.length; y++)
this.rows[y].remove();
this.hex.remove(this.rows[y]);
if (this.rows.length > rowCount)
this.rows.splice(rowCount, this.rows.length - rowCount);
}
@ -93,17 +114,23 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
///////////////////////////// Private Methods /////////////////////////////
// The window is being displayed for the first time
firstShow() {
super.firstShow();
this.center();
}
// Determine the height in pixels of one row of output
lineHeight() {
return Math.max(1, this.rows[0].address.getBounds().height);
return Math.max(10, this.rows[0].address.getBounds().height);
}
// Determine the number of rows of output
lines(fullyVisible, clientHeight, lineHeight) {
clientHeight = clientHeight || this.client.getBounds().height;
lineHeight = lineHeight || this.lineHeight();
let ret = clientHeight / lineHeight;
ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret);
lines(fullyVisible, gridHeight, lineHeight) {
gridHeight = gridHeight || this.client.getBounds().height;
lineHeight = lineHeight || this.lineHeight();
let ret = gridHeight / lineHeight;
ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret);
return Math.max(1, ret);
}
@ -127,29 +154,24 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
// Processing by key
else switch (e.key) {
case "ArrowDown":
this.address = (this.address + 16 & 0xFFFFFFFF) >>> 0;
this.refresh();
break;
case "ArrowUp":
this.address = (this.address - 16 & 0xFFFFFFFF) >>> 0;
this.refresh();
break;
case "PageUp":
this.address = (this.address - 16 * this.lines(true) &
0xFFFFFFFF) >>> 0;
this.refresh();
break;
case "PageDown":
this.address = (this.address + 16 * this.lines(true) &
0xFFFFFFFF) >>> 0;
this.refresh();
break;
default: return super.onkeydown(e);
}
@ -165,6 +187,12 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
let mag = Math.abs (e.deltaY);
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL)
mag = Math.max(1, Math.floor(mag / lineHeight));
// Configure element
e.preventDefault();
e.stopPropagation();
// Specify the new address
this.address = (this.address + sign * mag * 16 & 0xFFFFFFFF) >>> 0;
this.refresh(null, lineHeight);
}
@ -177,23 +205,34 @@ globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
};
// One row of output
MemoryWindow.Row = class Row {
MemoryWindow.Row = class Row extends Toolkit.Panel {
// Object constructor
constructor(client) {
constructor(parent) {
super(parent.application, {
layout : "grid",
columns : "repeat(17, max-content)",
hollow : false,
overflowX: "visible",
overflowY: "visible"
});
// Configure instance fields
this.bytes = new Array(16);
this.client = client;
this.bytes = new Array(16);
// Configure element
this.element.setAttribute("role", "row");
// Address label
this.address = client.add(client.newLabel({ text: "\u00a0" }));
this.address = this.add(parent.newLabel({ text: "\u00a0" }));
this.address.element.setAttribute("role", "gridcell");
this.address.element.setAttribute("name", "address");
// Byte labels
for (let x = 0; x < 16; x++) {
let lbl = this.bytes[x] =
client.add(client.newLabel({ text: "\u00a0" }));
this.add(parent.newLabel({ text: "\u00a0" }));
lbl.element.setAttribute("role", "gridcell");
lbl.element.setAttribute("name", "byte");
}
@ -203,13 +242,6 @@ MemoryWindow.Row = class Row {
///////////////////////////// Package Methods /////////////////////////////
// Remove components from the memory window
remove() {
this.client.remove(this.address);
for (let bytel of this.bytes)
this.client.remove(bytel);
}
// Update the output labels with emulation state content
update(address, bytes, offset) {
this.address.setText(

496
app/windows/Register.js Normal file
View File

@ -0,0 +1,496 @@
"use strict";
// List item for CPU window register lists
(CPUWindow.Register = class Register extends Toolkit.Panel {
// Static initializer
static initializer() {
let buffer = new ArrayBuffer(4);
this.F32 = new Float32Array(buffer);
this.S32 = new Int32Array (buffer);
this.U32 = new Uint32Array (buffer);
}
// Object constructor
constructor(debug, parent, system, id, name) {
super(parent.application, {
layout : "grid",
columns : "auto max-content",
overflowX: "visible",
overflowY: "visible"
});
// Configure instance fields
this.debug = debug;
this.expanded = false;
this.expansion = null;
this.fields = {};
this.format = "hex";
this.id = id;
this.name = name;
this.parent = parent;
this.system = system;
this.value = 0x00000000;
// Determine whether the register has expansion fields
this.expands = !system;
switch (id) {
case CPUWindow.CHCW:
case CPUWindow.ECR:
case CPUWindow.EIPSW:
case CPUWindow.FEPSW:
case CPUWindow.PC:
case CPUWindow.PIR:
case CPUWindow.PSW:
case CPUWindow.TKCW:
this.expands = true;
}
// Configure element
this.element.setAttribute("name", "register");
this.element.setAttribute("format", this.format);
// Name/expansion "check box"
this.chkExpand = this.add(this.newCheckBox({
enabled: this.expands,
text : name
}));
this.chkExpand.element.setAttribute("name", "expand");
this.chkExpand.element.setAttribute("aria-expanded", "false");
this.chkExpand.addChangeListener(e=>
this.setExpanded(this.chkExpand.isChecked()));
// Value text box
this.txtValue = this.add(this.newTextBox({ text: "00000000" }));
this.txtValue.element.setAttribute("name", "value");
this.txtValue.addCommitListener(e=>this.onvalue());
// Expansion controls
if (!system)
this.expansionProgram();
else switch (id) {
case CPUWindow.CHCW : this.expansionCHCW(); break;
case CPUWindow.ECR : this.expansionECR (); break;
case CPUWindow.EIPSW:
case CPUWindow.FEPSW:
case CPUWindow.PSW : this.expansionPSW (); break;
case CPUWindow.PC : this.expansionPC (); break;
case CPUWindow.PIR : this.expansionPIR (); break;
case CPUWindow.TKCW : this.expansionTKCW(); break;
}
if (this.expands) {
this.chkExpand.element.setAttribute(
"aria-controls", this.expansion.id);
}
}
///////////////////////////// Static Methods //////////////////////////////
// Force a 32-bit integer to be signed
static asSigned(value) {
this.U32[0] = value >>> 0;
return this.S32[0];
}
// Interpret a 32-bit integer as a float
static intBitsToFloat(value) {
this.U32[0] = value;
value = this.F32[0];
return Number.isFinite(value) ? value : 0;
}
// Interpret a float as a 32-bit integer
static floatToIntBits(value) {
if (!Number.isFinite(value))
return 0;
this.F32[0] = value;
return this.U32[0];
}
///////////////////////////// Package Methods /////////////////////////////
// Specify whether the expansion fields are visible
setExpanded(expanded) {
this.expanded = expanded = !!expanded;
this.expansion.setVisible(expanded);
this.chkExpand.setChecked(expanded);
this.chkExpand.element.setAttribute("aria-expanded", expanded);
}
// Specify the display mode of the register value
setFormat(format) {
this.format = format;
this.setValue(this.value);
this.element.setAttribute("format", format.replace("_", ""));
}
// Update the value of the register
setValue(value, pcFrom, pcTo) {
this.value = value;
// Value text box
let text;
switch (this.format) {
case "float":
text = CPUWindow.Register.intBitsToFloat(value).toString();
if (text.indexOf(".") == -1)
text += ".0";
break;
case "hex":
text = ("0000000" +
(value >>> 0).toString(16).toUpperCase()).slice(-8);
break;
case "signed":
text = CPUWindow.Register.asSigned(value).toString();
break;
case "unsigned":
text = (value >>> 0).toString();
}
this.txtValue.setText(text);
// Expansion fields
for (let field of Object.values(this.fields)) {
switch (field.type) {
case "bit":
field.setChecked(value >> field.bit & 1);
break;
case "decimal":
field.setText(value >> field.bit & field.mask);
break;
case "hex":
let digits = Math.max(1, Math.ceil(field.width / 4));
field.setText(("0".repeat(digits) +
(value >> field.bit & field.mask)
.toString(16).toUpperCase()
).slice(-digits));
}
}
// Special fields for PC
if (pcFrom === undefined)
return;
this.txtFrom.setText(("0000000" +
(pcFrom >>> 0).toString(16).toUpperCase()).slice(-8));
this.txtTo .setText(("0000000" +
(pcTo >>> 0).toString(16).toUpperCase()).slice(-8));
}
///////////////////////////// Private Methods /////////////////////////////
// Add a field component to the expansion area
addField(type, name, bit, width, readonly) {
let field, label, panel;
// Processing by type
switch (type) {
// Bit
case "bit":
field = this.newCheckBox({
enabled: !readonly,
text : name
});
field.addChangeListener(e=>this.onbit(field));
this.expansion.add(field);
break;
// Decimal number
case "decimal":
// Field
field = this.newTextBox({
enabled: !readonly,
name : name
});
field.addCommitListener(e=>this.onnumber(field));
label = this.newLabel({
label: true,
text : name
});
label.element.htmlFor = field.id;
if (readonly)
label.element.setAttribute("aria-disabled", "true");
// Enclose in a panel
panel = this.newPanel({
layout : "flex",
alignCross: "center",
alignMain : "start",
direction : "row"
});
panel.element.setAttribute("name", name);
panel.add(label);
panel.add(field);
this.expansion.add(panel);
break;
// Hexadecimal number
case "hex":
field = this.newTextBox({
enabled: !readonly,
name : name
});
field.addCommitListener(e=>this.onnumber(field));
label = this.newLabel({
label: true,
text : name
});
label.element.htmlFor = field.id;
if (readonly)
label.element.setAttribute("aria-disabled", "true");
this.expansion.add(label);
this.expansion.add(field);
}
// Configure field
field.bit = bit;
field.mask = (1 << width) - 1;
field.type = type;
field.width = width;
this.fields[name] = field;
}
// Expansion controls for CHCW
expansionCHCW() {
// Expansion area
this.expansion = this.newPanel({
layout : "block",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("name", "expansion");
this.expansion.element.setAttribute("register", "chcw");
// Fields
this.addField("bit", "ICE", 1, 1, false);
}
// Expansion controls for ECR
expansionECR() {
// Expansion area
this.expansion = this.newPanel({
layout : "grid",
columns : "max-content auto",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("name", "expansion");
this.expansion.element.setAttribute("register", "ecr");
this.expansion.element.style.justifyContent = "start";
// Fields
this.addField("hex", "FECC", 16, 16, false);
this.addField("hex", "EICC", 0, 16, false);
}
// Expansion controls for PC
expansionPC() {
// Expansion area
this.expansion = this.newPanel({
layout : "grid",
columns : "auto max-content",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("name", "expansion");
// From text box
let lbl = this.expansion.add(this.newLabel({
localized: true,
text : "{cpu.pcFrom}"
}));
lbl.element.setAttribute("name", "indent");
this.txtFrom = this.expansion.add(this.newTextBox());
this.txtFrom.element.setAttribute("name", "value");
// To text box
lbl = this.expansion.add(this.newLabel({
localized: true,
text : "{cpu.pcTo}"
}));
lbl.element.setAttribute("name", "indent");
this.txtTo = this.expansion.add(this.newTextBox());
this.txtTo.element.setAttribute("name", "value");
}
// Expansion controls for PIR
expansionPIR() {
// Expansion area
this.expansion = this.newPanel({
layout : "grid",
columns : "max-content auto",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("name", "expansion");
this.expansion.element.setAttribute("register", "pir");
// Fields
this.addField("hex", "PT", 0, 16, true);
}
// Expansion controls for program registers
expansionProgram() {
// Expansion area
this.expansion = this.newPanel({
layout : "grid",
columns : "auto",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("role", "radiogroup");
this.expansion.element.setAttribute("name", "expansion");
this.expansion.element.setAttribute("register", "program");
this.expansion.element.style.justifyContent = "start";
// Format selections
let group = new Toolkit.ButtonGroup();
for (let opt of [ "hex", "signed", "unsigned", "float_" ]) {
let fmt = group.add(this.expansion.add(this.newRadioButton({
checked: opt == "hex",
text : "{cpu." + opt + "}",
})));
fmt.addChangeListener(
(opt=>e=>this.setFormat(opt))
(opt.replace("_", ""))
);
}
}
// Expansion controls for EIPSW, FEPSW and PSW
expansionPSW() {
// Expansion area
this.expansion = this.newPanel({
layout : "grid",
columns : "max-content auto",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("name", "expansion");
this.expansion.element.setAttribute("register", "psw");
this.expansion.element.style.justifyContent = "start";
// Fields
this.addField("bit" , "CY" , 3, 1, false);
this.addField("bit" , "FRO", 9, 1, false);
this.addField("bit" , "OV" , 2, 1, false);
this.addField("bit" , "FIV", 8, 1, false);
this.addField("bit" , "S" , 1, 1, false);
this.addField("bit" , "FZD", 7, 1, false);
this.addField("bit" , "Z" , 0, 1, false);
this.addField("bit" , "FOV", 6, 1, false);
this.addField("bit" , "NP" , 15, 1, false);
this.addField("bit" , "FUD", 5, 1, false);
this.addField("bit" , "EP" , 14, 1, false);
this.addField("bit" , "FPR", 4, 1, false);
this.addField("bit" , "ID" , 12, 1, false);
this.addField("decimal", "I" , 16, 4, false);
this.addField("bit" , "AE" , 13, 1, false);
}
// Expansion controls for TKCW
expansionTKCW() {
// Expansion area
this.expansion = this.newPanel({
layout : "grid",
columns : "max-content auto",
overflowX: "visible",
overflowY: "visible",
visible : false
});
this.expansion.element.setAttribute("name", "expansion");
this.expansion.element.setAttribute("register", "tkcw");
this.expansion.element.style.justifyContent = "start";
// Fields
this.addField("bit" , "FIT", 7, 1, true);
this.addField("bit" , "FUT", 4, 1, true);
this.addField("bit" , "FZT", 6, 1, true);
this.addField("bit" , "FPT", 3, 1, true);
this.addField("bit" , "FVT", 5, 1, true);
this.addField("bit" , "OTM", 8, 1, true);
this.addField("bit" , "RDI", 2, 1, true);
this.addField("decimal", "RD" , 0, 2, true);
}
// Bit check box change event handler
onbit(field) {
let mask = 1 << field.bit;
let value = this.value;
if (field.isChecked())
value = (value | mask & 0xFFFFFFFF) >>> 0;
else value = (value & ~mask & 0xFFFFFFFF) >>> 0;
this.setRegister(value);
}
// Number text box commit event handler
onnumber(field) {
let value = parseInt(field.getText(),
field.type == "decimal" ? 10 : 16);
if (value == NaN)
value = this.value;
this.setRegister((
this.value & ~(field.mask << field.bit) |
(value & field.mask) << field.bit
) >>> 0);
}
// Value text box commit event handler
onvalue() {
// Process the entered value
let value = this.txtValue.getText();
switch (this.format) {
case "float":
value = parseFloat(value);
if (Number.isFinite(value))
value = CPUWindow.Register.floatToIntBits(value);
break;
case "hex":
value = parseInt(value, 16);
break;
case "signed":
case "unsigned":
value = parseInt(value);
}
// Update the value
if (!Number.isFinite(value))
this.setValue(this.value);
else this.setRegister((value & 0xFFFFFFFF) >>> 0);
}
// Update the value of the register
setRegister(value) {
this.debug.core.postMessage({
command: "SetRegister",
debug : "CPU",
id : this.id,
sim : this.debug.sim,
type : this.system ? this.id == -1 ? "pc" : "system" : "program",
value : value
});
}
}).initializer();

View File

@ -1,15 +1,21 @@
/* This file is included into vb.c and cannot be compiled on its own. */
#ifdef VBAPI
/**************************** Component Functions ****************************/
/* Read a data unit from a memory buffer */
static int32_t busReadMemory(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 */
#ifdef VB_BIGENDIAN
#else
switch (type) {
case VB_S8 : return (int8_t) *mem;
case VB_U8 : return *mem;
@ -18,16 +24,6 @@ static int32_t busReadMemory(uint8_t *mem, int type) {
}
return (int32_t ) mem[3] << 24 | (uint32_t) mem[2] << 16 |
(uint32_t) mem[1] << 8 | mem[0];
/* Little-endian implementation */
#else
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;
#endif
}
@ -60,8 +56,16 @@ static int32_t busRead(VB *emu, uint32_t address, int type, int debug) {
/* Write a data unit to a memory buffer */
static void busWriteMemory(uint8_t *mem, int type, int32_t value) {
/* 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 */
#ifdef VB_BIGENDIAN
#else
switch (type) {
case VB_S32:
mem[3] = value >> 24;
@ -72,14 +76,6 @@ static void busWriteMemory(uint8_t *mem, int type, int32_t value) {
mem[1] = value >> 8;
}
mem[0] = value;
/* Little-endian implementation */
#else
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;
#endif
}
@ -114,6 +110,4 @@ static void busWrite(
}
#endif /* VBAPI */

View File

@ -29,8 +29,13 @@ static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 };
/******************************* API Functions *******************************/
/* Retrieve the value of PC */
uint32_t vbGetProgramCounter(VB *emu) {
return emu->cpu.pc;
uint32_t vbGetProgramCounter(VB *emu, int type) {
switch (type) {
case VB_PC : return emu->cpu.pc;
case VB_PC_FROM: return emu->cpu.pcFrom;
case VB_PC_TO : return emu->cpu.pcTo;
}
return 0;
}
/* Retrieve the value of a program register */
@ -64,6 +69,7 @@ uint32_t vbGetSystemRegister(VB *emu, int id) {
case VB_PIR : return 0x00005346;
case VB_TKCW : return 0x000000E0;
case 29 : return emu->cpu.sr29;
case 30 : return 0x00000004;
case 31 : return emu->cpu.sr31;
case VB_ECR : return
(uint32_t) emu->cpu.ecr.fecc << 16 | emu->cpu.ecr.eicc;
@ -109,28 +115,29 @@ int32_t vbRead(VB *emu, uint32_t address, int type, int debug) {
/* Simulate a hardware reset */
void vbReset(VB *emu) {
uint32_t x;
uint32_t x; /* Iterator */
/* Initialize CPU registers */
/* CPU registers */
emu->cpu.pc = 0xFFFFFFF0;
vbSetSystemRegister(emu, VB_ECR, 0x0000FFF0);
vbSetSystemRegister(emu, VB_PSW, 0x00008000);
/* Initialize extra CPU registers (the hardware does not do this) */
/* Extra CPU registers (the hardware does not do this) */
for (x = 0; x < 32; x++)
emu->cpu.program[x] = 0;
emu->cpu.adtre = 0;
emu->cpu.eipc = 0;
emu->cpu.eipsw = 0;
emu->cpu.fepc = 0;
emu->cpu.fepsw = 0;
emu->cpu.sr29 = 0;
emu->cpu.sr31 = 0;
emu->cpu.program[x] = 0x00000000;
emu->cpu.adtre = 0x00000000;
emu->cpu.eipc = 0x00000000;
emu->cpu.eipsw = 0x00000000;
emu->cpu.fepc = 0x00000000;
emu->cpu.fepsw = 0x00000000;
emu->cpu.sr29 = 0x00000000;
emu->cpu.sr31 = 0x00000000;
emu->cpu.pcFrom = 0xFFFFFFF0;
emu->cpu.pcTo = 0xFFFFFFF0;
/* Erase WRAM (the hardware does not do this) */
/* WRAM (the hardware does not do this) */
for (x = 0; x < 0x10000; x++)
emu->wram[x] = 0;
emu->wram[x] = 0x00;
}
/* Specify a new value for PC */

View File

@ -17,7 +17,7 @@ extern "C" {
/********************************* Constants *********************************/
/* Value types */
/* Memory access types */
#define VB_S8 0
#define VB_U8 1
#define VB_S16 2
@ -36,6 +36,11 @@ extern "C" {
#define VB_PSW 5
#define VB_TKCW 7
/* PC types */
#define VB_PC 0
#define VB_PC_FROM 1
#define VB_PC_TO 2
/*********************************** Types ***********************************/
@ -96,11 +101,13 @@ struct VB {
/* Other registers */
uint32_t pc; /* Program counter */
uint32_t pcFrom; /* Source of most recent jump */
uint32_t pcTo; /* Destination of most recent jump */
int32_t program[32]; /* program registers */
/* Other fields */
uint32_t clocks; /* Clocks until next action */
uint8_t fatch; /* Index of fetch unit */
uint8_t fetch; /* Index of fetch unit */
uint8_t state; /* Operations state */
} cpu;
@ -112,7 +119,7 @@ struct VB {
/**************************** Function Prototypes ****************************/
VBAPI uint32_t vbGetProgramCounter (VB *emu);
VBAPI uint32_t vbGetProgramCounter (VB *emu, int type);
VBAPI int32_t vbGetProgramRegister (VB *emu, int id);
VBAPI void* vbGetROM (VB *emu, uint32_t *size);
VBAPI void* vbGetSRAM (VB *emu, uint32_t *size);

View File

@ -1,7 +1,7 @@
.PHONY: help
help:
@echo
@echo "Virtual Boy Emulator - August 30, 2021"
@echo "Virtual Boy Emulator - September 1, 2021"
@echo
@echo "Target build environment is any Debian with the following packages:"
@echo " emscripten"
@ -37,12 +37,12 @@ clean:
core:
@gcc core/vb.c -I core \
-fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
@gcc core/vb.c -I core -D VB_BIGENDIAN \
@gcc core/vb.c -I core -D VB_LITTLEENDIAN \
-fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90
.PHONY: wasm
wasm:
@emcc -o core.wasm wasm/wasm.c core/vb.c -Icore \
@emcc -o core.wasm wasm/wasm.c core/vb.c -Icore -D VB_LITTLEENDIAN \
--no-entry -O2 -flto -s WASM=1 -s EXPORTED_RUNTIME_METHODS=[] \
-s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing
@rm -f *.wasm.tmp*

View File

@ -36,8 +36,8 @@ EMSCRIPTEN_KEEPALIVE void ReadBuffer(
//////////////////////////////// Core Commands ////////////////////////////////
// Retrieve the value of PC
EMSCRIPTEN_KEEPALIVE uint32_t GetProgramCounter(int sim) {
return vbGetProgramCounter(&sims[sim]);
EMSCRIPTEN_KEEPALIVE uint32_t GetProgramCounter(int sim, int type) {
return vbGetProgramCounter(&sims[sim], type);
}
// Retrieve the value of a program register