From 49937af24e57bfb86f6a71184e05ceb150cf02d5 Mon Sep 17 00:00:00 2001 From: Alexander Bass Date: Sat, 9 Mar 2024 13:40:49 -0500 Subject: [PATCH] split out UI -> CPU signals --- src/computer.ts | 31 ++++---- src/eventHandler.ts | 31 +++----- src/events.ts | 55 +++++++++----- src/include/explainer.png | Bin 4441 -> 0 bytes src/include/index.html | 29 +++++++- src/include/texout.png | Bin 4487 -> 0 bytes src/include/tv.png | Bin 4528 -> 0 bytes src/index.ts | 41 +--------- src/num.ts | 24 ++++-- src/style/buttons.scss | 11 --- src/style/memory_registers.scss | 7 -- src/style/style.scss | 88 ++++++++++++++++++++-- src/style/windows.scss | 27 +++---- src/ui.ts | 33 +++----- src/ui/{edit_button.ts => editButton.ts} | 0 src/ui/editableHex.ts | 8 +- src/ui/frequencyIndicator.ts | 4 +- src/ui/memoryView.ts | 16 ++-- src/ui/pausePlay.ts | 11 ++- src/ui/registerView.ts | 12 +-- src/ui/saveLoad.ts | 91 +++++++++++++++++++++++ src/ui/uiComponent.ts | 15 +++- src/ui/windowBox.ts | 2 +- src/ui/windows/bankIndicator.ts | 22 ------ src/ui/windows/instructionExplainer.ts | 8 +- src/ui/windows/printout.ts | 6 +- src/ui/windows/screen.ts | 1 + 27 files changed, 349 insertions(+), 224 deletions(-) delete mode 100644 src/include/explainer.png delete mode 100644 src/include/texout.png delete mode 100644 src/include/tv.png rename src/ui/{edit_button.ts => editButton.ts} (100%) create mode 100644 src/ui/saveLoad.ts delete mode 100644 src/ui/windows/bankIndicator.ts diff --git a/src/computer.ts b/src/computer.ts index b446b0b..79b6621 100644 --- a/src/computer.ts +++ b/src/computer.ts @@ -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): 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 { diff --git a/src/eventHandler.ts b/src/eventHandler.ts index 4db7dbf..4ca7c98 100644 --- a/src/eventHandler.ts +++ b/src/eventHandler.ts @@ -14,29 +14,13 @@ export class Event { export class EventHandler { events: Array> = []; - 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(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 { }); } 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); } diff --git a/src/events.ts b/src/events.ts index 68e184b..2bd52eb 100644 --- a/src/events.ts +++ b/src/events.ts @@ -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 { @@ -60,30 +58,51 @@ interface CpuEventHandlerConstructor { export const CpuEventHandler = EventHandler 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 { + listen(type: E, listener: () => void): void; + dispatch(type: E): void; + listen(type: E, listener: (ev: UiCpuSignalMap[E]) => void): void; + dispatch(type: E, data: UiCpuSignalMap[E]): void; +} + +interface UICpuSignalHandlerConstructor { + new (): UiCpuSignalHandler; +} + +export const UiCpuSignalHandler = EventHandler 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; diff --git a/src/include/explainer.png b/src/include/explainer.png deleted file mode 100644 index 7ed74b866f60b6c72ba4b40c38221606075f5c7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4441 zcmeHKdsGuw8XuqrnhHYIDk^8E1MY%2naN~Al951>SE2?Zusp1SlgtE0@*){X$cjZo zx2=^!&)QlaEiDM{aS^vg+U_a^k*a&N)ur9jqxHekuClAt>f*M#TKCTIKHGElaL)EW zl9SBb@BY5u{l4G*?mhRuWXwnpmM)M&5EQJ}rDTF9Ec}8b;NR`@4S~m?FFTLVq&=|H z<*;y87UnCQEX;a23j}#D?ObzlXAd6q$TRnOf5|3ety}-)iCu|UTt-~v&ZD~PA@K#> zKRnQOpO0CmDPE#{{?jMcWPK$$*jv|hQ+KIs-oKviPfu7kBW=Y7wkGbWWp}@AJbis- z|E>D1&9fIC=jW~c=fk__u!OpsH{f)*)Y|+>;>Tg3gZp(>SC3_UMmyC*$)mE3BI8p!cp{+aG zT0>f^;qL?bZ}lA*Ko)P_J!fr?_jKv3+m*M^Uhz~JF~ukUcrjqFec#`i<>#*F^jwIo z9%#_Kn6M}RR7+G--?ERs3G4g!`d5nXfAL7;`^)i={~AT9dc*{dap$wJ;ldvU=A1^0S!2Grktr z)&7>(e%Dg5`iE|3L195ZbvP{Q-Ktbw=sRz}9K4H;OgonG@;36#Uu=4Kvb7M|y}UIT zcb#kW4}R}v;Nz{=I@+bbnO%^n_TxWNe|kDP^wNw4%G37~%J-joGIW!qb?EiZ%s@+m z{z&`Lfv^jm!Ryw2K(ezwyzICAt(g~ht&h>IjGj~Vo#H|BBHa1ZP>Zj!|DiC!mdE@64t#1?UOEpq!OZShQK0(FVP}-1|yv8aIr8cBV{<6>gCD_BtZ(tyBG_V znUXd_0a{w5nCG1ohIu?5nMWaWxQZ~jMx()S0wV|%AW(OOou|F1-5n`V40EKgZnKMX z@|?pC3!JpcQO0W#1oXqB@!6aP!x+8YJ)#281M|{OOfJJQn+==j;pS7z0m(=}KkDJm z1`8U?WZjN3mzhm1XYG9CL<+_{=I<rmVk*J0y6ecsFz!^e0fl6<8^R(T}3RHkx#sMCdG~ujV4k&0s zg_58Jywwb9A}u(pF`49ynwUVb+QosYq^%R95~vtJMXOCFwT9K8auwhs&9n(sGYSUP zsN+;}HLg(6ghixc%v74gWuw7#ayGh%#hmscu|p7?N;2xT2qDAAEk-NNTY!TWS;g7Q zyyG3&oQ=)mX+cf73fHJ`@Kq`a4S}o1K{>3;4JuLKl;bi5DfS4{LV;v}u(VL8fIzf^ zT&QFhOY;s_w!>l7B0@;8;5pW90LzJ?c{+vWSwM;tB!w#|JT4oTQwkNOBvD*R;S=x< zhO<=sFSM|D;P~Mw*KuwzeuZcnUQt2PbfY30PF1jFK@plI`O3U0cbWyClE*Kn7) zn6?+OVD}g)*HJt7Bc-5Zl>})51t5=;D^OBR6DUop%qVMOOmf!Yly6P+WnI3#J?q3>!%p6ZVYJjN`HY(j;C4OxR?A z->?mAUSKc8#x}zd&4g{|C;W`e#ZNc_pq?INO8QRAH7(ba6qpity1J(2nvw!j0#8@h z|4lCG_;rf4gHwD1-BAxIV}{QSI4iRHi; z$m)TU2K%Fa7E zn41*R1qTImKkmG|z8v3?mQ)wU?9ZN#d-om|6sj)71VHbYVgtLb z_6ErSIW>e4SIHSAQ93Y zbzpA(!OogzlsaDVUR~Vr@QOK&`HKtxR-40gTnTQh{`H}**j&xC?R>beEfCU140&Ey temJ}9^_h*(+%iN{{bOfL=XS~ diff --git a/src/include/index.html b/src/include/index.html index 58ad3e9..cd39acf 100644 --- a/src/include/index.html +++ b/src/include/index.html @@ -7,25 +7,46 @@ Virtual 8-Bit Computer +
VIRTUAL 8-BIT COMPUTER
-
←REGISTERS
+
↯REGISTERS
MEMORY↯
- - - +
+
+
+ + + + +
+ +
+
diff --git a/src/include/texout.png b/src/include/texout.png deleted file mode 100644 index 7b8aab32e0fc3fd2ede927fe2c7a48726add78db..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4487 zcmeHKdr(tX8V{&MSwT=iS8;nAa2BdJ_a?b{T?wp^Xd(uYXRHsrx%Vbq$jjtHLPjw~ zWzbbe+bYN^wt(7gDc1Vv!!ACcsAFvzcebon6xk}awX1X)A6vmaHz1GM&g?L={f|3y zU+4RMzw>>+^PMy2Y)wmD91=V$n8)LV=(Or|@P@d1Vi0(`yxt!0`q7(dVABaV&Dud9LqR{>_eNX!5fo+vWvWNQd&=eHk@Z7d^JK8*ov{2Z*>jc(d&8@+m^k6i(t>m64u1abravBU z2yG~Zo{a0f-%)o@Ft?;)`m4>J3+tvl*zn+TyW5wBNUnTeKCa64r|+nwFYabHx6UiQ zSEDRX*tepkKITxzg2sP_cl^A1yZLEbuhRRs&p!A0v{B|VX4=td%k0;Kx;Au9oOkJ! zZe{n!x*0Jg-+tQJJ-e&sa^P3nEBEz8t=nvvf9lxPmU=3`Iiq=b)?edtFM8{pH#_V1 zyjNemtna*IP41db{QdBlV?K>G?CrOOt{B7kjFe0d=@?D~XR(hE-$iuSim3R@r4(6jeUI^UF_J9_fO zz3|pEA*)t>gwazzz7bGXJ^9+3tK+mOvD1BzCI3Dgg*txIQ}5kSQ2mddfQxBI)8%K+ zy^Lnp-IpG8yFYsTWk!Ze*qAQqxSEol~lH30{eU@2oO z^b8p?87rN^5}cYMIjWSSGE5|rC`EEPHU!G1oi0#`9H$5sO8PC_u;3sWAS}VvDIoBB zfL!ooCrz++XQth5Q3<$^AkK5ptOwJHB3MFAurwe=#TbrCa8#Cwig2+Em!dE##nEAS zJH?oa{}-B@JWzc9kZTzi=)c(C)IXy#=ym;n{l6B*Kb0WJKPhm6>`%c(6w#DFPQcY~ zBJ&8FnFg!JK)JqXXMUj+XdoY`1Su>90}o>|xe-=~6>=Dpl5!&|5y=%I=}2~$-Nd>H zC%w=NbOc&~^7Ly3#rg*-ZY0{BM{}b91cOltJS3QCKrmz=VT4;VUSu4P{Ff&2e!#Fr z2Ke>2fyE1~g~;GyIG`D~?EH$?z*zi>Jpk&lPDZ8gm|SCWjY@%0fyb(AOs-KWFe>m^ zb^YJu3LZL6(KfIPa)ZOtcu9#99JGRrDT~$qBNy*!^68D>Ym!5|!o}kWqq#f4b11P0 zH1b)UUceIX-iqe8ug%^6 zHGgYmj^mS;7AV3_`8F5-Twl1arfkW@hsE!;mG0aU^+98+pz>~Wt-=@n%=~!o-V+x- z2qYe*rtm*}L?y*MzVr3XCHz-hC9Hns+AGC;RatJ!CDvH2>*&=*w|j#4*^%O$_TE>c q3|j*mJ_~rMsU~)7N5qWs5I?Q~55%M4zNx%n) zg}TwXgu;cU;@!#Ugkm~-ZJ zzk7e*y}x_Ed%o{?#)R4aev|z~B9Xsdr%8fuM7Vvt;nPu2&<$VT7bK_gNwgEO*{von z2OxaD4Ise9nM5MjmAB@lTTUQjd)p_fifcqM0qMIgP5Jyrt?$;o2a*p2{{xIQ;kR9d zwHI4_4{Xc{r&gRBwROiFwUJwTZ}aKT?!6l}t?+&p(;ibERR$9x%2WHO18Uk*CWmM;%ZU) zOO5l~v|`bvj>)&qU%o2q@p_t{(j%IjoL5y_+qF_8@+jfdYNKAQeik{5cjv12RJ!_E zzbS3qzfAjaz>9&ojmEOh3iRzGe%g=5Eh|ZRX|DK5;C3=3T>N%%c3s`>6CbaA^G{U) zRmI3pUTvM%_jaJ6tG7&C*x)*q`@;QI_nW?T78xb7vv<~cZMW>W&BmR%mC|rwMsY`l zVqMI(^plk#`>wzG<@bTtAHTLC^HIwaMZwpfpC~HQP0cLjCLEfOXuaayzN*b<#_8!@ zimp%flR{SC{H(2ON_$0<=lKm=w{=g=eLXGW=qDFXRr2S8->&SfuUdj`iL3I*?PvCQ z`d8l_{d8^X7f1ZwpRhPd?!kX4KX)o@+?8KVrcOPI$=i9-cic+vs_xCTNuy0M`a?$! zcLZLj^Kx6!nXgh0CCTZgP zA>f}1&Ek2RQX+9WonogbJ)fP6??v`r!v;}Wx3GT6ewYx5vTze7K3;Yfxn zT9O1D*5!5vX!C%D4;~D`G6VLu<@OwRI4mOpIlv569dK6ZkSS;D4aNZrfr3oVY;#*d zvWIB$oas4PLwplP+~Ev%1ey=v4$*!VyIUD*84OB|m02#hr`M=ZVSXiRWjI#p{v=pA zK@uhgBUnISBuO)vf+eLG%aU>m$Ylftz#u5S#lh1S1_)3PT+Bfn1)+!}l9#jz)BP_rMOr|y3K{OC}A*=SX!u42;i2( zSd?lzpn0o3*=o&Ep@K_@U^y^sfXj)cd0IpB0D|HKsl;VUJTe)VDhat#8inDMvflv# z&R}IZQ~v+b78VZ@4fSElb({mvpYIm+t*H4Rx9_j-H-~dCB?NIV3MI|-h2Ws`0PFS> zV)YF%S+pe+!0ypsuFvG$&y)g1W#A+kNnnJ6g88Fh709Jga!g9oj7&~T*$fjg6y0Gp z@lM(fVlyF+kSkc8Zmy6pccQ|FYCE%lkOfE>hRd+$gvt8}lk^8H5qie6h@&O{#YwbV zVNjESc6~C~ykIYs3^c=j&V;t}3ts)X_yuQx&?A!!i{BBtM(7$A1H)1tsjd;ahQ+|J zlt-%T|3;VJ^Wzk-z+I3N9+t$DbZg;3%R6JvY>oTKC3>VjUI@Q@ZMt-aNF)vxZV%VK znR!q+n%5h&qdUC3y{C@X&WXPXMM1najaOUE!meE8-YfzZadLbXB5W2@*91(0XKr7; zCN?>({%mUV_mUN$ByxTGw#)TjPr?hYUSzizZOHC}q8 z3H@ew#I1MUn11{H^q-D?aiS`I(Y-M&S+k%;#^h<20kg9Sq<! diff --git a/src/index.ts b/src/index.ts index 65f9d24..7eef404 100644 --- a/src/index.ts +++ b/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; - 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", () => { diff --git a/src/num.ts b/src/num.ts index fa348ca..96adb27 100644 --- a/src/num.ts +++ b/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) { diff --git a/src/style/buttons.scss b/src/style/buttons.scss index 74b34d4..7d922ae 100644 --- a/src/style/buttons.scss +++ b/src/style/buttons.scss @@ -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; diff --git a/src/style/memory_registers.scss b/src/style/memory_registers.scss index 8052540..8472844 100644 --- a/src/style/memory_registers.scss +++ b/src/style/memory_registers.scss @@ -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; diff --git a/src/style/style.scss b/src/style/style.scss index 64cd08d..3ff17a1 100644 --- a/src/style/style.scss +++ b/src/style/style.scss @@ -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%); + } +} diff --git a/src/style/windows.scss b/src/style/windows.scss index dc4b24d..a28c9fc 100644 --- a/src/style/windows.scss +++ b/src/style/windows.scss @@ -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; diff --git a/src/ui.ts b/src/ui.ts index d29a03b..03659a9 100644 --- a/src/ui.ts +++ b/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; + ui_events: UiEventHandler = new UiEventHandler(); + cpu_signaler: UiCpuSignalHandler = new UiCpuSignalHandler(); + private components: Array = []; 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(); } } diff --git a/src/ui/edit_button.ts b/src/ui/editButton.ts similarity index 100% rename from src/ui/edit_button.ts rename to src/ui/editButton.ts diff --git a/src/ui/editableHex.ts b/src/ui/editableHex.ts index 32f7fb1..8e8b894 100644 --- a/src/ui/editableHex.ts +++ b/src/ui/editableHex.ts @@ -8,22 +8,18 @@ const HEX_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", " export class EditorContext { private list: Array; 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, width: number, height: number, callback: (n: number, value: u8) => void) { + constructor(list: Array, 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; diff --git a/src/ui/frequencyIndicator.ts b/src/ui/frequencyIndicator.ts index a0f7c53..b1b4bf8 100644 --- a/src/ui/frequencyIndicator.ts +++ b/src/ui/frequencyIndicator.ts @@ -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(); } diff --git a/src/ui/memoryView.ts b/src/ui/memoryView.ts index 9db61dd..6609709 100644 --- a/src/ui/memoryView.ts +++ b/src/ui/memoryView.ts @@ -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(); diff --git a/src/ui/pausePlay.ts b/src/ui/pausePlay.ts index 66312f8..e8ce91e 100644 --- a/src/ui/pausePlay.ts +++ b/src/ui/pausePlay.ts @@ -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); } } diff --git a/src/ui/registerView.ts b/src/ui/registerView.ts index 024079b..b80251e 100644 --- a/src/ui/registerView.ts +++ b/src/ui/registerView.ts @@ -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(); diff --git a/src/ui/saveLoad.ts b/src/ui/saveLoad.ts new file mode 100644 index 0000000..d4baeca --- /dev/null +++ b/src/ui/saveLoad.ts @@ -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; + 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(); + }); + } +} diff --git a/src/ui/uiComponent.ts b/src/ui/uiComponent.ts index d970ea2..add22e7 100644 --- a/src/ui/uiComponent.ts +++ b/src/ui/uiComponent.ts @@ -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; } diff --git a/src/ui/windowBox.ts b/src/ui/windowBox.ts index 32415d6..5e0ce6e 100644 --- a/src/ui/windowBox.ts +++ b/src/ui/windowBox.ts @@ -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; diff --git a/src/ui/windows/bankIndicator.ts b/src/ui/windows/bankIndicator.ts deleted file mode 100644 index 3109465..0000000 --- a/src/ui/windows/bankIndicator.ts +++ /dev/null @@ -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); - }); - } -} diff --git a/src/ui/windows/instructionExplainer.ts b/src/ui/windows/instructionExplainer.ts index a83d334..bd342aa 100644 --- a/src/ui/windows/instructionExplainer.ts +++ b/src/ui/windows/instructionExplainer.ts @@ -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(); diff --git a/src/ui/windows/printout.ts b/src/ui/windows/printout.ts index f24e0bc..2975cd8 100644 --- a/src/ui/windows/printout.ts +++ b/src/ui/windows/printout.ts @@ -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); diff --git a/src/ui/windows/screen.ts b/src/ui/windows/screen.ts index 058f7dc..f63da2e 100644 --- a/src/ui/windows/screen.ts +++ b/src/ui/windows/screen.ts @@ -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 {