import { Util } from /**/"./Util.js"; // Text to hex digit conversion const DIGITS = { "0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9, "A": 10, "B": 11, "C": 12, "D": 13, "E": 14, "F": 15 }; /////////////////////////////////////////////////////////////////////////////// // Line // /////////////////////////////////////////////////////////////////////////////// // One line of output class Line { ///////////////////////// Initialization Methods ////////////////////////// constructor(parent, index) { // Configure instance fields this.index = index; this.parent = parent; // Address label this.lblAddress = document.createElement("div"); this.lblAddress.className = "tk tk-address"; parent.view.appendChild(this.lblAddress); // Byte labels this.lblBytes = new Array(16); for (let x = 0; x < 16; x++) { let lbl = this.lblBytes[x] = document.createElement("div"); lbl.className = "tk tk-byte tk-" + x.toString(16); parent.view.appendChild(lbl); } } ///////////////////////////// Package Methods ///////////////////////////// // Update the elements' display refresh() { let address = Util.u32(this.parent.address + this.index * 16); let data = this.parent.data; let dataAddress = this.parent.dataAddress; let hexCaps = this.parent.dasm.hexCaps; let offset = Util.s32(address - dataAddress); // Format the line's address let text = address.toString(16).padStart(8, "0"); if (hexCaps) text = text.toUpperCase(); this.lblAddress.innerText = text; // The line's data is not available if (offset < 0 || offset >= data.length) for (let lbl of this.lblBytes) lbl.innerText = "--"; // The line's data is available else for (let x = 0; x < 16; x++, offset++) { let lbl = this.lblBytes[x]; text = data[offset].toString(16).padStart(2, "0"); // The byte is the current selection if (Util.u32(address + x) == this.parent.selection) { lbl.classList.add("selected"); if (this.parent.digit !== null) text = this.parent.digit.toString(16); } // The byte is not the current selection else lbl.classList.remove("selected"); // Update the label's text if (hexCaps) text = text.toUpperCase(); lbl.innerText = text; } } // Specify whether the elements on this line are visible setVisible(visible) { visible = visible ? "block" : "none"; this.lblAddress.style.display = visible; for (let lbl of this.lblBytes) lbl.style.display = visible; } } /////////////////////////////////////////////////////////////////////////////// // Memory // /////////////////////////////////////////////////////////////////////////////// // Memory hex editor class Memory extends Toolkit.ScrollPane { ///////////////////////// Initialization Methods ////////////////////////// constructor(debug) { super(debug.app, { className : "tk tk-scrollpane tk-memory", horizontal: Toolkit.ScrollPane.AS_NEEDED, focusable : true, tabStop : true, tagName : "div", vertical : Toolkit.ScrollPane.NEVER }); // Configure instance fields this.address = 0x05000000; this.app = debug.app; this.dasm = debug.disassembler; this.data = []; this.dataAddress = this.address; this.debug = debug; this.digit = null; this.isSubscribed = false; this.lines = []; this.selection = this.address; this.sim = debug.sim; // Configure view let view = document.createElement("div"); view.className = "tk tk-view"; Object.assign(view.style, { display : "grid", gridTemplateColumns: "repeat(17, max-content)" }); this.setView(view); // Font-measuring element this.metrics = new Toolkit.Component(this.app, { className: "tk tk-metrics tk-mono", tagName : "div", style : { position : "absolute", visibility: "hidden" } }); this.metrics.element.innerText = "X"; this.append(this.metrics.element); // Configure event handlers Toolkit.addResizeListener(this.viewport, e=>this.onResize(e)); this.addEventListener("keydown" , e=>this.onKeyDown (e)); this.addEventListener("pointerdown", e=>this.onPointerDown(e)); this.addEventListener("wheel" , e=>this.onMouseWheel (e)); } ///////////////////////////// Event Handlers ////////////////////////////// // Typed a digit onDigit(digit) { // Begin an edit if (this.digit === null) { this.digit = digit; this.setSelection(this.selection, true); } // Complete an edit else { this.digit = this.digit << 4 | digit; this.setSelection(this.selection + 1); } } // Key press onKeyDown(e) { let key = e.key; // A hex digit was entered if (key.toUpperCase() in DIGITS) { this.onDigit(DIGITS[key.toUpperCase()]); key = "digit"; } // Processing by key switch (key) { // Arrow key navigation case "ArrowDown" : this.setSelection(this.selection + 16); break; case "ArrowLeft" : this.setSelection(this.selection - 1); break; case "ArrowRight": this.setSelection(this.selection + 1); break; case "ArrowUp" : this.setSelection(this.selection - 16); break; // Commit current edit case "Enter": case " ": if (this.digit !== null) this.setSelection(this.selection); break; // Goto case "g": case "G": if (!e.ctrlKey) return; this.promptGoto(); break; // Page key navigation case "PageDown": this.setSelection(this.selection + this.tall(false) * 16); break; case "PageUp": this.setSelection(this.selection - this.tall(false) * 16); break; // Hex digit: already processed case "digit": break; default: return; } // Configure event e.stopPropagation(); e.preventDefault(); } // Mouse wheel onMouseWheel(e) { // User agent scaling action if (e.ctrlKey) return; // No rotation has occurred let offset = Math.sign(e.deltaY) * 48; if (offset == 0) return; // Update the display address this.address = Util.u32(this.address + offset); this.fetch(this.address, true); } // Pointer down onPointerDown(e) { // Common handling this.focus(); e.stopPropagation(); e.preventDefault(); // Not a click action if (e.button != 0) return; // Determine the row that was clicked on let lineHeight = !this.metrics ? 0 : Math.max(1, Math.ceil(this.metrics.getBounds().height)); let y = Math.floor((e.y - this.getBounds().top) / lineHeight); // Determine the column that was clicked on let columns = this.lines[0].lblBytes; let bndCur = columns[0].getBoundingClientRect(); if (e.x >= bndCur.left) for (let x = 0; x < 16; x++) { let bndNext = x == 15 ? null : columns[x + 1].getBoundingClientRect(); // The current column was clicked: update the selection if (e.x < (x == 15 ? bndCur.right : bndCur.right + (bndNext.left - bndCur.right) / 2)) { this.setSelection(this.address + y * 16 + x); return; } // Advance to the next column bndCur = bndNext; } } // Viewport resized onResize(e) { let fetch = false; let tall = this.tall(true); // Add additional lines to the output for (let x = 0; x < tall; x++) { if (x >= this.lines.length) { fetch = true; this.lines.push(new Line(this, x)); } this.lines[x].setVisible(true); } // Remove extra lines from the output for (let x = tall; x < this.lines.length; x++) this.lines[x].setVisible(false); // Configure horizontal scroll bar if (this.metrics) this.horizontal.setIncrement(this.metrics.getBounds().width); // Update the display if (fetch) this.fetch(this.address, true); else this.refresh(); } ///////////////////////////// Public Methods ////////////////////////////// // Update with memory state from the core refresh(data) { // Update with data from the core thread if (data) { this.data = data.bytes; this.dataAddress = data.address; } // Update elements for (let y = 0, tall = this.tall(true); y < tall; y++) this.lines[y].refresh(); } // Subscribe to or unsubscribe from core updates setSubscribed(subscribed) { subscribed = !!subscribed; // Nothing to change if (subscribed == this.isSubscribed) return; // Configure instance fields this.isSubscribed = subscribed; // Subscribe to core updates if (subscribed) this.fetch(this.address); // Unsubscribe from core updates else this.sim.unsubscribe("memory"); } ///////////////////////////// Package Methods ///////////////////////////// // The disassembler configuration has changed dasmChanged() { this.refresh(); } ///////////////////////////// Private Methods ///////////////////////////// // Retrieve memory data from the core async fetch(address, prefresh) { // Configure instance fields this.address = address; // Update the view immediately if (prefresh) this.refresh(); // Retrieve data from the core this.refresh( await this.sim.read( address - 16 * 16, (this.tall(true) + 32) * 16, { subscribe: this.isSubscribed && "memory" }) ); } // Prompt the user to specify a new address promptGoto() { // Receive input from the user let address = prompt(this.app.translate("common.gotoPrompt")); if (address == null) return; // Process the input as an address in hexadecimal address = parseInt(address, 16); if (isNaN(address)) return; // The address is not currently visible in the output let tall = this.tall(false); if (Util.u32(address - this.address) >= tall * 16) { this.fetch(Util.u32( (address & 0xFFFFFFF0) - Math.floor(tall / 3) * 16)); } // Move the selection and refresh the display this.setSelection(Util.u32(address)); } // Specify which byte is selected setSelection(address, noCommit) { let fetch = false; // Commit a pending data entry if (!noCommit && this.digit !== null) { this.write(this.digit); this.digit = null; fetch = true; } // Configure instance fields this.selection = address = Util.u32(address); // Working variables let row = Util.s32(address - this.address & 0xFFFFFFF0) / 16; // The new address is above the top line of output if (row < 0) { this.fetch(Util.u32(this.address + row * 16), true); return; } // The new address is below the bottom line of output let tall = this.tall(false); if (row >= tall) { this.fetch(Util.u32(address - tall * 16 + 16 & 0xFFFFFFF0), true); return; } // Update the display if (fetch) this.fetch(this.address, true); else this.refresh(); } // Measure how many rows of output are visible tall(partial) { let lineHeight = !this.metrics ? 0 : Math.ceil(this.metrics.getBounds().height); return lineHeight <= 0 ? 1 : Math.max(1, Math[partial?"ceil":"floor"]( this.viewport.getBoundingClientRect().height / lineHeight)); } // Write a value to the core thread write(value) { this.data[Util.s32(this.selection - this.dataAddress)] = value; this.sim.write( this.selection, Uint8Array.from([ value ]), { refresh: true }); } } export { Memory };