diff --git a/.gitattributes b/.gitattributes index a7a4c68..68aa440 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,4 @@ *.class binary *.dll binary *.wasm binary +*.woff2 binary diff --git a/app/App.js b/app/App.js index c676c02..c1f58ed 100644 --- a/app/App.js +++ b/app/App.js @@ -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 diff --git a/app/Debugger.js b/app/Debugger.js index 846aaf9..6beeb24 100644 --- a/app/Debugger.js +++ b/app/Debugger.js @@ -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(); } diff --git a/app/Emulator.js b/app/Emulator.js index 4c2ebae..af9cd51 100644 --- a/app/Emulator.js +++ b/app/Emulator.js @@ -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); diff --git a/app/_boot.js b/app/_boot.js index 7ceb52d..5b7983e 100644 --- a/app/_boot.js +++ b/app/_boot.js @@ -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(); }; diff --git a/app/locale/en-US.js b/app/locale/en-US.js index dc506e1..7cac528 100644 --- a/app/locale/en-US.js +++ b/app/locale/en-US.js @@ -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", diff --git a/app/theme/Inconsolata_SemiExpanded-Medium.woff2 b/app/theme/Inconsolata_SemiExpanded-Medium.woff2 new file mode 100644 index 0000000..db38b3a Binary files /dev/null and b/app/theme/Inconsolata_SemiExpanded-Medium.woff2 differ diff --git a/app/theme/Inconsolata_SemiExpanded-Regular.woff2 b/app/theme/Inconsolata_SemiExpanded-Regular.woff2 deleted file mode 100644 index 4b4cec4..0000000 Binary files a/app/theme/Inconsolata_SemiExpanded-Regular.woff2 and /dev/null differ diff --git a/app/theme/dark.css b/app/theme/dark.css index 8f618ee..a79bbd7 100644 --- a/app/theme/dark.css +++ b/app/theme/dark.css @@ -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; } \ No newline at end of file diff --git a/app/theme/kiosk.css b/app/theme/kiosk.css index 6e634a3..8691fe6 100644 --- a/app/theme/kiosk.css +++ b/app/theme/kiosk.css @@ -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); } diff --git a/app/theme/light.css b/app/theme/light.css index 5d766d7..c3c548a 100644 --- a/app/theme/light.css +++ b/app/theme/light.css @@ -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; } diff --git a/app/theme/virtual.css b/app/theme/virtual.css index 9e55a6e..c4b7013 100644 --- a/app/theme/virtual.css +++ b/app/theme/virtual.css @@ -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"); } diff --git a/app/toolkit/Application.js b/app/toolkit/Application.js index 2f3448f..0d219c9 100644 --- a/app/toolkit/Application.js +++ b/app/toolkit/Application.js @@ -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); diff --git a/app/toolkit/Button.js b/app/toolkit/Button.js index 8d24fff..4f9ad8e 100644 --- a/app/toolkit/Button.js +++ b/app/toolkit/Button.js @@ -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); } }; diff --git a/app/toolkit/ButtonGroup.js b/app/toolkit/ButtonGroup.js new file mode 100644 index 0000000..6fe3693 --- /dev/null +++ b/app/toolkit/ButtonGroup.js @@ -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; + } + +}; diff --git a/app/toolkit/CheckBox.js b/app/toolkit/CheckBox.js new file mode 100644 index 0000000..ed70f27 --- /dev/null +++ b/app/toolkit/CheckBox.js @@ -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); + } + +}; diff --git a/app/toolkit/Component.js b/app/toolkit/Component.js index 0365bff..2224ad8 100644 --- a/app/toolkit/Component.js +++ b/app/toolkit/Component.js @@ -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"); + } } diff --git a/app/toolkit/Label.js b/app/toolkit/Label.js index 7703e8e..e670cb6 100644 --- a/app/toolkit/Label.js +++ b/app/toolkit/Label.js @@ -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 ///////////////////////////// diff --git a/app/toolkit/MenuBar.js b/app/toolkit/MenuBar.js index 662ceb1..8ebe6a9 100644 --- a/app/toolkit/MenuBar.js +++ b/app/toolkit/MenuBar.js @@ -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", diff --git a/app/toolkit/Panel.js b/app/toolkit/Panel.js index c7f06b5..4ea24ff 100644 --- a/app/toolkit/Panel.js +++ b/app/toolkit/Panel.js @@ -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); } diff --git a/app/toolkit/RadioButton.js b/app/toolkit/RadioButton.js new file mode 100644 index 0000000..c18d3b9 --- /dev/null +++ b/app/toolkit/RadioButton.js @@ -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); + } + +}; diff --git a/app/toolkit/Splitter.js b/app/toolkit/Splitter.js new file mode 100644 index 0000000..12fe4a7 --- /dev/null +++ b/app/toolkit/Splitter.js @@ -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; + } + +}; diff --git a/app/toolkit/TextBox.js b/app/toolkit/TextBox.js new file mode 100644 index 0000000..3655d2e --- /dev/null +++ b/app/toolkit/TextBox.js @@ -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); + } + +}; diff --git a/app/toolkit/Window.js b/app/toolkit/Window.js index e891665..650fd38 100644 --- a/app/toolkit/Window.js +++ b/app/toolkit/Window.js @@ -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; } }; diff --git a/app/windows/CPUWindow.js b/app/windows/CPUWindow.js new file mode 100644 index 0000000..3cae9be --- /dev/null +++ b/app/windows/CPUWindow.js @@ -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(); diff --git a/app/windows/MemoryWindow.js b/app/windows/MemoryWindow.js index f15d848..7158624 100644 --- a/app/windows/MemoryWindow.js +++ b/app/windows/MemoryWindow.js @@ -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( diff --git a/app/windows/Register.js b/app/windows/Register.js new file mode 100644 index 0000000..5b33c76 --- /dev/null +++ b/app/windows/Register.js @@ -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(); diff --git a/core/bus.c b/core/bus.c index ce253e5..8b51b32 100644 --- a/core/bus.c +++ b/core/bus.c @@ -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 */ diff --git a/core/vb.c b/core/vb.c index a72a835..5d0e2be 100644 --- a/core/vb.c +++ b/core/vb.c @@ -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 */ diff --git a/core/vb.h b/core/vb.h index e6c9921..874cd1c 100644 --- a/core/vb.h +++ b/core/vb.h @@ -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); diff --git a/makefile b/makefile index cc95c2a..506715a 100644 --- a/makefile +++ b/makefile @@ -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* diff --git a/wasm/wasm.c b/wasm/wasm.c index d57ed8d..4c6a39a 100644 --- a/wasm/wasm.c +++ b/wasm/wasm.c @@ -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