split out UI -> CPU signals

This commit is contained in:
Alexander Bass 2024-03-09 13:40:49 -05:00
parent 1f6a95c253
commit 49937af24e
27 changed files with 349 additions and 224 deletions

View file

@ -1,4 +1,4 @@
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "./events"; import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "./events";
import { byte_array_to_js_source, format_hex } from "./etc"; import { byte_array_to_js_source, format_hex } from "./etc";
import { Instruction, ISA } from "./instructionSet"; import { Instruction, ISA } from "./instructionSet";
import { m256, u2, u3, u8 } from "./num"; import { m256, u2, u3, u8 } from "./num";
@ -28,14 +28,6 @@ export class Computer {
private current_instr: TempInstrState | null = null; private current_instr: TempInstrState | null = null;
events: CpuEventHandler = new CpuEventHandler(); events: CpuEventHandler = new CpuEventHandler();
constructor() {
// Add events
for (const [, e_type] of Object.entries(CpuEvent)) {
this.events.register_event(e_type as CpuEvent);
}
this.events.seal();
}
cycle(): void { cycle(): void {
const current_byte = this.getMemorySilent(this.program_counter, 0); const current_byte = this.getMemorySilent(this.program_counter, 0);
@ -102,6 +94,7 @@ export class Computer {
} }
this.events.dispatch(CpuEvent.Cycle); this.events.dispatch(CpuEvent.Cycle);
} }
private getMemorySilent(address: u8, bank_override?: u2): u8 { private getMemorySilent(address: u8, bank_override?: u2): u8 {
const bank = this.banks[bank_override ?? this.bank]; const bank = this.banks[bank_override ?? this.bank];
const value = bank[address] as u8; const value = bank[address] as u8;
@ -116,8 +109,8 @@ export class Computer {
return value; return value;
} }
setMemory(address: u8, value: u8): void { setMemory(address: u8, value: u8, bank?: u2): void {
this.banks[this.bank][address] = value; this.banks[bank ?? this.bank][address] = value;
this.events.dispatch(CpuEvent.MemoryChanged, { address, bank: this.bank, value }); this.events.dispatch(CpuEvent.MemoryChanged, { address, bank: this.bank, value });
} }
@ -173,12 +166,16 @@ export class Computer {
this.carry_flag = false; this.carry_flag = false;
} }
init_events(ui: UiEventHandler): void { init_events(ui: UiCpuSignalHandler): void {
ui.listen(UiEvent.RequestCpuCycle, (cycle_count) => { ui.listen(UiCpuSignal.RequestCpuCycle, (cycle_count) => {
for (let i = 0; i < cycle_count; i++) this.cycle(); for (let i = 0; i < cycle_count; i++) this.cycle();
}); });
ui.listen(UiEvent.RequestMemoryChange, ({ address, value }) => this.setMemory(address, value)); ui.listen(UiCpuSignal.RequestMemoryChange, ({ address, bank, value }) => this.setMemory(address, value, bank));
ui.listen(UiEvent.RequestRegisterChange, ({ register_no, value }) => this.setRegister(register_no, value)); ui.listen(UiCpuSignal.RequestRegisterChange, ({ register_no, value }) => this.setRegister(register_no, value));
ui.listen(UiCpuSignal.RequestMemoryDump, () =>
this.events.dispatch(CpuEvent.MemoryDumped, { memory: this.dump_memory() })
);
ui.listen(UiCpuSignal.RequestCpuReset, () => this.reset());
} }
load_memory(program: Array<u8>): void { load_memory(program: Array<u8>): void {
@ -195,8 +192,8 @@ export class Computer {
this.program_counter = 0; this.program_counter = 0;
} }
dump_memory(): Uint8Array { dump_memory(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
return this.banks[0]; return this.banks;
} }
private step_forward(): void { private step_forward(): void {

View file

@ -14,29 +14,13 @@ export class Event<T> {
export class EventHandler<T> { export class EventHandler<T> {
events: Array<Event<T>> = []; events: Array<Event<T>> = [];
private sealed: boolean;
constructor() {
this.sealed = false;
}
seal(): void {
if (this.sealed) {
throw new Error("Already Sealed");
}
this.sealed = true;
}
register_event(identifier: T): void {
if (this.sealed) {
throw new Error("Can't add event to sealed event handler");
}
const event = new Event<T>(identifier);
this.events.push(event);
}
dispatch(identifier: T, event_data?: unknown): void { dispatch(identifier: T, event_data?: unknown): void {
const event = this.events.find((e) => e.identifier === identifier); const event = this.events.find((e) => e.identifier === identifier);
if (event === undefined) { if (event === undefined) {
throw new Error("Event not found"); // throw new Error("Event not found");
console.log(`Event for ${identifier} was dispatched without any listeners. Data:`, event_data);
return;
} }
for (const callback of event.callbacks) { for (const callback of event.callbacks) {
callback(event_data); callback(event_data);
@ -54,10 +38,13 @@ export class EventHandler<T> {
}); });
} }
listen(identifier: T, callback: (event_data: unknown) => void): void { listen(identifier: T, callback: (event_data: unknown) => void): void {
if (!this.sealed) throw new Error("Event handler must be sealed before adding listener"); let event = this.events.find((e) => e.identifier === identifier);
const event = this.events.find((e) => e.identifier === identifier);
if (event === undefined) { if (event === undefined) {
throw new Error("No event found given identifier"); // If no event found, create it.
// Type system is used to verify that events are valid.
// If this were plain JS, a registerEvent method would likely be better to avoid listening to events that will never exist.
event = new Event(identifier);
this.events.push(event);
} }
event.callbacks.push(callback); event.callbacks.push(callback);
} }

View file

@ -22,16 +22,13 @@ export enum CpuEvent {
Print, Print,
Reset, Reset,
Halt, Halt,
// ClockStarted, MemoryDumped,
// ClockStopped,
MemoryAccessed, MemoryAccessed,
SwitchBank, SwitchBank,
SetFlagCarry, SetFlagCarry,
} }
type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle; type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle;
// | CpuEvent.ClockStarted
// | CpuEvent.ClockStopped;
interface CpuEventMap { interface CpuEventMap {
[CpuEvent.MemoryChanged]: { address: u8; bank: u2; value: u8 }; [CpuEvent.MemoryChanged]: { address: u8; bank: u2; value: u8 };
@ -45,6 +42,7 @@ interface CpuEventMap {
[CpuEvent.SwitchBank]: { bank: u2 }; [CpuEvent.SwitchBank]: { bank: u2 };
[CpuEvent.Print]: string; [CpuEvent.Print]: string;
[CpuEvent.SetFlagCarry]: boolean; [CpuEvent.SetFlagCarry]: boolean;
[CpuEvent.MemoryDumped]: { memory: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] };
} }
export interface CpuEventHandler extends EventHandler<CpuEvent> { export interface CpuEventHandler extends EventHandler<CpuEvent> {
@ -60,30 +58,51 @@ interface CpuEventHandlerConstructor {
export const CpuEventHandler = EventHandler<CpuEvent> as CpuEventHandlerConstructor; export const CpuEventHandler = EventHandler<CpuEvent> as CpuEventHandlerConstructor;
//
// Ui -> CPU Signaler definition
//
export enum UiCpuSignal {
RequestCpuCycle,
RequestMemoryChange,
RequestRegisterChange,
RequestCpuReset,
RequestMemoryDump,
}
type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset | UiCpuSignal.RequestMemoryDump;
interface UiCpuSignalMap {
[UiCpuSignal.RequestCpuCycle]: number;
[UiCpuSignal.RequestMemoryChange]: { address: u8; bank: u2; value: u8 };
[UiCpuSignal.RequestRegisterChange]: { register_no: u3; value: u8 };
}
export interface UiCpuSignalHandler extends EventHandler<UiCpuSignal> {
listen<E extends VoidDataUiCpuSignalList>(type: E, listener: () => void): void;
dispatch<E extends VoidDataUiCpuSignalList>(type: E): void;
listen<E extends keyof UiCpuSignalMap>(type: E, listener: (ev: UiCpuSignalMap[E]) => void): void;
dispatch<E extends keyof UiCpuSignalMap>(type: E, data: UiCpuSignalMap[E]): void;
}
interface UICpuSignalHandlerConstructor {
new (): UiCpuSignalHandler;
}
export const UiCpuSignalHandler = EventHandler<UiCpuSignal> as UICpuSignalHandlerConstructor;
// //
// Ui Event Handler Definition // Ui Event Handler Definition
// //
export enum UiEvent { export enum UiEvent {
// Maybe move these into a UI -> CPU signal system?
RequestCpuCycle,
RequestMemoryChange,
RequestRegisterChange,
// Ui Events
EditOn, EditOn,
EditOff, EditOff,
ConsoleOn, ChangeViewBank,
ConsoleOff,
ExplainerOn,
ExplainerOff,
VideoOn,
VideoOff,
} }
interface UiEventMap { interface UiEventMap {
[UiEvent.RequestCpuCycle]: number; [UiEvent.ChangeViewBank]: { bank: u2 };
[UiEvent.RequestMemoryChange]: { address: u8; value: u8 };
[UiEvent.RequestRegisterChange]: { register_no: u3; value: u8 };
} }
type VoidDataUiEventList = UiEvent.EditOn | UiEvent.EditOff; type VoidDataUiEventList = UiEvent.EditOn | UiEvent.EditOff;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

View file

@ -7,25 +7,46 @@
<title>Virtual 8-Bit Computer</title> <title>Virtual 8-Bit Computer</title>
</head> </head>
<body id="root"> <body id="root">
<noscript>
This computer requires JavaScript. Your browser either doesn't support it, or you have it disabled.
</noscript>
<main> <main>
<div id="grid"> <div id="grid">
<div id="title">VIRTUAL 8-BIT COMPUTER</div> <div id="title">VIRTUAL 8-BIT COMPUTER</div>
<div id="registers"></div> <div id="registers"></div>
<div id="labelcontainer"> <div id="labelcontainer">
<div id="registers_label">REGISTERS</div> <div id="registers_label">REGISTERS</div>
<div id="memory_label">MEMORY↯</div> <div id="memory_label">MEMORY↯</div>
</div> </div>
<div id="memory"></div> <div id="memory"></div>
<div id="controls_bar"> <div id="controls_bar">
<span id="controls_buttons"></span> <span id="controls_buttons"></span>
<label for="binary_upload" class="button">Load Binary</label> <span id="save_load_buttons"></span>
<input id="binary_upload" name="binary_upload" id="binary_upload" style="display: none" type="file" />
<button type="button" id="save_button">Save</button>
<button type="button" id="edit_button"></button> <button type="button" id="edit_button"></button>
</div> </div>
<span id="cycles"></span> <span id="cycles"></span>
<div id="memory_bank_view">
<div id="bank_boxes">
<button class="nostyle">1</button>
<button class="nostyle selected">2</button>
<button class="nostyle">3</button>
<button class="nostyle">4</button>
</div>
<script>
const d = document.getElementById("bank_boxes");
const a = [...d.children];
for (const b of a) {
b.addEventListener("click", () => {
a.forEach((ab) => ab.classList.remove("selected"));
b.classList.add("selected");
});
}
console.log(a);
</script>
</div>
</div> </div>
<div id="window_box"> <div id="window_box">
<div id="instruction_explainer"></div> <div id="instruction_explainer"></div>
<div id="printout"></div> <div id="printout"></div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

View file

@ -43,40 +43,12 @@ function main(): void {
const ui = new UI(); const ui = new UI();
ui.init_events(computer.events); ui.init_events(computer.events);
computer.load_memory(program); computer.load_memory(program);
computer.init_events(ui.events); computer.init_events(ui.cpu_signaler);
window.comp = computer; window.comp = computer;
window.ui = ui; window.ui = ui;
$("ISA").textContent = generate_isa(ISA); $("ISA").textContent = generate_isa(ISA);
$("binary_upload").addEventListener("change", (e) => {
const t = e.target;
if (t === null) {
return;
}
const file: File | undefined = (t as HTMLInputElement).files?.[0];
if (file === undefined) {
console.log("No files attribute on file input");
return;
}
const reader = new FileReader();
console.log(file);
reader.addEventListener("load", (e) => {
if (e.target !== null) {
const data = e.target.result;
if (data instanceof ArrayBuffer) {
const view = new Uint8Array(data);
const array = [...view] as Array<u8>;
computer.reset();
computer.load_memory(array);
} else {
console.log("not array");
}
}
});
reader.readAsArrayBuffer(file);
});
let fire = false; let fire = false;
window.firehose = (): void => { window.firehose = (): void => {
if (fire === false) { if (fire === false) {
@ -88,17 +60,6 @@ function main(): void {
console.error("Firehose already started"); console.error("Firehose already started");
} }
}; };
$("save_button").addEventListener("click", () => {
const memory = computer.dump_memory();
const blob = new Blob([memory], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "bin.bin";
link.click();
});
} }
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {

View file

@ -28,8 +28,21 @@ export type u2 = 0 | 1 | 2 | 3;
export type u3 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; export type u3 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7;
export type u4 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15; export type u4 = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15;
/**
* Takes the input number and returns it modulus 256. Converts to `u8`
*
* @param number
* @returns number mod 256 (u8)
*/
export const m256 = (number: number): u8 => (number % 256) as u8; export const m256 = (number: number): u8 => (number % 256) as u8;
/**
* Determines whether a number is a u2 type (unsigned 2-bit integer).
* Does not check for non integers
*
* @param n Input number to be checked
* @returns true or false
*/
export function isU2(n: number): n is u2 { export function isU2(n: number): n is u2 {
if (n < 4 && n >= 0) { if (n < 4 && n >= 0) {
return true; return true;
@ -41,9 +54,8 @@ export function isU2(n: number): n is u2 {
* Determines whether a number is a u3 type (unsigned 3-bit integer). * Determines whether a number is a u3 type (unsigned 3-bit integer).
* Does not check for non integers * Does not check for non integers
* *
* @param n - Input number to be checked * @param n Input number to be checked
* @returns true or false * @returns true or false
*
*/ */
export function isU3(n: number): n is u3 { export function isU3(n: number): n is u3 {
if (n < 8 && n >= 0) { if (n < 8 && n >= 0) {
@ -51,13 +63,13 @@ export function isU3(n: number): n is u3 {
} }
return false; return false;
} }
/** /**
* Determines whether a number is a u4 type (unsigned 4-bit integer). * Determines whether a number is a u4 type (unsigned 4-bit integer).
* Does not check for non integers * Does not check for non integers
* *
* @param n - Input number to be checked * @param n Input number to be checked
* @returns true or false * @returns true or false
*
*/ */
export function isU4(n: number): n is u4 { export function isU4(n: number): n is u4 {
if (n < 16 && n >= 0) { if (n < 16 && n >= 0) {
@ -65,13 +77,13 @@ export function isU4(n: number): n is u4 {
} }
return false; return false;
} }
/** /**
* Determines whether a number is a u8 type (unsigned 8-bit integer). * Determines whether a number is a u8 type (unsigned 8-bit integer).
* Does not check for non integers * Does not check for non integers
* *
* @param n - Input number to be checked * @param n Input number to be checked
* @returns true or false * @returns true or false
*
*/ */
export function isU8(n: number): n is u8 { export function isU8(n: number): n is u8 {
if (n < 256 && n >= 0) { if (n < 256 && n >= 0) {

View file

@ -38,17 +38,6 @@ label.button:hover {
color: white; color: white;
} }
#controls_bar {
grid-area: buttons;
display: flex;
gap: 10px;
#controls_buttons {
display: flex;
gap: inherit;
}
}
input[type="range"] { input[type="range"] {
background-color: transparent; background-color: transparent;
-webkit-appearance: none; -webkit-appearance: none;

View file

@ -29,13 +29,6 @@
color: lightgray; color: lightgray;
} }
#labelcontainer {
column-gap: 18px;
font-size: 0.85em;
display: flex;
align-items: center;
user-select: none;
}
.celled_viewer { .celled_viewer {
display: grid; display: grid;
max-width: fit-content; max-width: fit-content;

View file

@ -33,13 +33,12 @@ main {
grid-template-columns: min-content min-content min-content; grid-template-columns: min-content min-content min-content;
grid-template-rows: min-content min-content min-content; grid-template-rows: min-content min-content min-content;
grid-template-areas: grid-template-areas:
"cycles registers regmemlabel" ". regmemlabel . cycles "
"title memory memory" ". registers . bank "
". buttons buttons "; "title memory memory memory"
". buttons buttons buttons ";
#memory { #memory {
grid-area: memory; grid-area: memory;
// grid-column: 2/4;
// grid-row: 2/6;
} }
#window_box { #window_box {
grid-area: windowbox; grid-area: windowbox;
@ -47,12 +46,15 @@ main {
#registers { #registers {
grid-area: registers; grid-area: registers;
} }
#memory_bank_view {
grid-area: bank;
}
#labelcontainer { #labelcontainer {
grid-area: regmemlabel; grid-area: regmemlabel;
} }
#cycles { #cycles {
grid-area: cycles; grid-area: cycles;
text-align: left; text-align: right;
align-self: center; align-self: center;
font-size: 0.48em; font-size: 0.48em;
user-select: none; user-select: none;
@ -66,6 +68,45 @@ main {
} }
} }
#labelcontainer {
column-gap: 18px;
font-size: 0.75em;
display: flex;
flex-direction: row;
align-items: center;
user-select: none;
}
#memory_bank_view {
display: flex;
#bank_boxes {
display: flex;
align-items: flex-end;
$pad: 8px;
$border-width: 5px;
$border: $border-width solid var(--border);
button {
border-top: $border;
border-bottom: unset;
padding-inline: $pad + $border-width;
}
button:first-child {
border-left: $border;
padding-left: $pad;
}
button:last-child {
padding-right: $pad;
border-right: $border;
}
button.selected {
color: lightgray;
padding-block: 5px;
padding-inline: $pad;
border-inline: $border;
}
}
}
.invalid { .invalid {
--color: var(--mem-invalid); --color: var(--mem-invalid);
} }
@ -91,3 +132,38 @@ div#main.editor {
} }
} }
} }
#controls_bar {
grid-area: buttons;
display: flex;
gap: 10px;
#controls_buttons {
display: flex;
gap: inherit;
justify-content: inherit;
}
#save_load_buttons {
display: flex;
gap: inherit;
justify-content: inherit;
}
}
#edit_button {
aspect-ratio: 1;
display: flex;
justify-content: center;
align-content: center;
img {
min-height: 30px;
min-width: 30px;
&:hover {
filter: grayscale(100%) brightness(500%);
}
}
&.on img {
filter: grayscale(100%) brightness(500%);
}
}

View file

@ -14,22 +14,18 @@
flex-direction: column; flex-direction: column;
gap: 10px; gap: 10px;
margin-left: 10px; margin-left: 10px;
max-width: 500px; width: 500px;
.window { .window {
overflow-y: hidden; overflow-y: hidden;
position: relative; position: relative;
border: 5px Solid var(--border); border: 5px Solid var(--border);
border-bottom: unset; border-bottom: unset;
&.collapsed {
.window_title {
// border-bottom: unset;
}
}
.window_title { .window_title {
position: sticky;
user-select: none;
display: flex; display: flex;
position: sticky;
align-items: center; align-items: center;
user-select: none;
justify-content: space-between; justify-content: space-between;
font-size: 0.6em; font-size: 0.6em;
color: lightgray; color: lightgray;
@ -45,21 +41,24 @@
repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px); repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px);
#text { #text {
display: inline-block; word-break: keep-all;
white-space: nowrap;
text-align: center; text-align: center;
height: 100%;
padding-inline: 10px; padding-inline: 10px;
background-color: black; background-color: black;
} }
#collapse_button { #collapse_button {
height: 23px !important; height: 23px;
aspect-ratio: 1; aspect-ratio: 1;
border: 2px solid white; border: 2px solid yellow;
background-color: black; background-color: black;
margin-right: 3px; margin-right: 3px;
} }
} }
} }
.window.collapsed > :not(:first-child) {
display: none;
}
} }
.window#tv { .window#tv {
@ -71,11 +70,10 @@
} }
#instruction_explainer { #instruction_explainer {
grid-area: explainer;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 5px; gap: 5px;
height: 400px; height: 300px;
#expl_box { #expl_box {
padding-inline: 20px; padding-inline: 20px;
padding-block-start: 10px; padding-block-start: 10px;
@ -97,7 +95,6 @@
} }
#printout { #printout {
grid-area: printout;
height: 500px; height: 500px;
word-wrap: break-word; word-wrap: break-word;
word-break: break-all; word-break: break-all;

View file

@ -1,28 +1,22 @@
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "./events"; import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEvent, UiEventHandler } from "./events";
import { $ } from "./etc"; import { $ } from "./etc";
import { InstructionExplainer } from "./ui/windows/instructionExplainer"; import { InstructionExplainer } from "./ui/windows/instructionExplainer";
import { MemoryView } from "./ui/memoryView"; import { MemoryView } from "./ui/memoryView";
import { frequencyIndicator } from "./ui/frequencyIndicator"; import { frequencyIndicator } from "./ui/frequencyIndicator";
import { RegisterView } from "./ui/registerView"; import { RegisterView } from "./ui/registerView";
import { Screen } from "./ui/windows/screen"; import { Screen } from "./ui/windows/screen";
import { EditButton } from "./ui/edit_button"; import { EditButton } from "./ui/editButton";
import { UiComponent, UiComponentConstructor } from "./ui/uiComponent"; import { UiComponent, UiComponentConstructor } from "./ui/uiComponent";
import { pausePlay } from "./ui/pausePlay"; import { pausePlay } from "./ui/pausePlay";
import { Printout } from "./ui/windows/printout"; import { Printout } from "./ui/windows/printout";
import { SaveLoad } from "./ui/saveLoad";
export class UI { export class UI {
events: UiEventHandler = new UiEventHandler(); ui_events: UiEventHandler = new UiEventHandler();
cpu_signaler: UiCpuSignalHandler = new UiCpuSignalHandler();
private components: Array<UiComponent>; private components: Array<UiComponent> = [];
constructor() { constructor() {
for (const [, e_type] of Object.entries(UiEvent)) {
this.events.register_event(e_type as UiEvent);
}
this.events.seal();
this.components = [];
this.register_component(MemoryView, $("memory")); this.register_component(MemoryView, $("memory"));
this.register_component(frequencyIndicator, $("cycles")); this.register_component(frequencyIndicator, $("cycles"));
this.register_component(InstructionExplainer, $("instruction_explainer")); this.register_component(InstructionExplainer, $("instruction_explainer"));
@ -31,15 +25,16 @@ export class UI {
this.register_component(Printout, $("printout")); this.register_component(Printout, $("printout"));
this.register_component(EditButton, $("edit_button")); this.register_component(EditButton, $("edit_button"));
this.register_component(pausePlay, $("controls_buttons")); this.register_component(pausePlay, $("controls_buttons"));
this.register_component(SaveLoad, $("save_load_buttons"));
const pp_button = $("pause_play_button");
} }
private register_component(ctor: UiComponentConstructor, e: HTMLElement): void { private register_component(ctor: UiComponentConstructor, e: HTMLElement): void {
if (e === undefined) { if (e === undefined) {
// shouldn't be able to happen, but I sometimes let the type system slide when getting elements from the DOM.
console.log(ctor); console.log(ctor);
throw new Error("Could not find HTML element while registering UI component"); throw new Error("Could not find HTML element while registering UI component");
} }
const component = new ctor(e, this.events); const component = new ctor(e, this.ui_events, this.cpu_signaler);
this.components.push(component); this.components.push(component);
} }
@ -48,14 +43,10 @@ export class UI {
this.reset(); this.reset();
}); });
for (const c of this.components) { for (const c of this.components) if (c.init_cpu_events) c.init_cpu_events(cpu_events);
if (c.init_cpu_events) c.init_cpu_events(cpu_events);
}
} }
reset(): void { reset(): void {
for (const c of this.components) { for (const c of this.components) if (c.reset) c.reset();
c.reset();
}
} }
} }

View file

@ -8,22 +8,18 @@ const HEX_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "
export class EditorContext { export class EditorContext {
private list: Array<HTMLElement>; private list: Array<HTMLElement>;
private width: number; private width: number;
private height: number;
private enabled: boolean = false; private enabled: boolean = false;
private current_cell_info: { left?: string; right?: string; old?: string }; private current_cell_info: { left?: string; right?: string; old?: string };
private edit_callback: (n: number, value: u8) => void; private edit_callback: (n: number, value: u8) => void;
constructor(list: Array<HTMLElement>, width: number, height: number, callback: (n: number, value: u8) => void) { constructor(list: Array<HTMLElement>, width: number, callback: (n: number, value: u8) => void) {
this.list = list; this.list = list;
this.width = width; this.width = width;
this.height = height;
this.edit_callback = callback; this.edit_callback = callback;
this.current_cell_info = {}; this.current_cell_info = {};
for (const [i, cell] of this.list.entries()) { for (const [i, cell] of this.list.entries()) {
cell.setAttribute("spellcheck", "false"); cell.setAttribute("spellcheck", "false");
cell.addEventListener("keydown", (e) => { cell.addEventListener("keydown", (e) => this.keydown(e, i));
this.keydown(e, i);
});
cell.addEventListener("input", (e) => { cell.addEventListener("input", (e) => {
const target = e.target as HTMLElement; const target = e.target as HTMLElement;
if (target === null) return; if (target === null) return;

View file

@ -8,9 +8,9 @@ export class frequencyIndicator implements UiComponent {
private last_value: number = 0; private last_value: number = 0;
private last_time: number = 0; private last_time: number = 0;
events: UiEventHandler; events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) { constructor(element: HTMLElement, events: UiEventHandler) {
this.element = element; this.element = element;
this.events = e; this.events = events;
this.start(); this.start();
} }

View file

@ -1,26 +1,24 @@
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "../events"; import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../events";
import { ParamType } from "../instructionSet"; import { ParamType } from "../instructionSet";
import { u8 } from "../num.js"; import { u8 } from "../num.js";
import { UiComponent } from "./uiComponent"; import { UiComponent } from "./uiComponent";
import { CelledViewer } from "./celledViewer"; import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex"; import { EditorContext } from "./editableHex";
type MemoryCell = {
el: HTMLDivElement;
};
export class MemoryView extends CelledViewer implements UiComponent { export class MemoryView extends CelledViewer implements UiComponent {
program_counter: u8 = 0; program_counter: u8 = 0;
last_accessed_cell: u8 | null = null; last_accessed_cell: u8 | null = null;
events: UiEventHandler; events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) { cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(16, 16, element); super(16, 16, element);
this.program_counter = 0; this.program_counter = 0;
this.events = e; this.events = events;
this.cpu_signals = cpu_signals;
const list = this.cells.map((c) => c.el); const list = this.cells.map((c) => c.el);
const editor = new EditorContext(list, this.width, this.height, (i, value) => { const editor = new EditorContext(list, this.width, (i, value) => {
this.events.dispatch(UiEvent.RequestMemoryChange, { address: i as u8, value }); this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address: i as u8, bank: 0, value });
}); });
this.events.listen(UiEvent.EditOn, () => { this.events.listen(UiEvent.EditOn, () => {
editor.enable(); editor.enable();

View file

@ -1,5 +1,5 @@
import { el } from "../etc"; import { el } from "../etc";
import { UiEventHandler, UiEvent } from "../events"; import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../events";
import { UiComponent } from "./uiComponent"; import { UiComponent } from "./uiComponent";
const MAX_SLIDER = 1000; const MAX_SLIDER = 1000;
@ -12,9 +12,12 @@ export class pausePlay implements UiComponent {
events: UiEventHandler; events: UiEventHandler;
on: boolean = false; on: boolean = false;
cycle_delay: number; cycle_delay: number;
constructor(element: HTMLElement, events: UiEventHandler) { cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.element = element; this.element = element;
this.events = events; this.events = events;
this.cpu_signals = cpu_signals;
this.start_button = el("button", "pause_play_button"); this.start_button = el("button", "pause_play_button");
this.step_button = el("button", "step_button"); this.step_button = el("button", "step_button");
this.range = el("input", "speed_range"); this.range = el("input", "speed_range");
@ -72,7 +75,7 @@ export class pausePlay implements UiComponent {
if (this.on === false) { if (this.on === false) {
return; return;
} }
this.events.dispatch(UiEvent.RequestCpuCycle, 1); this.cpu_signals.dispatch(UiCpuSignal.RequestCpuCycle, 1);
setTimeout(loop, this.cycle_delay); setTimeout(loop, this.cycle_delay);
}; };
loop(); loop();
@ -81,7 +84,7 @@ export class pausePlay implements UiComponent {
if (this.on) { if (this.on) {
this.stop(); this.stop();
} else { } else {
this.events.dispatch(UiEvent.RequestCpuCycle, 1); this.cpu_signals.dispatch(UiCpuSignal.RequestCpuCycle, 1);
} }
} }

View file

@ -1,4 +1,4 @@
import { CpuEvent, CpuEventHandler, UiEvent, UiEventHandler } from "../events"; import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../events";
import { u3 } from "../num"; import { u3 } from "../num";
import { CelledViewer } from "./celledViewer"; import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex"; import { EditorContext } from "./editableHex";
@ -6,13 +6,15 @@ import { UiComponent } from "./uiComponent";
export class RegisterView extends CelledViewer implements UiComponent { export class RegisterView extends CelledViewer implements UiComponent {
events: UiEventHandler; events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) { cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(8, 1, element); super(8, 1, element);
this.events = e; this.events = events;
this.cpu_signals = cpu_signals;
const list = this.cells.map((c) => c.el); const list = this.cells.map((c) => c.el);
const editor = new EditorContext(list, this.width, this.height, (i, value) => { const editor = new EditorContext(list, this.width, (i, value) => {
this.events.dispatch(UiEvent.RequestRegisterChange, { register_no: i as u3, value }); this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: i as u3, value });
}); });
this.events.listen(UiEvent.EditOn, () => { this.events.listen(UiEvent.EditOn, () => {
editor.enable(); editor.enable();

91
src/ui/saveLoad.ts Normal file
View file

@ -0,0 +1,91 @@
import { el } from "../etc";
import { UiEventHandler, CpuEventHandler, CpuEvent, UiCpuSignalHandler, UiCpuSignal } from "../events";
import { u2, u8, m256 } from "../num";
import { UiComponent } from "./uiComponent";
export class SaveLoad implements UiComponent {
element: HTMLElement;
events: UiEventHandler;
save_button: HTMLButtonElement;
binary_upload: HTMLInputElement;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.element = element;
this.events = events;
this.cpu_signals = cpu_signals;
this.save_button = el("button", "save_button");
this.binary_upload = el("input", "binary_upload");
this.binary_upload.type = "file";
this.binary_upload.name = "binary_upload";
this.binary_upload.style.display = "none";
const label = el("label");
this.save_button.textContent = "Save";
label.textContent = "Load Binary";
label.classList.add("button");
label.setAttribute("for", "binary_upload");
this.element.appendChild(this.binary_upload);
this.element.appendChild(label);
this.element.appendChild(this.save_button);
this.save_button.addEventListener("click", () => {
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump);
});
this.binary_upload.addEventListener("change", (e) => {
this.upload_changed(e);
});
}
private upload_changed(e: Event): void {
const t = e.target;
if (t === null) {
return;
}
const file: File | undefined = (t as HTMLInputElement).files?.[0];
if (file === undefined) {
console.log("No files attribute on file input");
return;
}
const reader = new FileReader();
console.log(file);
reader.addEventListener("load", (e) => {
if (e.target !== null) {
const data = e.target.result;
if (data instanceof ArrayBuffer) {
const view = new Uint8Array(data);
const array = [...view] as Array<u8>;
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset);
for (const [i, v] of array.entries()) {
const address = m256(i);
const bank = Math.floor(i / 256) as u2;
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value: v });
}
} else {
console.log("not array");
}
}
});
reader.readAsArrayBuffer(file);
}
// eslint-disable-next-line class-methods-use-this
init_cpu_events(e: CpuEventHandler): void {
e.listen(CpuEvent.MemoryDumped, ({ memory }) => {
const flattened = new Uint8Array(256 * memory.length);
for (let x = 0; x < 4; x++) {
for (let y = 0; y < 256; x++) {
flattened[256 * x + y] = memory[x][y];
}
}
const blob = new Blob([flattened], { type: "application/octet-stream" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "bin.bin";
link.click();
link.remove();
});
}
}

View file

@ -3,19 +3,28 @@
* @copyright Alexander Bass 2024 * @copyright Alexander Bass 2024
* @license GPL-3.0 * @license GPL-3.0
*/ */
import { CpuEventHandler, UiEventHandler } from "../events"; import { CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../events";
// A UiComponent represents one DOM element and its contents.
// A UiComponent reacts to events to change its state, and creates events when it wants to communicate with other UiComponents, or with the CPU.
// These event/signal handlers are available to each UiComponent:
// - UiEventHandler: dispatch/listen to events created as a result of Ui actions
// - CpuEventHandler: listen to events created as a result of CPU actions
// - UiCpuEventSignaler: dispatch signals to request actions from the CPU
export interface UiComponent { export interface UiComponent {
element: HTMLElement; element: HTMLElement;
/** Allows listening and emitting UiEvents*/ /** Allows listening and emitting UiEvents*/
events: UiEventHandler; events: UiEventHandler;
/** Creating signals for the cpu to process */
cpu_signals?: UiCpuSignalHandler;
/** Completely reset the state of the component */ /** Completely reset the state of the component */
reset: () => void; reset?: () => void;
soft_reset?: () => void; soft_reset?: () => void;
/** Allows listening CPUEvents*/ /** Allows listening CPUEvents*/
init_cpu_events?: (c: CpuEventHandler) => void; init_cpu_events?: (c: CpuEventHandler) => void;
} }
export interface UiComponentConstructor { export interface UiComponentConstructor {
new (el: HTMLElement, ue: UiEventHandler): UiComponent; new (el: HTMLElement, ui_event_handler: UiEventHandler, cpu_signaler: UiCpuSignalHandler): UiComponent;
} }

View file

@ -1,8 +1,8 @@
import { el } from "../etc"; import { el } from "../etc";
export abstract class WindowBox { export abstract class WindowBox {
element: HTMLElement; element: HTMLElement;
readonly title: string;
title_bar: HTMLElement; title_bar: HTMLElement;
readonly title: string;
private resize: HTMLElement; private resize: HTMLElement;
private collapse_button: HTMLButtonElement; private collapse_button: HTMLButtonElement;
private collapsed: boolean = false; private collapsed: boolean = false;

View file

@ -1,22 +0,0 @@
import { UiEventHandler, CpuEventHandler, CpuEvent } from "../../events";
import { u1, u2 } from "../../num";
import { UiComponent } from "../uiComponent";
class BankIndicator implements UiComponent {
element: HTMLElement;
events: UiEventHandler;
constructor(element: HTMLElement, events: UiEventHandler) {
this.element = element;
this.events = events;
}
reset(): void {}
select_bank(bank_no: u2): void {}
init_cpu_events(c: CpuEventHandler): void {
c.listen(CpuEvent.SwitchBank, ({ bank }) => {
this.select_bank(bank);
});
}
}

View file

@ -1,5 +1,5 @@
import { el, format_hex } from "../../etc"; import { el, format_hex } from "../../etc";
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events"; import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events";
import { Instruction, ParamType, ParameterType } from "../../instructionSet"; import { Instruction, ParamType, ParameterType } from "../../instructionSet";
import { u8 } from "../../num"; import { u8 } from "../../num";
import { WindowBox } from "../windowBox"; import { WindowBox } from "../windowBox";
@ -7,9 +7,11 @@ import { UiComponent } from "../uiComponent";
export class InstructionExplainer extends WindowBox implements UiComponent { export class InstructionExplainer extends WindowBox implements UiComponent {
events: UiEventHandler; events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) { cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(element, "Instruction Explainer"); super(element, "Instruction Explainer");
this.events = e; this.cpu_signals = cpu_signals;
this.events = events;
} }
add_instruction(instr: Instruction, pos: u8, byte: u8): void { add_instruction(instr: Instruction, pos: u8, byte: u8): void {
this.reset(); this.reset();

View file

@ -1,13 +1,15 @@
import { el } from "../../etc"; import { el } from "../../etc";
import { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events"; import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events";
import { WindowBox } from "../windowBox"; import { WindowBox } from "../windowBox";
import { UiComponent } from "../uiComponent"; import { UiComponent } from "../uiComponent";
export class Printout extends WindowBox implements UiComponent { export class Printout extends WindowBox implements UiComponent {
events: UiEventHandler; events: UiEventHandler;
text_box: HTMLElement; text_box: HTMLElement;
constructor(element: HTMLElement, events: UiEventHandler) { cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(element, "Printout"); super(element, "Printout");
this.cpu_signals = cpu_signals;
this.events = events; this.events = events;
this.text_box = el("div", "printout_text"); this.text_box = el("div", "printout_text");
this.element.appendChild(this.text_box); this.element.appendChild(this.text_box);

View file

@ -22,6 +22,7 @@ export class Screen extends WindowBox implements UiComponent {
} }
this.ctx = ctx; this.ctx = ctx;
this.element.appendChild(this.screen); this.element.appendChild(this.screen);
this.test_pattern();
} }
private test_pattern(): void { private test_pattern(): void {