From ff07e1907f2cf90a59ca76d005d9898fddf8cd2f Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Thu, 2 Sep 2021 00:16:22 +0000 Subject: [PATCH] Adding registers panes of CPU window --- .gitattributes | 1 + app/App.js | 6 +- app/Debugger.js | 15 +- app/Emulator.js | 50 +- app/_boot.js | 24 +- app/locale/en-US.js | 15 +- .../Inconsolata_SemiExpanded-Medium.woff2 | Bin 0 -> 40712 bytes .../Inconsolata_SemiExpanded-Regular.woff2 | Bin 40364 -> 0 bytes app/theme/dark.css | 36 +- app/theme/kiosk.css | 417 ++++++++++++--- app/theme/light.css | 36 +- app/theme/virtual.css | 38 +- app/toolkit/Application.js | 1 - app/toolkit/Button.js | 43 +- app/toolkit/ButtonGroup.js | 44 ++ app/toolkit/CheckBox.js | 190 +++++++ app/toolkit/Component.js | 50 +- app/toolkit/Label.js | 32 +- app/toolkit/MenuBar.js | 2 +- app/toolkit/Panel.js | 110 +++- app/toolkit/RadioButton.js | 59 +++ app/toolkit/Splitter.js | 301 +++++++++++ app/toolkit/TextBox.js | 138 +++++ app/toolkit/Window.js | 245 ++++----- app/windows/CPUWindow.js | 292 +++++++++++ app/windows/MemoryWindow.js | 132 +++-- app/windows/Register.js | 496 ++++++++++++++++++ core/bus.c | 46 +- core/vb.c | 39 +- core/vb.h | 13 +- makefile | 6 +- wasm/wasm.c | 4 +- 32 files changed, 2437 insertions(+), 444 deletions(-) create mode 100644 app/theme/Inconsolata_SemiExpanded-Medium.woff2 delete mode 100644 app/theme/Inconsolata_SemiExpanded-Regular.woff2 create mode 100644 app/toolkit/ButtonGroup.js create mode 100644 app/toolkit/CheckBox.js create mode 100644 app/toolkit/RadioButton.js create mode 100644 app/toolkit/Splitter.js create mode 100644 app/toolkit/TextBox.js create mode 100644 app/windows/CPUWindow.js create mode 100644 app/windows/Register.js 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 0000000000000000000000000000000000000000..db38b3a1289bcd133838d8ceffb5c0c3d4bd81bf GIT binary patch literal 40712 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

ypWDvfQ%uDKZNogj(kl+y1U@d3I5Vp^@r}Pe7 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

Q-2&`WXJk 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

b;*%R_8rEQ3#iE2LX~N-%q1fw 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 *: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