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 { Instruction, ISA } from "./instructionSet";
import { m256, u2, u3, u8 } from "./num";
@ -28,14 +28,6 @@ export class Computer {
private current_instr: TempInstrState | null = null;
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 {
const current_byte = this.getMemorySilent(this.program_counter, 0);
@ -102,6 +94,7 @@ export class Computer {
}
this.events.dispatch(CpuEvent.Cycle);
}
private getMemorySilent(address: u8, bank_override?: u2): u8 {
const bank = this.banks[bank_override ?? this.bank];
const value = bank[address] as u8;
@ -116,8 +109,8 @@ export class Computer {
return value;
}
setMemory(address: u8, value: u8): void {
this.banks[this.bank][address] = value;
setMemory(address: u8, value: u8, bank?: u2): void {
this.banks[bank ?? this.bank][address] = value;
this.events.dispatch(CpuEvent.MemoryChanged, { address, bank: this.bank, value });
}
@ -173,12 +166,16 @@ export class Computer {
this.carry_flag = false;
}
init_events(ui: UiEventHandler): void {
ui.listen(UiEvent.RequestCpuCycle, (cycle_count) => {
init_events(ui: UiCpuSignalHandler): void {
ui.listen(UiCpuSignal.RequestCpuCycle, (cycle_count) => {
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));
ui.listen(UiCpuSignal.RequestMemoryChange, ({ address, bank, value }) => this.setMemory(address, value, bank));
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 {
@ -195,8 +192,8 @@ export class Computer {
this.program_counter = 0;
}
dump_memory(): Uint8Array {
return this.banks[0];
dump_memory(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
return this.banks;
}
private step_forward(): void {

View file

@ -14,29 +14,13 @@ export class Event<T> {
export class EventHandler<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 {
const event = this.events.find((e) => e.identifier === identifier);
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) {
callback(event_data);
@ -54,10 +38,13 @@ export class EventHandler<T> {
});
}
listen(identifier: T, callback: (event_data: unknown) => void): void {
if (!this.sealed) throw new Error("Event handler must be sealed before adding listener");
const event = this.events.find((e) => e.identifier === identifier);
let event = this.events.find((e) => e.identifier === identifier);
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);
}

View file

@ -22,16 +22,13 @@ export enum CpuEvent {
Print,
Reset,
Halt,
// ClockStarted,
// ClockStopped,
MemoryDumped,
MemoryAccessed,
SwitchBank,
SetFlagCarry,
}
type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle;
// | CpuEvent.ClockStarted
// | CpuEvent.ClockStopped;
interface CpuEventMap {
[CpuEvent.MemoryChanged]: { address: u8; bank: u2; value: u8 };
@ -45,6 +42,7 @@ interface CpuEventMap {
[CpuEvent.SwitchBank]: { bank: u2 };
[CpuEvent.Print]: string;
[CpuEvent.SetFlagCarry]: boolean;
[CpuEvent.MemoryDumped]: { memory: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] };
}
export interface CpuEventHandler extends EventHandler<CpuEvent> {
@ -60,30 +58,51 @@ interface 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
//
export enum UiEvent {
// Maybe move these into a UI -> CPU signal system?
RequestCpuCycle,
RequestMemoryChange,
RequestRegisterChange,
// Ui Events
EditOn,
EditOff,
ConsoleOn,
ConsoleOff,
ExplainerOn,
ExplainerOff,
VideoOn,
VideoOff,
ChangeViewBank,
}
interface UiEventMap {
[UiEvent.RequestCpuCycle]: number;
[UiEvent.RequestMemoryChange]: { address: u8; value: u8 };
[UiEvent.RequestRegisterChange]: { register_no: u3; value: u8 };
[UiEvent.ChangeViewBank]: { bank: u2 };
}
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>
</head>
<body id="root">
<noscript>
This computer requires JavaScript. Your browser either doesn't support it, or you have it disabled.
</noscript>
<main>
<div id="grid">
<div id="title">VIRTUAL 8-BIT COMPUTER</div>
<div id="registers"></div>
<div id="labelcontainer">
<div id="registers_label">REGISTERS</div>
<div id="registers_label">REGISTERS</div>
<div id="memory_label">MEMORY↯</div>
</div>
<div id="memory"></div>
<div id="controls_bar">
<span id="controls_buttons"></span>
<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>
<span id="save_load_buttons"></span>
<button type="button" id="edit_button"></button>
</div>
<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 id="window_box">
<div id="instruction_explainer"></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();
ui.init_events(computer.events);
computer.load_memory(program);
computer.init_events(ui.events);
computer.init_events(ui.cpu_signaler);
window.comp = computer;
window.ui = ui;
$("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;
window.firehose = (): void => {
if (fire === false) {
@ -88,17 +60,6 @@ function main(): void {
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", () => {

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 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;
/**
* 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 {
if (n < 4 && n >= 0) {
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).
* Does not check for non integers
*
* @param n - Input number to be checked
* @param n Input number to be checked
* @returns true or false
*
*/
export function isU3(n: number): n is u3 {
if (n < 8 && n >= 0) {
@ -51,13 +63,13 @@ export function isU3(n: number): n is u3 {
}
return false;
}
/**
* Determines whether a number is a u4 type (unsigned 4-bit integer).
* Does not check for non integers
*
* @param n - Input number to be checked
* @param n Input number to be checked
* @returns true or false
*
*/
export function isU4(n: number): n is u4 {
if (n < 16 && n >= 0) {
@ -65,13 +77,13 @@ export function isU4(n: number): n is u4 {
}
return false;
}
/**
* Determines whether a number is a u8 type (unsigned 8-bit integer).
* Does not check for non integers
*
* @param n - Input number to be checked
* @param n Input number to be checked
* @returns true or false
*
*/
export function isU8(n: number): n is u8 {
if (n < 256 && n >= 0) {

View file

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

View file

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

View file

@ -33,13 +33,12 @@ main {
grid-template-columns: min-content min-content min-content;
grid-template-rows: min-content min-content min-content;
grid-template-areas:
"cycles registers regmemlabel"
"title memory memory"
". buttons buttons ";
". regmemlabel . cycles "
". registers . bank "
"title memory memory memory"
". buttons buttons buttons ";
#memory {
grid-area: memory;
// grid-column: 2/4;
// grid-row: 2/6;
}
#window_box {
grid-area: windowbox;
@ -47,12 +46,15 @@ main {
#registers {
grid-area: registers;
}
#memory_bank_view {
grid-area: bank;
}
#labelcontainer {
grid-area: regmemlabel;
}
#cycles {
grid-area: cycles;
text-align: left;
text-align: right;
align-self: center;
font-size: 0.48em;
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 {
--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;
gap: 10px;
margin-left: 10px;
max-width: 500px;
width: 500px;
.window {
overflow-y: hidden;
position: relative;
border: 5px Solid var(--border);
border-bottom: unset;
&.collapsed {
.window_title {
// border-bottom: unset;
}
}
.window_title {
position: sticky;
user-select: none;
display: flex;
position: sticky;
align-items: center;
user-select: none;
justify-content: space-between;
font-size: 0.6em;
color: lightgray;
@ -45,21 +41,24 @@
repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px);
#text {
display: inline-block;
word-break: keep-all;
white-space: nowrap;
text-align: center;
height: 100%;
padding-inline: 10px;
background-color: black;
}
#collapse_button {
height: 23px !important;
height: 23px;
aspect-ratio: 1;
border: 2px solid white;
border: 2px solid yellow;
background-color: black;
margin-right: 3px;
}
}
}
.window.collapsed > :not(:first-child) {
display: none;
}
}
.window#tv {
@ -71,11 +70,10 @@
}
#instruction_explainer {
grid-area: explainer;
display: flex;
flex-direction: column;
gap: 5px;
height: 400px;
height: 300px;
#expl_box {
padding-inline: 20px;
padding-block-start: 10px;
@ -97,7 +95,6 @@
}
#printout {
grid-area: printout;
height: 500px;
word-wrap: break-word;
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 { InstructionExplainer } from "./ui/windows/instructionExplainer";
import { MemoryView } from "./ui/memoryView";
import { frequencyIndicator } from "./ui/frequencyIndicator";
import { RegisterView } from "./ui/registerView";
import { Screen } from "./ui/windows/screen";
import { EditButton } from "./ui/edit_button";
import { EditButton } from "./ui/editButton";
import { UiComponent, UiComponentConstructor } from "./ui/uiComponent";
import { pausePlay } from "./ui/pausePlay";
import { Printout } from "./ui/windows/printout";
import { SaveLoad } from "./ui/saveLoad";
export class UI {
events: UiEventHandler = new UiEventHandler();
private components: Array<UiComponent>;
ui_events: UiEventHandler = new UiEventHandler();
cpu_signaler: UiCpuSignalHandler = new UiCpuSignalHandler();
private components: Array<UiComponent> = [];
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(frequencyIndicator, $("cycles"));
this.register_component(InstructionExplainer, $("instruction_explainer"));
@ -31,15 +25,16 @@ export class UI {
this.register_component(Printout, $("printout"));
this.register_component(EditButton, $("edit_button"));
this.register_component(pausePlay, $("controls_buttons"));
const pp_button = $("pause_play_button");
this.register_component(SaveLoad, $("save_load_buttons"));
}
private register_component(ctor: UiComponentConstructor, e: HTMLElement): void {
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);
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);
}
@ -48,14 +43,10 @@ export class UI {
this.reset();
});
for (const c of this.components) {
if (c.init_cpu_events) c.init_cpu_events(cpu_events);
}
for (const c of this.components) if (c.init_cpu_events) c.init_cpu_events(cpu_events);
}
reset(): void {
for (const c of this.components) {
c.reset();
}
for (const c of this.components) if (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 {
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) {
constructor(list: Array<HTMLElement>, width: number, callback: (n: number, value: u8) => void) {
this.list = list;
this.width = width;
this.height = height;
this.edit_callback = callback;
this.current_cell_info = {};
for (const [i, cell] of this.list.entries()) {
cell.setAttribute("spellcheck", "false");
cell.addEventListener("keydown", (e) => {
this.keydown(e, i);
});
cell.addEventListener("keydown", (e) => this.keydown(e, i));
cell.addEventListener("input", (e) => {
const target = e.target as HTMLElement;
if (target === null) return;

View file

@ -8,9 +8,9 @@ export class frequencyIndicator implements UiComponent {
private last_value: number = 0;
private last_time: number = 0;
events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) {
constructor(element: HTMLElement, events: UiEventHandler) {
this.element = element;
this.events = e;
this.events = events;
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 { u8 } from "../num.js";
import { UiComponent } from "./uiComponent";
import { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex";
type MemoryCell = {
el: HTMLDivElement;
};
export class MemoryView extends CelledViewer implements UiComponent {
program_counter: u8 = 0;
last_accessed_cell: u8 | null = null;
events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) {
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(16, 16, element);
this.program_counter = 0;
this.events = e;
this.events = events;
this.cpu_signals = cpu_signals;
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 });
const editor = new EditorContext(list, this.width, (i, value) => {
this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address: i as u8, bank: 0, value });
});
this.events.listen(UiEvent.EditOn, () => {
editor.enable();

View file

@ -1,5 +1,5 @@
import { el } from "../etc";
import { UiEventHandler, UiEvent } from "../events";
import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../events";
import { UiComponent } from "./uiComponent";
const MAX_SLIDER = 1000;
@ -12,9 +12,12 @@ export class pausePlay implements UiComponent {
events: UiEventHandler;
on: boolean = false;
cycle_delay: number;
constructor(element: HTMLElement, events: UiEventHandler) {
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
this.element = element;
this.events = events;
this.cpu_signals = cpu_signals;
this.start_button = el("button", "pause_play_button");
this.step_button = el("button", "step_button");
this.range = el("input", "speed_range");
@ -72,7 +75,7 @@ export class pausePlay implements UiComponent {
if (this.on === false) {
return;
}
this.events.dispatch(UiEvent.RequestCpuCycle, 1);
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuCycle, 1);
setTimeout(loop, this.cycle_delay);
};
loop();
@ -81,7 +84,7 @@ export class pausePlay implements UiComponent {
if (this.on) {
this.stop();
} 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 { CelledViewer } from "./celledViewer";
import { EditorContext } from "./editableHex";
@ -6,13 +6,15 @@ import { UiComponent } from "./uiComponent";
export class RegisterView extends CelledViewer implements UiComponent {
events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) {
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(8, 1, element);
this.events = e;
this.events = events;
this.cpu_signals = cpu_signals;
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 });
const editor = new EditorContext(list, this.width, (i, value) => {
this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: i as u3, value });
});
this.events.listen(UiEvent.EditOn, () => {
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
* @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 {
element: HTMLElement;
/** Allows listening and emitting UiEvents*/
events: UiEventHandler;
/** Creating signals for the cpu to process */
cpu_signals?: UiCpuSignalHandler;
/** Completely reset the state of the component */
reset: () => void;
reset?: () => void;
soft_reset?: () => void;
/** Allows listening CPUEvents*/
init_cpu_events?: (c: CpuEventHandler) => void;
}
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";
export abstract class WindowBox {
element: HTMLElement;
readonly title: string;
title_bar: HTMLElement;
readonly title: string;
private resize: HTMLElement;
private collapse_button: HTMLButtonElement;
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 { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events";
import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events";
import { Instruction, ParamType, ParameterType } from "../../instructionSet";
import { u8 } from "../../num";
import { WindowBox } from "../windowBox";
@ -7,9 +7,11 @@ import { UiComponent } from "../uiComponent";
export class InstructionExplainer extends WindowBox implements UiComponent {
events: UiEventHandler;
constructor(element: HTMLElement, e: UiEventHandler) {
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(element, "Instruction Explainer");
this.events = e;
this.cpu_signals = cpu_signals;
this.events = events;
}
add_instruction(instr: Instruction, pos: u8, byte: u8): void {
this.reset();

View file

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

View file

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