Add editable memory cells

This commit is contained in:
Alexander Bass 2024-03-06 01:46:29 -05:00
parent 7e4d3c8da8
commit 8a38dded92
18 changed files with 499 additions and 309 deletions

29
ISA.txt
View file

@ -1,29 +0,0 @@
INSTRUCTIONS
--- Control Flow ---
0x00: NoOp - 0 Parameter
0x10: Goto - 1 Parameter - moves the program counter to (P1) in program memory
0x11: Goto if low bit - 2 Parameter - Moves the program counter to (P1) if register (P2)'s least significant bit is 1.
--- Memory and register management ---
0x20: Load to register - 2 Parameter - Loads into register (P1) the byte at processing memory location (P2)
0x21: Write register to memory - 2 Parameter - Writes the byte in register (P1) to the processing memory location (P2)
0x28: Copy Register -> Register - 2 Parameter - Copies byte from register (P1) to register (P2)
0x2F: Assign value to register - 2 Parameter - Assigns register (P1) to value (P2)
--- Operations ---
0x30: increment register - 1 Parameter - Increments register (P1) by 1.
0x31: decrement register - 1 Parameter - Decrements register (P1) by 1.
0x40: Add registers - 2 Parameter - Adds the contents of (P1) and (P2) and stores result to register (P1). (Overflow will be taken mod 256)
0x41: Reserved
--- Bit Operations ---
0x48: Bitwise and - 2 Parameter - Ands & each bit of register (P1) and register (P2) and stores result to register (P1)
0x49: Bitwise or - you get the point
0x4A: Bitwise not - 1 Parameter - Inverts each bit of register (P1)
0x4B: Left bit shift - 2 Parameter - Shifts bits in register (P1) to the left by (P2) and stores result to register (P1)
0x4C: right bit shift- 2 Parameter - same as left
--- Comparison Operations ---
0x50: Equals - 3 Parameter - If byte in register (P1) equals byte in register (P2), set byte in register (P3) to 0x01
0x51: Less than - 3 Parameter - If byte in register (P1) less than byte in register (P2), set byte in register (P3) to 0x01
0x52: Greater than - 3 Parameter - If byte in register (P1) greater than byte in register (P2), set byte in register (P3) to 0x01
--- Development ---
0xFE: Print byte as ASCII from register - 1 Parameter - Prints the ASCII byte in register (P1) to console
0xFF: Print byte from register - 1 Parameter - Prints the byte in register (P1) to console
0x66: Halt and Catch Fire - 0 Parameter - Fire! FIRE EVERYWHERE!!!!!

14
TODO
View file

@ -1,14 +0,0 @@
Live memory and register editing (Probably should pause autostep when it reaches the cell you're modifying)
HCF flames
add hcf
Move start/stop/auto logic into computer
Speed control slider behavior
Speed control slider styling
UI for screen (toggling (click an icon?))
UI for togging other UI elements
UI for showing which Memory bank is selected
VRAM select instruction
Error log

View file

@ -1,33 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<script src="./dist/main.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Virtual 8-Bit Computer</title>
</head>
<body>
<div id="main">
<div id="title">VIRTUAL 8-BIT COMPUTER</div>
<div id="registers"></div>
<div id="labelcontainer">
<div id="registers_label">←REGISTERS</div>
<div id="memory_label">MEMORY↯</div>
</div>
<div id="memory"></div>
<div id="instruction_explainer"></div>
<div id="printout"></div>
<div id="controls_bar">
<button type="button" id="pause_play_button">Start</button>
<button type="button" id="step_button">Step</button>
<label for="binary_upload" class="button">Load Binary</label>
<input id="binary_upload" name="binary_upload" id="binary_upload" style="display: none" type="file" />
<button type="button" id="save_button">Save</button>
<input type="range" name="" id="delay_range" min="0" max="1500" />
</div>
<span id="cycles"></span>
</div>
<canvas id="screen"></canvas>
<pre id="ISA"></pre>
</body>
</html>

View file

@ -1,7 +1,7 @@
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "./events";
import { byte_array_to_js_source, format_hex } from "./etc";
import { Instruction, ISA } from "./instructionSet";
import { m256, u1, u2, u3, u8 } from "./num";
import { m256, u2, u3, u8 } from "./num";
export type TempInstrState = {
pos: u8;
@ -178,6 +178,7 @@ export class Computer {
for (let i = 0; i < cycle_count; i++) this.cycle();
});
ui.listen(UiEvent.RequestMemoryChange, ({ address, value }) => this.setMemory(address, value));
ui.listen(UiEvent.RequestRegisterChange, ({ register_no, value }) => this.setRegister(register_no, value));
}
load_memory(program: Array<u8>): void {

View file

@ -31,13 +31,20 @@ export const byte_array_to_js_source = (bytes: Array<u8>): string => {
* @param type
* @param id id attribute to set
*/
export function el<E extends keyof HTMLElementTagNameMap>(type: E, id?: string): HTMLElementTagNameMap[E];
export function el(type: string, id?: string): HTMLElement | undefined {
export function el<E extends keyof HTMLElementTagNameMap>(
type: E,
id?: string,
class_list?: string
): HTMLElementTagNameMap[E];
export function el(type: string, id?: string, class_list?: string): HTMLElement | undefined {
const element = document.createElement(type);
if (id === undefined) {
return element;
if (id !== undefined) {
element.id = id;
}
element.id = id;
if (class_list !== undefined) {
element.className = class_list;
}
return element;
}
@ -49,3 +56,13 @@ export function in_range(check: number, start: number, end: number): boolean {
if (check >= start && check <= end) return true;
return false;
}
export function at<T>(l: Array<T>, i: number): T | null {
if (i < 0) {
return null;
}
if (i >= l.length) {
return null;
}
return l[i];
}

View file

@ -5,7 +5,7 @@
*/
import { EventHandler } from "./eventHandler";
import { Instruction, ParameterType } from "./instructionSet";
import { u1, u2, u3, u8 } from "./num";
import { u2, u3, u8 } from "./num";
//
// CPU Event Handler Definition
@ -65,18 +65,28 @@ export const CpuEventHandler = EventHandler<CpuEvent> as CpuEventHandlerConstruc
//
export enum UiEvent {
// Maybe move these into a UI -> CPU signal system?
RequestCpuCycle,
RequestMemoryChange,
RequestRegisterChange,
// Ui Events
EditOn,
EditOff,
}
interface UiEventMap {
[UiEvent.RequestCpuCycle]: number;
[UiEvent.RequestMemoryChange]: { address: u8; value: u8 };
[UiEvent.RequestRegisterChange]: { register_no: u3; value: u8 };
}
type VoidDataUiEventList = UiEvent.EditOn | UiEvent.EditOff;
export interface UiEventHandler extends EventHandler<UiEvent> {
listen<E extends keyof UiEventMap>(type: E, listener: (ev: UiEventMap[E]) => void): void;
dispatch<E extends keyof UiEventMap>(type: E, data: UiEventMap[E]): void;
listen<E extends VoidDataUiEventList>(type: E, listener: () => void): void;
dispatch<E extends VoidDataUiEventList>(type: E): void;
}
interface UiEventHandlerConstructor {

View file

@ -22,26 +22,9 @@ declare global {
}
function main(): void {
// const program: Array<u8> = [
// 0x2f, 0x00, 0xf0, 0x20, 0x07, 0x00, 0x50, 0x05, 0x06, 0x07, 0x11, 0x00, 0x05, 0xfe, 0x07, 0x30, 0x00, 0x10, 0x03,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57,
// 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0a, 0x00, 0x00, 0x00,
// ];
const program: Array<u8> = [
0x2f, 0x00, 0x00, 0x2f, 0x01, 0xff, 0x21, 0x01, 0x0d, 0xb1, 0x01, 0x21, 0x01, 0x00, 0x31, 0x01, 0xb1, 0x00, 0x10,
0x06, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x19, 0x00, 0xf0, 0x14, 0x00, 0x01, 0x5e, 0x00, 0xf0, 0x01, 0x21, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
@ -55,24 +38,6 @@ function main(): void {
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57,
0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0a, 0x00, 0x00, 0x00,
];
// const program = [
// 0x01, 0x00, 0x01, 0x00, 0xa0, 0x10, 0xff, 0x00, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x30, 0x00, 0xa1,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
// 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x20, 0x57,
// 0x6f, 0x72, 0x6c, 0x64, 0x21, 0x0a, 0x00, 0x00, 0x00,
// ];
const computer = new Computer();
const ui = new UI();
@ -137,10 +102,5 @@ function main(): void {
}
document.addEventListener("DOMContentLoaded", () => {
// at least you know it's bad
try {
main();
} catch (e) {
alert(e);
}
main();
});

View file

@ -170,6 +170,34 @@ ISA.insertInstruction(0x13, {
c.setRegister(register_no_2, c.getRegister(register_no_1));
},
});
ISA.insertInstruction(0x14, {
name: "Load RM -> R",
desc: "Copy the byte in memory addressed by register (P1) to register (P2)",
params: [
new RegisParam("Copy the byte in the memory cell addressed in this register"),
new RegisParam("To this register"),
],
execute(c, p) {
const [register_no_1, register_no_2] = p;
if (!isU3(register_no_1)) throw new Error("todo");
if (!isU3(register_no_2)) throw new Error("todo");
c.setRegister(register_no_2, c.getMemory(c.getRegister(register_no_1)));
},
});
ISA.insertInstruction(0x15, {
name: "Save R -> RM",
desc: "Copy the byte in register (P1) to the memory cell addressed in register (P2)",
params: [
new RegisParam("Copy the value in this register"),
new RegisParam("To the memory cell addressed in this register"),
],
execute(c, p) {
const [register_no_1, register_no_2] = p;
if (!isU3(register_no_1)) throw new Error("todo");
if (!isU3(register_no_2)) throw new Error("todo");
c.setMemory(c.getRegister(register_no_2), c.getRegister(register_no_1));
},
});
ISA.insertInstruction(0x17, {
name: "Zero Register",
@ -608,6 +636,7 @@ ISA.insertInstruction(0x50, {
c.setRegister(register_no_1, m256(sum));
},
});
ISA.insertInstruction(0x51, {
name: "Add",
desc: "Adds to the byte in register (P1) with the value in register (P2)",
@ -620,8 +649,9 @@ ISA.insertInstruction(0x51, {
c.setRegister(register_no_1, m256(sum));
},
});
ISA.insertInstruction(0x52, {
name: "Add",
name: "Subtract",
desc: "Subtracts from the value in register (P1) by the value in register (P2)",
params: [new RegisParam("set this register to"), new RegisParam("it's difference with the value in this register")],
execute(c, p) {
@ -635,8 +665,9 @@ ISA.insertInstruction(0x52, {
c.setRegister(register_no_1, m256(difference));
},
});
ISA.insertInstruction(0x53, {
name: "Add",
name: "Subtract",
desc: "Subtracts from the value in register (P1) by the constant value (P2)",
params: [new RegisParam("set this register to"), new ConstParam("it's difference with this constant")],
execute(c, p) {

View file

@ -8,16 +8,6 @@ pre {
:root {
--Border: #ffff00;
// --mem-instruction: #adff2f;
// --mem-register: #800080;
// --mem-constant: #d3d3d3;
// --mem-memory: #d3d3d3;
// --mem-invalid: #ff0000;
// --mem-instruction: #2f962a;
// --mem-register: #dc21d1;
// --mem-constant: #d3d3d3;
// --mem-memory: #4d86f0;
// --mem-invalid: #bf2e2e;
--mem-instruction: #3af78f;
--mem-memory: #ff26a8;
--mem-register: #9e0ef7;
@ -25,6 +15,10 @@ pre {
--mem-invalid: #bf2e2e;
}
img {
image-rendering: pixelated;
}
body {
color: #808080;
background-color: black;
@ -44,13 +38,13 @@ body {
#main {
justify-content: center;
display: grid;
grid-template-columns: min-content max-content max-content 10px 500px;
grid-template-columns: min-content max-content max-content min-content 500px;
grid-template-rows: min-content 1.5fr 10px 2fr 2fr min-content;
grid-template-areas:
"cycles registers regmemlabel . explainer "
"title . . . explainer "
"title . . . ."
"title . . . printout "
"title . . ribbon explainer "
"title . . ribbon ."
"title . . ribbon printout "
"title . . . printout "
". buttons buttons . .";
#memory {
@ -70,6 +64,9 @@ body {
user-select: none;
transform: scale(-1, -1);
}
#ribbon_menu {
grid-area: ribbon;
}
}
#printout {
@ -124,6 +121,17 @@ body {
--color: var(--mem-instruction);
}
div[contenteditable] {
&.caret_selected {
box-sizing: border-box;
outline: 2px solid red;
}
outline: none;
}
.pending_edit {
color: green;
}
#memory {
grid-area: memory;
display: grid;
@ -131,21 +139,29 @@ body {
gap: 5px;
padding: 10px;
border: 5px solid yellow;
div {
user-select: none;
caret-color: transparent;
text-align: center;
color: var(--color);
}
.program_counter {
outline: 3px solid orange;
}
.instruction_argument,
.current_instruction {
outline: 3px dashed var(--color);
}
.recent_edit {
color: lime;
}
div.last_access {
color: orange;
}
.invalid {
&::after {
user-select: none;
@ -159,10 +175,31 @@ body {
}
}
div#main.editor {
#memory,
#registers {
border-style: dashed;
div {
cursor: text;
}
}
}
#ribbon_menu {
margin-inline: 8px;
.editor_toggle {
//TODO CHANGE COLORS WHen
&.off {
}
&.on {
}
}
}
#registers {
grid-area: registers;
border: 5px solid yellow;
border-bottom: none;
border-bottom: none !important;
grid-template-columns: repeat(8, min-content);
max-width: fit-content;
@ -191,6 +228,25 @@ label.button {
user-select: none;
}
button:disabled {
border: 4px solid rgb(255, 255, 128);
color: orange;
cursor: default;
}
input[type="range"]:disabled {
cursor: default;
}
button.no_style {
border: none;
color: inherit;
margin: 0;
padding: 0;
font-size: inherit;
background-color: inherit;
}
button:hover,
label.button:hover {
color: white;
@ -201,6 +257,10 @@ label.button:hover {
display: flex;
gap: 10px;
#controls_buttons {
display: flex;
gap: inherit;
}
}
input[type="range"] {
@ -208,8 +268,8 @@ input[type="range"] {
-webkit-appearance: none;
appearance: none;
margin: 18px 0;
// width: 100%;
}
input[type="range"]:focus {
outline: none;
}

View file

@ -1,16 +1,13 @@
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "./events";
import { $, el, format_hex } from "./etc";
import { $ } from "./etc";
import { InstructionExplainer } from "./ui/instructionExplainer";
import { MemoryView } from "./ui/memoryView";
import { frequencyIndicator } from "./ui/frequencyIndicator";
import { RegisterView } from "./ui/registerView";
import { Screen } from "./ui/screen";
import { UiComponent, UiComponentConstructor } from "./ui/uiComponent.js";
// Certainly the messiest portion of this program
// Needs to be broken into components
// Breaking up into components has started but has yet to conclude
let delay = 100;
import { Ribbon } from "./ui/ribbon";
import { UiComponent, UiComponentConstructor } from "./ui/uiComponent";
import { pausePlay } from "./ui/pausePlay";
export class UI {
printout: HTMLElement;
@ -34,32 +31,12 @@ export class UI {
this.register_component(InstructionExplainer, $("instruction_explainer"));
this.register_component(RegisterView, $("registers"));
this.register_component(Screen, $("screen") as HTMLCanvasElement);
this.register_component(Ribbon, $("ribbon_menu"));
this.register_component(pausePlay, $("controls_buttons"));
this.printout = $("printout");
this.auto_running = false;
const pp_button = $("pause_play_button");
pp_button.addEventListener("click", () => {
if (this.auto_running) {
this.stop_auto();
pp_button.textContent = "Starp";
} else {
this.start_auto();
pp_button.textContent = "Storp";
}
});
$("step_button").addEventListener("click", () => {
if (this.auto_running) {
this.stop_auto();
}
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
});
$("delay_range").addEventListener("input", (e) => {
delay = parseInt((e.target as HTMLInputElement).value, 10);
// console.log(delay);
});
}
private register_component(c: UiComponentConstructor, e: HTMLElement): void {
if (e === undefined) {
@ -79,34 +56,14 @@ export class UI {
});
for (const c of this.components) {
c.init_cpu_events(cpu_events);
if (c.init_cpu_events) c.init_cpu_events(cpu_events);
}
}
reset(): void {
this.stop_auto();
for (const c of this.components) {
c.reset();
}
this.printout.textContent = "";
}
start_auto(speed: number = 200): void {
if (this.auto_running) {
return;
}
this.auto_running = true;
const loop = (): void => {
if (this.auto_running === false) {
return;
}
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
setTimeout(loop, delay);
};
loop();
}
stop_auto(): void {
this.auto_running = false;
}
}

View file

@ -6,7 +6,6 @@
import { NonEmptyArray, el, format_hex } from "../etc";
import { u8 } from "../num";
// TODO, make generic
interface GenericCell {
el: HTMLElement;
}
@ -22,7 +21,7 @@ export abstract class CelledViewer {
this.height = height;
for (let i = 0; i < this.width * this.height; i++) {
const mem_cell_el = el("div");
mem_cell_el.textContent = "00";
mem_cell_el.append("0", "0");
this.element.appendChild(mem_cell_el);
const mem_cell = { el: mem_cell_el };
this.cells.push(mem_cell);
@ -31,7 +30,7 @@ export abstract class CelledViewer {
reset(): void {
for (let i = 0; i < this.height * this.width; i++) {
this.cells[i].el.textContent = "00";
this.set_cell_value(i as u8, 0);
this.cells[i].el.className = "";
}
}
@ -59,6 +58,10 @@ export abstract class CelledViewer {
}
set_cell_value(address: u8, value: u8): void {
this.cells[address].el.textContent = format_hex(value);
const str = format_hex(value);
const a = str[0];
const b = str[1];
this.cells[address].el.textContent = "";
this.cells[address].el.append(a, b);
}
}

View file

@ -1,122 +1,112 @@
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function set_caret(el: any, pos: number): boolean {
const selection = window.getSelection();
const range = document.createRange();
if (selection === null) {
return false;
}
// This file was cobbled together and is the messiest part of this project
selection.removeAllRanges();
range.selectNode(el);
import { at } from "../etc";
import { u8 } from "../num";
range.setStart(el, pos);
range.setEnd(el, pos);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
el.focus();
return true;
}
const HEX_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
function get_caret(el: HTMLElement): null | number {
const sel = window.getSelection();
if (sel === null) {
return null;
}
const pos = sel.getRangeAt(0).startOffset;
const endPos = pos + Array.from(el.innerHTML.slice(0, pos)).length - el.innerHTML.slice(0, pos).split("").length;
return endPos;
}
export class EditorContext {
private list: Array<HTMLElement>;
private width: number;
private height: number;
private enabled: boolean = false;
private current_cell_info: { left?: string; right?: string; old?: string };
private edit_callback: (n: number, value: u8) => void;
constructor(list: Array<HTMLElement>, width: number, height: number, callback: (n: number, value: u8) => void) {
this.list = list;
this.width = width;
this.height = height;
this.edit_callback = callback;
this.current_cell_info = {};
const hex_characters = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];
function replace_non_hex(c: string): string {
if (hex_characters.includes(c)) {
return c;
}
return "0";
}
for (const [i, cell] of this.list.entries()) {
cell.setAttribute("spellcheck", "false");
cell.addEventListener("keydown", (e) => {
this.keydown(e, i);
});
cell.addEventListener("focus", () => {
if (!this.enabled) return;
this.current_cell_info.old = cell.textContent ?? "00";
this.current_cell_info.left = undefined;
this.current_cell_info.right = undefined;
cell.classList.add("caret_selected");
function editable_constraints(e: Event): boolean {
const target = e.target as HTMLDivElement;
const text = target.innerHTML ?? "";
if (text.length !== 2) {
const pos = get_caret(target);
const new_str = [...(target.textContent ?? "").substring(0, 2).padStart(2, "0").toUpperCase()]
.map(replace_non_hex)
.join("");
target.innerHTML = "";
// For the caret selection to work right, each character must be its own node, complicating this greatly
target.append(new_str.substring(0, 1), new_str.substring(1));
// Reset cursor position (I know there's an API for this, but this is a simpler, more robust solution)
cell.textContent = cell.textContent ?? "00";
});
if (pos !== null) {
if (pos >= 2) {
return true;
}
set_caret(target, pos);
cell.addEventListener("blur", () => {
const left = this.current_cell_info.left;
const right = this.current_cell_info.right;
cell.classList.remove("caret_selected");
if (left === undefined || right === undefined) {
cell.textContent = this.current_cell_info.old ?? "00";
} else if (left !== undefined && right !== undefined) {
const text = `${left}${right}`;
cell.textContent = text;
const val = Number.parseInt(text, 16) as u8;
this.edit_callback(i, val);
cell.classList.add("recent_edit");
}
});
}
}
return false;
}
function at<T>(l: Array<T>, i: number): T | null {
if (i < 0) {
return null;
enable(): void {
this.enabled = true;
for (const cell of this.list) {
cell.setAttribute("contenteditable", "true");
}
}
if (i >= l.length) {
return null;
disable(): void {
this.enabled = false;
for (const cell of this.list) {
cell.removeAttribute("contenteditable");
cell.blur();
}
this.current_cell_info = {};
}
return l[i];
}
export function make_editable(
list: Array<HTMLElement>,
width: number,
height: number,
on_edit: (n: number, value: string) => void
): void {
for (const [i, cell] of list.entries()) {
cell.setAttribute("contenteditable", "true");
cell.setAttribute("spellcheck", "false");
const next: null | HTMLElement = at(list, i + 1);
const prev: null | HTMLElement = at(list, i - 1);
const up: null | HTMLElement = at(list, i - width);
const down: null | HTMLElement = at(list, i + width);
cell.addEventListener("keydown", (e) => {
const caret_position = get_caret(cell);
const k = e.key;
if (k === "ArrowUp") {
(up ?? prev)?.focus();
cell.blur();
} else if (k === "ArrowDown") {
(down ?? next)?.focus();
cell.blur();
} else if ((k === "ArrowLeft" || k === "Backspace") && caret_position === 0) {
prev?.focus();
cell.blur();
} else if (k === "ArrowRight" && caret_position === 1) {
next?.focus();
cell.blur();
} else if (k === "Enter") {
cell.blur();
} else if (k === "Escape") {
cell.blur();
return;
} else {
return;
}
e.preventDefault();
});
let previous_text = cell.textContent ?? "";
cell.addEventListener("input", (e) => {
const current_text = cell.textContent ?? "";
if (current_text !== previous_text) {
previous_text = cell.textContent ?? "";
on_edit(i, current_text);
}
if (editable_constraints(e) === true) {
private keydown(e: KeyboardEvent, cell_index: number): void {
if (!this.enabled) return;
const cell = e.target as HTMLElement;
const next: null | HTMLElement = at(this.list, cell_index + 1);
const prev: null | HTMLElement = at(this.list, cell_index - 1);
const up: null | HTMLElement = at(this.list, cell_index - this.width);
const down: null | HTMLElement = at(this.list, cell_index + this.width);
const k = e.key;
if (k === "ArrowUp") {
(up ?? prev)?.focus();
cell.blur();
} else if (k === "ArrowDown") {
(down ?? next)?.focus();
cell.blur();
} else if (k === "ArrowLeft" || k === "Backspace") {
prev?.focus();
cell.blur();
} else if (k === "ArrowRight") {
next?.focus();
cell.blur();
} else if (k === "Enter") {
cell.blur();
} else if (k === "Escape") {
cell.blur();
return;
} else if (HEX_CHARACTERS.includes(k.toUpperCase())) {
if (this.current_cell_info.left === undefined) {
this.current_cell_info.left = k.toUpperCase();
cell.innerHTML = `<span class="pending_edit">${this.current_cell_info.left}</span>0`;
} else if (this.current_cell_info.right === undefined) {
this.current_cell_info.right = k.toUpperCase();
cell.textContent = `${this.current_cell_info.left}${this.current_cell_info.right}`;
next?.focus();
cell.blur();
}
});
} else if (k === "Tab") {
return;
}
e.preventDefault();
}
}

View file

@ -1,8 +1,9 @@
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../events";
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "../events";
import { ParamType } from "../instructionSet";
import { u8 } from "../num.js";
import { UiComponent } from "./uiComponent";
import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex";
type MemoryCell = {
el: HTMLDivElement;
@ -16,6 +17,23 @@ export class MemoryView extends CelledViewer implements UiComponent {
super(16, 16, element);
this.program_counter = 0;
this.events = e;
const list = this.cells.map((c) => c.el);
const editor = new EditorContext(list, this.width, this.height, (i, value) => {
this.events.dispatch(UiEvent.RequestMemoryChange, { address: i as u8, value });
});
this.events.listen(UiEvent.EditOn, () => {
editor.enable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
this.events.listen(UiEvent.EditOff, () => {
editor.disable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
}
set_program_counter(position: u8): void {

104
src/ui/pausePlay.ts Normal file
View file

@ -0,0 +1,104 @@
import { el } from "../etc";
import { UiEventHandler, CpuEventHandler, UiEvent } from "../events";
import { UiComponent } from "./uiComponent";
const MAX_SLIDER = 1000;
export class pausePlay implements UiComponent {
element: HTMLElement;
start_button: HTMLButtonElement;
step_button: HTMLButtonElement;
range: HTMLInputElement;
events: UiEventHandler;
on: boolean = false;
cycle_delay: number;
constructor(element: HTMLElement, events: UiEventHandler) {
this.element = element;
this.events = events;
this.start_button = el("button", "pause_play_button");
this.step_button = el("button", "step_button");
this.range = el("input", "speed_range");
this.range.max = MAX_SLIDER.toString();
this.range.min = "0";
this.range.type = "range";
this.start_button.addEventListener("click", () => this.toggle());
this.step_button.addEventListener("click", () => this.step());
this.range.addEventListener("input", (e) => {
const delay = MAX_SLIDER - parseInt((e.target as HTMLInputElement).value, 10) + 10;
this.cycle_delay = delay;
});
this.start_button.textContent = "Start";
this.step_button.textContent = "Step";
this.element.appendChild(this.start_button);
this.element.appendChild(this.step_button);
this.element.appendChild(this.range);
this.cycle_delay = 1000;
this.range.value = "0";
this.events.listen(UiEvent.EditOn, () => {
this.disable();
});
this.events.listen(UiEvent.EditOff, () => {
this.enable();
});
}
disable(): void {
this.stop();
this.start_button.setAttribute("disabled", "true");
this.step_button.setAttribute("disabled", "true");
this.range.setAttribute("disabled", "true");
}
enable(): void {
this.start_button.removeAttribute("disabled");
this.step_button.removeAttribute("disabled");
this.range.removeAttribute("disabled");
}
toggle(): void {
if (this.on) {
this.start_button.textContent = "Start";
this.on = false;
} else {
this.on = true;
this.cycle();
this.start_button.textContent = "Storp";
}
}
private cycle(): void {
const loop = (): void => {
if (this.on === false) {
return;
}
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
setTimeout(loop, this.cycle_delay);
};
loop();
}
private step(): void {
if (this.on) {
this.stop();
} else {
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
}
}
start(): void {
if (!this.on) {
this.toggle();
}
}
stop(): void {
if (this.on) {
this.toggle();
}
}
reset(): void {
this.stop();
this.enable();
}
}

View file

@ -1,5 +1,7 @@
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../events";
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "../events";
import { u3 } from "../num";
import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex";
import { UiComponent } from "./uiComponent";
export class RegisterView extends CelledViewer implements UiComponent {
@ -7,6 +9,23 @@ export class RegisterView extends CelledViewer implements UiComponent {
constructor(element: HTMLElement, e: UiEventHandler) {
super(8, 1, element);
this.events = e;
const list = this.cells.map((c) => c.el);
const editor = new EditorContext(list, this.width, this.height, (i, value) => {
this.events.dispatch(UiEvent.RequestRegisterChange, { register_no: i as u3, value });
});
this.events.listen(UiEvent.EditOn, () => {
editor.enable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
this.events.listen(UiEvent.EditOff, () => {
editor.disable();
for (const cell of this.cells) {
cell.el.className = "";
}
});
}
init_cpu_events(c: CpuEventHandler): void {

60
src/ui/ribbon.ts Normal file
View file

@ -0,0 +1,60 @@
import { el, $ } from "../etc";
import { UiEventHandler, UiEvent } from "../events";
import { UiComponent } from "./uiComponent";
function new_button(name: string, img_path: string, additional_class?: string): HTMLButtonElement {
const button = el("button", "", "no_style ribbon_button");
const image = el("img");
image.src = img_path;
image.width = 64;
image.height = 64;
if (additional_class !== undefined) {
button.classList.add(additional_class);
}
button.appendChild(image);
return button;
}
export class Ribbon implements UiComponent {
element: HTMLElement;
events: UiEventHandler;
edit_button: HTMLButtonElement;
console_button: HTMLButtonElement;
display_button: HTMLButtonElement;
explainer_button: HTMLButtonElement;
constructor(element: HTMLElement, event: UiEventHandler) {
this.element = element;
this.events = event;
this.edit_button = new_button("Edit", "pencil.png", "editor_toggle");
this.console_button = new_button("Console", "texout.png");
this.display_button = new_button("Video", "tv.png");
this.explainer_button = new_button("Explainer", "explainer.png");
this.edit_button.addEventListener("click", () => this.edit_toggle());
this.element.appendChild(this.edit_button);
this.element.appendChild(this.console_button);
this.element.appendChild(this.display_button);
this.element.appendChild(this.explainer_button);
}
reset(): void {
const is_on = this.edit_button.classList.contains("on");
if (is_on) {
this.edit_toggle();
}
}
edit_toggle(): void {
const is_on = this.edit_button.classList.contains("on");
if (is_on) {
this.edit_button.classList.remove("on");
$("main").classList.remove("editor");
this.edit_button.classList.add("off");
this.events.dispatch(UiEvent.EditOff);
} else {
this.events.dispatch(UiEvent.EditOn);
$("main").classList.add("editor");
this.edit_button.classList.add("on");
this.edit_button.classList.remove("off");
}
}
}

View file

@ -7,11 +7,11 @@ import { CpuEventHandler, UiEventHandler } from "../events";
export interface UiComponent {
element: HTMLElement;
/** Allows listening and emitting UiEvent's*/
/** Allows listening and emitting UiEvents*/
events: UiEventHandler;
reset: () => void;
/** Allows listening CPUEvent's*/
init_cpu_events: (c: CpuEventHandler) => void;
/** Allows listening CPUEvents*/
init_cpu_events?: (c: CpuEventHandler) => void;
}
export interface UiComponentConstructor {

36
todo.md Normal file
View file

@ -0,0 +1,36 @@
Edit Mode
- Select where program counter is
Implement HCF
Move start/stop/auto logic into computer
Speed control slider behavior
Speed control slider styling
Overclock Box
error in instruction when number out of range (fix new Error("todo"))
UI for screen (toggling (click an icon?))
UI for togging other UI elements
UI for showing which Memory bank is selected
VRAM select instruction
Improve instruction explainer. Clearly show what is an instruction and what is a parameter
Verify mod256 behavior on negatives
UI showing CPU flag(s) (Carry)
Error log
Responsive layout
standardize names of all things
Documentation with standard names
Example Programs
Ui for togging your mother.