split out UI -> CPU signals
This commit is contained in:
parent
1f6a95c253
commit
49937af24e
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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 |
|
@ -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 |
41
src/index.ts
41
src/index.ts
|
@ -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", () => {
|
||||
|
|
24
src/num.ts
24
src/num.ts
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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%);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
33
src/ui.ts
33
src/ui.ts
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
91
src/ui/saveLoad.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in a new issue