Adding Memory window

This commit is contained in:
2021-08-30 02:14:06 +00:00
parent f753f9f59b
commit b2adf1f9dc
20 changed files with 1260 additions and 96 deletions
+84 -26
View File
@@ -3,8 +3,49 @@
// Top-level state and UI manager
globalThis.App = class App {
// Object constructor
constructor() {
// Produce a new class instance
static async create() {
let ret = new App();
await ret.init();
return ret;
}
// Perform initial tasks
async init() {
// Initialization tasks
this.initEnv();
this.initMenus();
// WebAssembly core module
this.core = new Worker(Bundle.get("app/Emulator.js").toDataURL());
await new Promise((resolve,reject)=>{
this.core.onmessage = e=>{
this.core.onmessage = e=>this.onmessage(e.data);
resolve();
};
this.core.postMessage({
command: "Init",
wasm : Bundle.get("core.wasm").data.buffer
});
});
// Desktop pane
this.desktop = this.gui.newPanel({ layout: "desktop" });
this.desktop.setRole("group");
this.desktop.element.setAttribute("desktop", "");
this.gui.add(this.desktop);
// Debugging windows
this.debuggers = [
new Debugger(this, 0),
new Debugger(this, 1)
];
}
// Initialize environment settings
initEnv() {
// Configure themes
Bundle.get("app/theme/kiosk.css").style();
@@ -29,6 +70,10 @@ globalThis.App = class App {
// Configure locales
this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString());
this.gui.setLocale(navigator.language);
}
// Initialize main menu bar
initMenus() {
// Menu bar
this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" });
@@ -40,27 +85,41 @@ globalThis.App = class App {
let item = menu.newMenuItem({ text: "{menu.file.loadROM}"});
item.addClickListener(()=>this.loadROM());
// Debug menu
menu = this.mainMenu.newMenu({ text: "{menu.debug._}" });
item = menu.newMenuItem({ text: "{memory._}" });
item.addClickListener(
()=>this.debuggers[0].memory.setVisible(true, true));
// Theme menu
menu = this.mainMenu.newMenu({ text: "{menu.theme._}"});
item = menu.newMenuItem({ text: "{menu.theme.light}"});
menu = this.mainMenu.newMenu({ text: "{menu.theme._}" });
item = menu.newMenuItem({ text: "{menu.theme.light}" });
item.addClickListener(()=>this.setTheme("light"));
item = menu.newMenuItem({ text: "{menu.theme.dark}"});
item = menu.newMenuItem({ text: "{menu.theme.dark}" });
item.addClickListener(()=>this.setTheme("dark"));
item = menu.newMenuItem({ text: "{menu.theme.virtual}"});
item = menu.newMenuItem({ text: "{menu.theme.virtual}" });
item.addClickListener(()=>this.setTheme("virtual"));
}
// Desktop pane
let desktop = this.gui.newPanel({ layout: "desktop" });
desktop.setRole("group");
desktop.element.setAttribute("desktop", "");
this.gui.add(desktop);
let wnd = this.gui.newWindow({
title: "{memory._}"
});
desktop.add(wnd);
wnd.setLocation ( 20, 10);
wnd.setClientSize(384, 224);
///////////////////////////// Message Methods /////////////////////////////
// Message received
onmessage(msg) {
if ("debug" in msg) {
this.debuggers[msg.sim].message(msg);
return;
}
switch (msg.command) {
case "SetROM": this.setROM(msg); break;
}
}
// ROM buffer has been configured
setROM(msg) {
let dbg = this.debuggers[msg.sim];
dbg.memory.setVisible(true, true);
dbg.refresh();
}
@@ -71,12 +130,12 @@ globalThis.App = class App {
loadROM() {
let file = document.createElement("input");
file.type = "file";
file.addEventListener("input", ()=>this.setROM(file.files[0]));
file.addEventListener("input", ()=>this.selectROM(file.files[0]));
file.click();
}
// Specify a ROM file
async setROM(file) {
async selectROM(file) {
// No file is specified (perhaps the user canceled)
if (file == null)
@@ -94,18 +153,17 @@ globalThis.App = class App {
// Load the file data into a byte buffer
let filename = file.name;
try { file = new Uint8Array(await file.arrayBuffer()); }
try { file = await file.arrayBuffer(); }
catch {
alert(this.gui.translate("{app.readFileError}"));
return;
}
// Testing output pending further features
alert(this.gui.translate("{app.romLoaded}", {
filename: filename,
size : file.length + " byte" + (file.length == 1 ? "" : "s")
}));
this.core.postMessage({
command: "SetROM",
rom : file,
sim : 0
});
}
// Specify the current color theme
+43
View File
@@ -0,0 +1,43 @@
"use strict";
// Debugging UI manager
globalThis.Debugger = class Debugger {
// Object constructor
constructor(app, sim) {
// Configure instance fields
this.app = app;
this.core = app.core;
this.gui = app.gui;
this.sim = sim;
// Configure 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);
}
///////////////////////////// Package Methods /////////////////////////////
// Message received from emulation thread
message(msg) {
switch (msg.debug) {
case "Memory": this.memory.message(msg); break;
}
}
// Reload all output
refresh() {
this.memory.refresh();
}
};
+79
View File
@@ -0,0 +1,79 @@
"use strict";
// Worker that manages a WebAssembly instance of the C core library
(globalThis.Emulator = class Emulator {
// Static initializer
static initializer() {
new Emulator();
}
// Object constructor
constructor() {
// Configure message port
onmessage = e=>this.onmessage(e.data);
}
///////////////////////////// Message Methods /////////////////////////////
// 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;
}
}
// Initialize the WebAssembly core module
async init(msg) {
// Load and instantiate the WebAssembly module
this.wasm = await WebAssembly.instantiate(msg.wasm,
{ env: { emscripten_notify_memory_growth: function(){} }});
this.wasm.instance.exports.Init();
// Configure instance fields
this.core = this.wasm.instance.exports;
this.memory = this.core.memory.buffer;
postMessage({ command: "Init" });
}
// 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);
msg.buffer = buffer.buffer.slice(
buffer.pointer, buffer.pointer + msg.size);
this.core.Free(buffer.pointer);
postMessage(msg, msg.buffer);
}
// Supply a ROM buffer
setROM(msg) {
let rom = new Uint8Array(msg.rom);
let buffer = this.malloc(Uint8Array, rom.length);
for (let x = 0; x < rom.length; x++)
buffer[x] = rom[x];
msg.success = !!this.core.SetROM(msg.sim, buffer.pointer, rom.length);
delete msg.rom;
postMessage(msg);
}
///////////////////////////// Utility Methods /////////////////////////////
// Allocate a typed array in WebAssembly core memory
malloc(type, size) {
let pointer = this.core.Malloc(size);
let ret = new type(this.memory, pointer, size);
ret.pointer = pointer;
return ret;
}
}).initializer();
+4 -1
View File
@@ -285,15 +285,18 @@ for (let file of manifest) {
// Program startup
let run = async function() {
await Bundle.run("app/App.js");
await Bundle.run("app/Debugger.js");
await Bundle.run("app/toolkit/Toolkit.js");
await Bundle.run("app/toolkit/Component.js");
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/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/Window.js");
new App();
await Bundle.run("app/windows/MemoryWindow.js");
await App.create();
};
run();
+6 -2
View File
@@ -4,6 +4,7 @@
app : {
close : "Close",
console : "Console",
goto_ : "Enter the address to seek to:",
romLoaded : "Successfully loaded file \"{filename}\" ({size})",
romNotVB : "The selected file is not a Virtual Boy ROM.",
readFileError: "Unable to read the selected file."
@@ -12,8 +13,11 @@
_: "Memory"
},
menu: {
_ : "Main application menu",
file: {
_ : "Main application menu",
debug: {
_: "Debug",
},
file : {
_ : "File",
loadROM: "Load ROM..."
},
+4 -2
View File
@@ -2,11 +2,13 @@
--control : #222222;
--control-focus : #444444;
--control-shadow : #999999;
--control-text : #cccccc;
--desktop : #111111;
--text : #cccccc;
--window-blur : #555555;
--window-blur-text : #cccccc;
--window-close-text: #cccccc;
--window-close : #ee9999;
--window-close-blur: #998080;
--window-close-text: #ffffff;
--window-focus : #007ACC;
--window-focus-text: #ffffff;
}
+62 -28
View File
@@ -1,9 +1,15 @@
/* Common styles for all themes */
:root {
color : var(--text);
font-family: Arial, sans-serif;
font-size : 12px;
--font-dialog: Arial, sans-serif;
--font-hex : Consolas, monospace;
--font-size : 12;
}
:root {
color : var(--control-text);
font-family: var(--font-dialog);
font-size : calc(1px * var(--font-size));
}
body {
@@ -47,13 +53,14 @@ body {
[role="menubar"] {
background : var(--control);
border-bottom: 1px solid var(--text);
border-bottom: 1px solid var(--control-text);
padding : 2px 3px 3px 2px;
}
[role="menubar"] [role="menuitem"] {
margin : 1px;
padding: 3px;
line-height: 1em;
margin : 1px;
padding : 3px;
}
[role="menubar"] [role="menuitem"]:focus {
@@ -77,8 +84,8 @@ body {
[role="menubar"] [role="menu"] {
background: var(--control);
box-shadow:
0 0 0 1px var(--text),
1px 1px 0 1px var(--text)
0 0 0 1px var(--control-text),
1px 1px 0 1px var(--control-text)
;
min-height: 16px;
min-width : 16px;
@@ -97,13 +104,14 @@ body {
background: var(--control);
box-shadow:
0 0 0 1px var(--control),
0 0 0 2px var(--text),
1px 1px 0 2px var(--text)
0 0 0 2px var(--control-text),
1px 1px 0 2px var(--control-text)
;
row-gap : 3px;
}
[role="dialog"] [name="title-bar"] {
min-height: calc(1em + 5px);
}
[role="dialog"][focus="true"] [name="title-bar"] {
@@ -125,13 +133,14 @@ body {
[role="dialog"] [name="title-icon"],
[role="dialog"] [name="title-close-box"] {
align-self : stretch;
min-width : calc(1em + 4px);
width : calc(1em + 4px);
min-width : calc(1em + 5px);
width : calc(1em + 5px);
}
[role="dialog"] [name="title"] {
color : var(--window-focus-text);
font-weight : bold;
line-height : calc(1em + 1px);
overflow : hidden;
padding : 2px;
text-align : center;
@@ -140,28 +149,29 @@ body {
}
[role="dialog"][focus="false"] [name="title"] {
color : var(--window-blur-text);
color: var(--window-blur-text);
}
[role="dialog"] [name="title-close-box"] {
align-items : center;
display : flex;
justify-content: flex-start;
justify-content: center;
}
[role="dialog"] [name="title-close"] {
align-items : center;
background : var(--control);
box-shadow :
0 0 0 1px var(--control),
0 0 0 2px var(--control-shadow)
;
background : var(--window-close);
box-shadow : 0 0 0 1px var(--control-shadow);
display : flex;
height : 11px;
height : 13px;
justify-content: center;
overflow : hidden;
padding : 0;
width : 11px;
width : 13px;
}
[role="dialog"][focus="false"] [name="title-close"] {
background: var(--window-close-blur);
}
[role="dialog"] [name="title-close"]:focus {
@@ -169,20 +179,44 @@ body {
}
[role="dialog"] [name="title-close"][active] {
box-shadow :
-1px -1px 0 1px var(--control),
-1px -1px 0 2px var(--control-shadow)
;
margin: 2px 0 0 2px;
box-shadow: 0 0 0 1px var(--control-shadow);
margin : 0;
}
[role="dialog"] [name="title-close"]:after {
color : var(--window-close-text);
content : '\00d7';
font-size : 12px;
line-height: 1em;
font-size : 13px;
line-height: 13px;
}
[role="dialog"] [name="title-close"][active]:after {
color : var(--window-close-text);
content : '\00d7';
font-size : 13px;
line-height: 13px;
margin : 1px -1px -1px 1px;
}
[role="dialog"] [name="client"] {
background: var(--control);
}
/******************************* Memory Window *******************************/
[window="memory"] [name="client"] {
align-items: center;
column-gap : calc(1px * var(--font-size) / 2);
}
[window="memory"] [name="address"],
[window="memory"] [name="byte"] {
font-family: var(--font-hex);
line-height: 1em;
}
[window="memory"] [name="address"] {
align-self: start;
}
+4 -2
View File
@@ -2,11 +2,13 @@
--control : #eeeeee;
--control-focus : #cccccc;
--control-shadow : #999999;
--control-text : #000000;
--desktop : #cccccc;
--text : #000000;
--window-blur : #cccccc;
--window-blur-text : #444444;
--window-close-text: #444444;
--window-close : #ee9999;
--window-close-blur: #d4c4c4;
--window-close-text: #ffffff;
--window-focus : #80ccff;
--window-focus-text: #000000;
}
+3 -1
View File
@@ -2,10 +2,12 @@
--control : #000000;
--control-focus : #550000;
--control-shadow : #aa0000;
--control-text : #ff0000;
--desktop : #000000;
--text : #ff0000;
--window-blur : #000000;
--window-blur-text : #aa0000;
--window-close : #aa0000;
--window-close-blur: #550000;
--window-close-text: #ff0000;
--window-focus : #550000;
--window-focus-text: #ff0000;
+15 -6
View File
@@ -39,11 +39,6 @@ Toolkit.Component = class Component {
this.resizeListeners.push(listener);
}
// Remove the component from its parent
remove() {
this.parent && this.parent.remove(this);
}
// Retrieve the bounding box of the element
getBounds() {
return this.element.getBoundingClientRect();
@@ -62,6 +57,14 @@ Toolkit.Component = class Component {
return true;
}
// Specify the location and size of the component
setBounds(left, top, width, height) {
this.setLeft (left );
this.setTop (top );
this.setWidth (width );
this.setHeight(height);
}
// Specify the display CSS property of the visible element
setDisplay(display) {
this.display = display || null;
@@ -90,6 +93,12 @@ Toolkit.Component = class Component {
this.setTop (top );
}
// Specify a localization property value
setProperty(key, value) {
this.properties[key] = value;
this.localize();
}
// Specify both the width and the height of the component
setSize(width, height) {
this.setHeight(height);
@@ -146,7 +155,7 @@ Toolkit.Component = class Component {
onresized() {
let bounds = this.getBounds();
for (let listener of this.resizeListeners)
listener(bounds, this);
listener(bounds);
}
};
+52
View File
@@ -0,0 +1,52 @@
"use strict";
// Display text component
Toolkit.Label = class Label extends Toolkit.Component {
// Object constructor
constructor(application, options) {
super(application, "div", options);
options = options || {};
// Configure instance fields
this.localized = "localized" in options ? !!options.localized : false;
this.text = options.text || "";
// Configure element
this.element.style.cursor = "default";
this.element.style.userSelect = "none";
// Configure properties
this.setText(this.text);
if (this.localized)
this.application.addComponent(this);
}
///////////////////////////// Public Methods //////////////////////////////
// Retrieve the label's display text
getText() {
return this.text;
}
// Specify the label's display text
setText(text) {
this.text = text || "";
this.localize();
}
///////////////////////////// Package Methods /////////////////////////////
// Update display text with localized strings
localize() {
let text = this.text;
if (this.localized && this.application)
text = this.application.translate(text, this);
this.element.innerText = text;
}
};
+9
View File
@@ -17,6 +17,9 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
this.menu.element.style.position = "absolute";
this.menu.element.setAttribute("role", "menu");
this.menu.element.setAttribute("aria-labelledby", this.id);
this.menu.element.addEventListener("pointerdown",e=>this.stopEvent(e));
this.menu.element.addEventListener("pointermove",e=>this.stopEvent(e));
this.menu.element.addEventListener("pointerup" ,e=>this.stopEvent(e));
this.containers.push(this.menu);
this.children = this.menu.children;
@@ -213,4 +216,10 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem {
e.stopPropagation();
}
// Prevent an event from bubbling
stopEvent(e) {
e.preventDefault();
e.stopPropagation();
}
};
+24 -8
View File
@@ -6,9 +6,9 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
// Object constructor
constructor(application, options) {
super(application, "div", options);
options = options || {};
// Configure instance fields
options = options || {};
this.alignCross = "start";
this.alignMain = "start";
this.application = application;
@@ -22,8 +22,10 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
this.wrap = false;
// Configure element
this.element.style.minHeight = "0";
this.element.style.minWidth = "0";
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
@@ -57,6 +59,11 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
return new Toolkit.Button(this.application, options);
}
// Create a Label and associate it with the application
newLabel(options) {
return new Toolkit.Label(this.application, options);
}
// Create a MenuBar and associate it with the application
newMenuBar(options) {
return new Toolkit.MenuBar(this.application, options);
@@ -141,6 +148,16 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
///////////////////////////// Package Methods /////////////////////////////
// Move a window to the foreground
bringToFront(wnd) {
for (let child of this.children)
child.element.style.zIndex = child == wnd ? "0" : "1";
}
///////////////////////////// Private Methods /////////////////////////////
// Resize event handler
@@ -152,12 +169,13 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
// Ensure all child windows are visible in the viewport
for (let wnd of this.children) {
if (!wnd.isVisible())
continue;
let bounds = wnd.getBounds();
wnd.contain(
desktop,
bounds,
bounds.x - desktop.x,
bounds.y - desktop.y
bounds.y - desktop.y,
desktop, bounds
);
}
@@ -244,8 +262,6 @@ Toolkit.Panel = class Panel extends Toolkit.Component {
this.element.style.removeProperty("grid-template-rows" );
this.element.style.removeProperty("justify-content" );
this.element.style.removeProperty("flex-direction" );
this.element.style.removeProperty("overflow-x" );
this.element.style.removeProperty("overflow-y" );
}
};
+74 -13
View File
@@ -9,11 +9,16 @@ Toolkit.Window = class Window extends Toolkit.Panel {
options = options || {};
// Configure instance fields
this.dragBounds = null;
this.dragCursor = { x: 0, y: 0 };
this.dragEdge = null;
this.lastFocus = this.element;
this.title = options.title || "";
this.closeListeners = [];
this.dragBounds = null;
this.dragCursor = { x: 0, y: 0 };
this.dragEdge = 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", {
@@ -23,6 +28,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
overflowY : "visible"
});
this.setRole("dialog");
this.setBounds(0, 0, 64, 64);
this.element.style.position = "absolute";
this.element.setAttribute("aria-modal", "false");
this.element.setAttribute("focus" , "false");
@@ -53,6 +59,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
layout : "flex",
alignCross: "center",
direction : "row",
noShrink : true,
overflowX : "visible",
overflowY : "visible"
}));
@@ -64,7 +71,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.titleIcon.element.style.removeProperty("min-width");
// Configure title text element
this.titleElement = this.titleBar.add(this.newPanel({}));
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";
@@ -81,9 +88,13 @@ Toolkit.Window = class Window extends Toolkit.Panel {
toolTip : "{app.close}"
}));
this.titleClose.element.setAttribute("name", "title-close");
this.titleClose.addClickListener(e=>this.onclose(e));
// Configure client area
this.client = this.body.add(this.newPanel({}));
this.client = this.body.add(this.newPanel({
overflowX: "hidden",
overflowY: "hidden"
}));
this.client.element.style.flexGrow = "1";
this.client.element.setAttribute("name", "client");
this.client.element.addEventListener(
@@ -91,6 +102,8 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Configure properties
this.setTitle(this.title);
if (this.shown)
this.setClientSize(this.initialHeight, this.initialWidth);
application.addComponent(this);
}
@@ -98,6 +111,12 @@ Toolkit.Window = class Window extends Toolkit.Panel {
///////////////////////////// Public Methods //////////////////////////////
// Add a callback for close events
addCloseListener(listener) {
if (this.closeListeners.indexOf(listener) == -1)
this.closeListeners.push(listener);
}
// Specify the size of the client rectangle in pixels
setClientSize(width, height) {
let bounds = this.getBounds();
@@ -114,6 +133,19 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.localize();
}
// Specify whether the component is visible
setVisible(visible, focus) {
super.setVisible(visible);
if (this.client === undefined)
return;
if (visible)
this.contain();
if (focus)
this.focus();
if (!this.shown)
this.firstShow();
}
///////////////////////////// Package Methods /////////////////////////////
@@ -123,6 +155,7 @@ Toolkit.Window = class Window extends Toolkit.Panel {
if (this.lastFocus != this)
this.lastFocus.focus();
else this.element.focus();
this.parent.bringToFront(this);
}
@@ -130,9 +163,14 @@ Toolkit.Window = class Window extends Toolkit.Panel {
///////////////////////////// Private Methods /////////////////////////////
// Position the window using a tentative location in the desktop
contain(desktop, bounds, x, y, client) {
bounds = bounds || this.getBounds();
client = client || this.client.getBounds();
contain(x, y, desktop, bounds, client) {
desktop = desktop || this.parent.getBounds();
bounds = bounds || this.getBounds();
client = client || this.client.getBounds();
if (x === undefined)
x = bounds.x - desktop.x;
if (y === undefined)
y = bounds.y - desktop.y;
// Restrict window position
x = Math.min(x, desktop.width - 16);
@@ -172,6 +210,25 @@ Toolkit.Window = class Window extends Toolkit.Panel {
return null;
}
// 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;
@@ -193,6 +250,12 @@ Toolkit.Window = class Window extends Toolkit.Panel {
this.focus();
}
// Window close
onclose(e) {
for (let listener of this.closeListeners)
listener(e);
}
// Focus gained event capture
onfocus(e) {
@@ -284,11 +347,9 @@ Toolkit.Window = class Window extends Toolkit.Panel {
// Move the window
if (this.dragEdge == null) {
this.contain(
desktop,
bounds,
this.dragBounds.x - desktop.x + rX,
this.dragBounds.y - desktop.y + rY,
client
desktop, bounds, client
);
return;
}
+232
View File
@@ -0,0 +1,232 @@
"use strict";
// Hex editor style memory viewer
globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window {
// Object constructor
constructor(debug, options) {
super(debug.gui, options);
// Configure instance fields
this.address = 0xFFFFFFF0;
this.debug = debug;
this.rows = [new MemoryWindow.Row(this.client)];
// Configure element
this.element.setAttribute("window", "memory");
// Configure client
this.client.setLayout("grid", { columns: "repeat(19, max-content)" });
this.client.element.style.gridAutoRows = "max-content";
this.client.setOverflow("auto", "hidden");
this.client.addResizeListener(b=>this.refresh(b.height));
this.client.element.addEventListener("keydown", e=>this.onkeydown(e));
this.client.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));
}
///////////////////////////// Package Methods /////////////////////////////
// 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));
// Request bus data from the WebAssembly core
this.debug.core.postMessage({
command: "ReadBuffer",
debug : "Memory",
address: this.address,
sim : this.debug.sim,
size : rowCount * 16
});
// Configure elements
for (let y = this.rows.length; y < rowCount; y++)
this.rows[y] = new MemoryWindow.Row(this.client);
for (let y = rowCount; y < this.rows.length; y++)
this.rows[y].remove();
if (this.rows.length > rowCount)
this.rows.splice(rowCount, this.rows.length - rowCount);
}
///////////////////////////// Message Methods /////////////////////////////
// Message received
message(msg) {
switch (msg.command) {
case "ReadBuffer": this.readBuffer(msg); break;
}
}
// Received bytes from the bus
readBuffer(msg) {
let bytes = new Uint8Array(msg.buffer);
for (
let x = 0, address = this.address, offset = 0;
x < this.rows.length && offset < bytes.length;
x++, address = (address + 16 & 0xFFFFFFFF) >>> 0, offset += 16
) this.rows[x].update(address, bytes, offset);
}
///////////////////////////// Private Methods /////////////////////////////
// Determine the height in pixels of one row of output
lineHeight() {
let ret = this.rows[0].address.getBounds().height;
return ret;
}
// 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);
return Math.max(1, Math.min(ret, Math.floor(clientHeight / 8)));
}
// Key down event handler
onkeydown(e) {
// Control is pressed
if (e.ctrlKey) switch (e.key) {
case "g": case "G":
let addr = prompt(this.application.translate("{app.romLoaded}"));
if (addr === null)
break;
this.seek(
(parseInt(addr, 16) & 0xFFFFFFF0) >>> 0,
Math.floor(this.lines(true) / 3)
);
this.refresh();
break;
default: return;
}
// 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: console.log(e.key); return;
}
// Configure event
e.preventDefault();
e.stopPropagation();
}
// Mouse wheel event handler
onwheel(e) {
let lineHeight = this.lineHeight();
let sign = Math.sign(e.deltaY);
let mag = Math.abs (e.deltaY);
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL)
mag = Math.max(1, Math.floor(mag / lineHeight));
this.address = (this.address + sign * mag * 16 & 0xFFFFFFFF) >>> 0;
this.refresh(null, lineHeight);
}
// Move to a new address positioned at a particular row of output
seek(address, index) {
this.address = (address - (index || 0) * 16 & 0xFFFFFFFF) >>> 0;
}
};
// One row of output
MemoryWindow.Row = class Row {
// Object constructor
constructor(client) {
// Configure instance fields
this.bytes = new Array(16);
this.client = client;
this.spacers = new Array(2);
// Address label
this.address = client.add(client.newLabel({ text: "\u00a0" }));
this.address.element.setAttribute("name", "address");
// Byte labels
for (let x = 0; x < 16; x++) {
if ((x & 7) == 0) {
let lbl = this.spacers[x / 8] =
client.add(client.newLabel({ text: "" }));
lbl.element.setAttribute("name", "byte");
}
let lbl = this.bytes[x] =
client.add(client.newLabel({ text: "\u00a0" }));
lbl.element.setAttribute("name", "byte");
}
}
///////////////////////////// Package Methods /////////////////////////////
// Remove components from the memory window
remove() {
this.client.remove(this.address);
for (let bytel of this.bytes)
this.client.remove(bytel);
for (let spacer of this.spacers)
this.client.remove(spacer);
}
// Update the output labels with emulation state content
update(address, bytes, offset) {
this.address.setText(
("0000000" + address.toString(16).toUpperCase()).slice(-8));
for (let x = 0; x < 16; x++, offset++)
this.bytes[x].setText(
("0" + bytes[offset].toString(16).toUpperCase()).slice(-2));
}
};