diff --git a/src/computer.ts b/src/computer.ts index 17292bc..a65dc95 100644 --- a/src/computer.ts +++ b/src/computer.ts @@ -163,17 +163,6 @@ export default class Computer { this.events.dispatch(CpuEvent.SetVramBank, { bank }); } - reset(): void { - this.events.dispatch(CpuEvent.Reset); - this.banks = initBanks(); - this.registers = new Uint8Array(8); - this.call_stack = []; - this.current_instr = null; - this.program_counter = 0; - this.carry_flag = false; - this.vram_bank = 3; - } - initEvents(ui: UiCpuSignalHandler): void { ui.listen(UiCpuSignal.RequestCpuCycle, (cycle_count) => { for (let i = 0; i < cycle_count; i++) this.cycle(); @@ -185,6 +174,31 @@ export default class Computer { ui.listen(UiCpuSignal.RequestProgramCounterChange, ({ address }) => { this.setProgramCounter(address); }); + ui.listen(UiCpuSignal.RequestCpuSoftReset, () => this.softReset()); + } + + softReset(): void { + this.events.dispatch(CpuEvent.SoftReset); + for (let i = 0; i < 8; i++) this.setRegister(i as u3, 0); + while (this.popCallStack() !== null) 0; + this.setVramBank(DEFAULT_VRAM_BANK); + this.setCarry(false); + this.current_instr = null; + this.setProgramCounter(0); + this.setBank(0); + } + reset(): void { + this.events.dispatch(CpuEvent.Reset); + // Hard reset + this.banks = initBanks(); + // Soft reset + for (let i = 0; i < 8; i++) this.setRegister(i as u3, 0); + while (this.popCallStack() !== null) 0; + this.setVramBank(DEFAULT_VRAM_BANK); + this.setCarry(false); + this.current_instr = null; + this.setProgramCounter(0); + this.setBank(0); } loadMemory(program: Array): void { diff --git a/src/events.ts b/src/events.ts index 1fec75b..e57fe90 100644 --- a/src/events.ts +++ b/src/events.ts @@ -21,6 +21,7 @@ export enum CpuEvent { Cycle, Print, Reset, + SoftReset, Halt, MemoryAccessed, SwitchBank, @@ -28,7 +29,7 @@ export enum CpuEvent { SetVramBank, } -type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle; +type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.SoftReset | CpuEvent.Cycle; interface CpuEventMap { [CpuEvent.MemoryChanged]: { address: u8; bank: u2; value: u8 }; @@ -67,11 +68,12 @@ export enum UiCpuSignal { RequestMemoryChange, RequestRegisterChange, RequestCpuReset, + RequestCpuSoftReset, RequestMemoryDump, RequestProgramCounterChange, } -type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset; +type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset | UiCpuSignal.RequestCpuSoftReset; interface UiCpuSignalMap { [UiCpuSignal.RequestCpuCycle]: number; diff --git a/src/index.ts b/src/index.ts index 670dd37..4e1b073 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,7 @@ function main(): void { window.ui = ui; // Todo, move to ui component + // or move to documentation $("ISA").textContent = generateIsa(ISA); let fire = false; diff --git a/src/style/hover_text_box.scss b/src/style/hover_text_box.scss new file mode 100644 index 0000000..df7c6da --- /dev/null +++ b/src/style/hover_text_box.scss @@ -0,0 +1,5 @@ +.hover_text_box { + border: 5px solid yellow; + background-color: black; + user-select: none; +} diff --git a/src/style/style.scss b/src/style/style.scss index 9172226..746a51e 100644 --- a/src/style/style.scss +++ b/src/style/style.scss @@ -1,4 +1,5 @@ @use "memory_registers"; +@use "hover_text_box"; @use "windows"; @use "buttons"; @use "vars"; diff --git a/src/ui.ts b/src/ui.ts index 0ea7305..12360b2 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -47,9 +47,8 @@ export default class UI { } initEvents(cpu_events: CpuEventHandler): void { - cpu_events.listen(CpuEvent.Reset, () => { - this.reset(); - }); + cpu_events.listen(CpuEvent.Reset, () => this.reset()); + cpu_events.listen(CpuEvent.SoftReset, () => this.softReset()); for (const c of this.components) if (c.initCpuEvents) c.initCpuEvents(cpu_events); } @@ -57,4 +56,8 @@ export default class UI { reset(): void { for (const c of this.components) if (c.reset) c.reset(); } + + softReset(): void { + for (const c of this.components) if (c.softReset) c.softReset(); + } } diff --git a/src/ui/components/bankViewSelector.ts b/src/ui/components/bankViewSelector.ts index 7587e27..c4368da 100644 --- a/src/ui/components/bankViewSelector.ts +++ b/src/ui/components/bankViewSelector.ts @@ -6,7 +6,7 @@ import UiComponent from "../uiComponent"; export default class BankSelector implements UiComponent { container: HTMLElement; events: UiEventHandler; - private bank_buttons: Array; + bank_buttons: Array; constructor(element: HTMLElement, events: UiEventHandler) { this.container = element; this.events = events; @@ -28,8 +28,13 @@ export default class BankSelector implements UiComponent { this.container.appendChild(bank_boxes); } + reset(): void { for (const b of this.bank_buttons) b.classList.remove("selected"); this.bank_buttons[0].classList.add("selected"); } + + softReset(): void { + this.reset(); + } } diff --git a/src/ui/components/editButton.ts b/src/ui/components/editButton.ts index 91357c9..ab22c90 100644 --- a/src/ui/components/editButton.ts +++ b/src/ui/components/editButton.ts @@ -12,18 +12,18 @@ export default class EditButton implements UiComponent { this.cpu_signals = cpu_signals; const image = el("img").at("src", "pencil.png").st("width", "20px").st("height", "20px").fin(); this.container.classList.add("editor_toggle"); - this.container.addEventListener("click", () => this.edit_toggle()); + this.container.addEventListener("click", () => this.editToggle()); this.container.appendChild(image); } - reset(): void { + disable(): void { const is_on = this.container.classList.contains("on"); if (is_on) { - this.edit_toggle(); + this.editToggle(); } } - edit_toggle(): void { + editToggle(): void { const is_on = this.container.classList.contains("on"); if (is_on) { this.container.classList.remove("on"); @@ -35,7 +35,14 @@ export default class EditButton implements UiComponent { $("root").classList.add("editor"); this.container.classList.add("on"); this.container.classList.remove("off"); - this.cpu_signals.dispatch(UiCpuSignal.RequestProgramCounterChange, { address: 0 }); } } + + reset(): void { + this.disable(); + } + + softReset(): void { + this.disable(); + } } diff --git a/src/ui/components/frequencyIndicator.ts b/src/ui/components/frequencyIndicator.ts index 247121c..2742378 100644 --- a/src/ui/components/frequencyIndicator.ts +++ b/src/ui/components/frequencyIndicator.ts @@ -3,10 +3,10 @@ import UiComponent from "../uiComponent"; export default class frequencyIndicator implements UiComponent { container: HTMLElement; - private running: number | null = null; - private count: number = 0; - private last_value: number = 0; - private last_time: number = 0; + running: number | null = null; + count: number = 0; + last_value: number = 0; + last_time: number = 0; events: UiEventHandler; constructor(element: HTMLElement, events: UiEventHandler) { this.container = element; @@ -19,16 +19,16 @@ export default class frequencyIndicator implements UiComponent { if (this.running !== null) { throw new Error("Tried starting frequencyIndicator twice!"); } - setInterval(this.update_indicator.bind(this), 1000); + window.setInterval(this.updateIndicator.bind(this), 1000); } stop(): void { if (this.running === null) return; - clearInterval(this.running); + window.clearInterval(this.running); this.running = null; } - update_indicator(): void { + updateIndicator(): void { const new_time = performance.now(); const dt = (new_time - this.last_time) / 1000 || 1; const value = Math.round(this.count / dt); @@ -44,15 +44,21 @@ export default class frequencyIndicator implements UiComponent { this.count = 0; } - clock_cycle(): void { + clockCycle(): void { this.count += 1; } + reset(): void { this.stop(); this.count = 0; this.last_value = 0; this.start(); } + + softReset(): void { + this.reset(); + } + initCpuEvents(c: CpuEventHandler): void { c.listen(CpuEvent.Cycle, () => { this.count += 1; diff --git a/src/ui/components/memoryView.ts b/src/ui/components/memoryView.ts index a62cf8b..3008725 100644 --- a/src/ui/components/memoryView.ts +++ b/src/ui/components/memoryView.ts @@ -53,6 +53,7 @@ export default class MemoryView implements UiComponent { }); } this.events.listen(UiEvent.ChangeViewBank, ({ bank }) => this.setBank(bank)); + this.setProgramCounter(0); } get program(): CelledViewer { @@ -76,6 +77,12 @@ export default class MemoryView implements UiComponent { this.setProgramCounter(0); } + softReset(): void { + for (const viewer of this.banks) viewer.clearAllClasses(); + this.last_accessed_cell = null; + this.setProgramCounter(0); + } + initCpuEvents(c: CpuEventHandler): void { c.listen(CpuEvent.MemoryAccessed, ({ address, bank, value }) => { if (this.last_accessed_cell?.address !== address || this.last_accessed_cell?.bank !== bank) { diff --git a/src/ui/components/pausePlay.ts b/src/ui/components/pausePlay.ts index b4560fc..ebc754d 100644 --- a/src/ui/components/pausePlay.ts +++ b/src/ui/components/pausePlay.ts @@ -1,6 +1,7 @@ import { el } from "../../etc"; import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events"; import UiComponent from "../uiComponent"; +import HoverTextBox from "../hoverTextBox"; const MAX_SLIDER = 1000; @@ -41,18 +42,22 @@ export default class pausePlay implements UiComponent { this.events.listen(UiEvent.EditOn, () => this.disable()); this.events.listen(UiEvent.EditOff, () => this.enable()); + const tb = new HoverTextBox(this.start_button, el("span").tx("hover test").st("color", "yellow").fin(), "left", 10); + tb.show(); } disable(): void { this.stop(); this.start_button.setAttribute("disabled", "true"); this.step_button.setAttribute("disabled", "true"); + this.range.setAttribute("disabled", "true"); } enable(): void { this.start_button.removeAttribute("disabled"); this.step_button.removeAttribute("disabled"); + this.range.removeAttribute("disabled"); } @@ -76,6 +81,7 @@ export default class pausePlay implements UiComponent { }; loop(); } + private step(): void { if (this.on) { this.stop(); @@ -98,4 +104,8 @@ export default class pausePlay implements UiComponent { this.stop(); this.enable(); } + + softReset(): void { + this.reset(); + } } diff --git a/src/ui/components/registerView.ts b/src/ui/components/registerView.ts index 9a9dbbc..22a4c70 100644 --- a/src/ui/components/registerView.ts +++ b/src/ui/components/registerView.ts @@ -7,10 +7,7 @@ export default class RegisterView extends CelledViewer implements UiComponent { events: UiEventHandler; cpu_signals: UiCpuSignalHandler; constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { - super(8, 1, element, (address: u8, value: u8) => { - if (!isU3(address)) throw new Error("unreachable"); - this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: address as u3, value }); - }); + super(8, 1, element, (a, v) => this.onEdit(a, v)); this.events = events; this.cpu_signals = cpu_signals; @@ -24,8 +21,16 @@ export default class RegisterView extends CelledViewer implements UiComponent { }); } + onEdit(address: u8, value: u8): void { + if (!isU3(address)) throw new Error("unreachable"); + this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: address as u3, value }); + } + initCpuEvents(c: CpuEventHandler): void { c.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => this.setCellValue(register_no, value)); - c.listen(CpuEvent.Reset, () => this.reset()); + } + + softReset(): void { + this.clearAllClasses(); } } diff --git a/src/ui/components/resetButtons.ts b/src/ui/components/resetButtons.ts index 07fbc74..0f459ad 100644 --- a/src/ui/components/resetButtons.ts +++ b/src/ui/components/resetButtons.ts @@ -1,5 +1,5 @@ import { el } from "../../etc"; -import { UiEventHandler, UiCpuSignalHandler, UiEvent, UiCpuSignal } from "../../events"; +import { UiEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events"; import UiComponent from "../uiComponent"; export default class ResetButtons implements UiComponent { @@ -10,17 +10,22 @@ export default class ResetButtons implements UiComponent { this.container = element; this.events = events; this.cpu_signals = cpu_signals; - const reset_button = el("button").cl("nostyle").tx("R").fin(); - const trash_button = el("button").cl("nostyle").tx("T").fin(); + const reset_button = el("button").cl("nostyle").ti("Reset State").tx("⟳").fin(); + const trash_button = el("button").cl("nostyle").ti("Delete Code").tx("🗑").fin(); reset_button.addEventListener("click", () => this.resetClicked()); trash_button.addEventListener("click", () => this.trashClicked()); this.container.append(reset_button, trash_button); } - resetClicked(): void {} + resetClicked(): void { + this.cpu_signals.dispatch(UiCpuSignal.RequestCpuSoftReset); + } trashClicked(): void { - this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset); + const a = confirm("Clear all code? Irreversible"); + if (a) { + this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset); + } } } diff --git a/src/ui/components/saveLoad.ts b/src/ui/components/saveLoad.ts index d967936..aa9f7d2 100644 --- a/src/ui/components/saveLoad.ts +++ b/src/ui/components/saveLoad.ts @@ -1,6 +1,6 @@ import { el } from "../../etc"; import { UiEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events"; -import { u2, u8, m256, isU2 } from "../../num"; +import { u8, m256, isU2 } from "../../num"; import UiComponent from "../uiComponent"; export default class SaveLoad implements UiComponent { @@ -14,16 +14,16 @@ export default class SaveLoad implements UiComponent { this.events = events; this.cpu_signals = cpu_signals; - this.save_button = el("button").id("save_button").tx("Save").fin(); + this.save_button = el("button").id("save_button").tx("⬇").fin(); this.binary_upload = el("input") .id("binary_upload") .at("type", "file") .at("name", "binary_upload") .st("display", "none") .fin(); - const label = el("label").cl("button").at("for", "binary_upload").tx("Load Binary").fin(); + const label = el("label").cl("button").at("for", "binary_upload").tx("⬆").fin(); - this.container.append(this.binary_upload, label, this.save_button); + this.container.append(this.save_button, this.binary_upload, label); this.save_button.addEventListener("click", () => { this.download(); diff --git a/src/ui/hoverTextBox.ts b/src/ui/hoverTextBox.ts new file mode 100644 index 0000000..2309855 --- /dev/null +++ b/src/ui/hoverTextBox.ts @@ -0,0 +1,48 @@ +import { el } from "../etc"; + +type RelativePosition = "top" | "bottom" | "left" | "right"; + +export default class HoverTextBox { + parent: HTMLElement; + position: RelativePosition; + gap: number; + contents: HTMLElement; + shown: false | { container: HTMLElement; resize_event_fn: () => void }; + constructor(parent: HTMLElement, contents: HTMLElement, position: RelativePosition, gap: number) { + this.gap = gap; + this.position = position; + this.parent = parent; + this.contents = contents; + this.shown = false; + } + + show(): void { + if (this.shown) return; + const container = el("div").st("position", "absolute").cl("hover_text_box").fin(); + container.appendChild(this.contents); + const adjustBoxPosition = (cont: HTMLElement, parent: HTMLElement): void => { + const parent_x = parent.offsetLeft; + const parent_y = parent.offsetTop; + const style = window.getComputedStyle(cont); + + const new_cont_x = parent_x; + const new_cont_y = parent_y + this.gap + cont.offsetHeight + 5; + + cont.style.setProperty("top", `${new_cont_y}px`); + cont.style.setProperty("left", `${new_cont_x}px`); + }; + document.body.appendChild(container); + adjustBoxPosition(container, this.parent); + const adjust_fn = adjustBoxPosition.bind(undefined, container, this.parent); + window.addEventListener("resize", adjust_fn); + this.shown = { container: container, resize_event_fn: adjust_fn }; + } + + hide(): void { + if (!this.shown) return; + this.shown.container.innerHTML = ""; + this.shown.container.remove(); + window.removeEventListener("resize", this.shown.resize_event_fn); + this.shown = false; + } +} diff --git a/src/ui/windowBox.ts b/src/ui/windowBox.ts index 0f358b0..bdc4ead 100644 --- a/src/ui/windowBox.ts +++ b/src/ui/windowBox.ts @@ -12,7 +12,7 @@ export default abstract class WindowBox { title_bar: HTMLElement; readonly title: string; private collapse_button: HTMLButtonElement; - private collapsed = false; + private is_collapsed = false; private fit_content = false; private resize?: HTMLElement; private resize_func?: (e: MouseEvent) => void; @@ -44,7 +44,7 @@ export default abstract class WindowBox { } else { this.resize = el("div").id("resize").fin(); this.container.appendChild(this.resize); - this.resize_func = this.resize_move.bind(this); + this.resize_func = this.resizeMove.bind(this); this.resize.addEventListener("mousedown", (e) => { if (this.resize_func) window.addEventListener("mousemove", this.resize_func); }); @@ -58,7 +58,7 @@ export default abstract class WindowBox { this.removeResizeListeners(); if (this.resize) this.resize.style.visibility = "hidden"; this.setHeight(this.title_bar.offsetHeight - BORDER_STROKE); - this.collapsed = true; + this.is_collapsed = true; } correctHeightValue(height: number): number { @@ -74,7 +74,7 @@ export default abstract class WindowBox { } toggleCollapse(): void { - if (this.collapsed) { + if (this.is_collapsed) { this.uncollapse(); } else { this.collapse(); @@ -91,15 +91,15 @@ export default abstract class WindowBox { const new_height = this.correctHeightValue(this.title_bar.offsetHeight + 200); this.setHeight(new_height); - this.collapsed = false; + this.is_collapsed = false; } removeResizeListeners(): void { if (this.resize_func) window.removeEventListener("mousemove", this.resize_func); } - resize_move(e: MouseEvent): void { - if (this.collapsed) { + resizeMove(e: MouseEvent): void { + if (this.is_collapsed) { this.uncollapse(); this.removeResizeListeners(); return; @@ -111,4 +111,8 @@ export default abstract class WindowBox { } this.setHeight(e.clientY - this.container.offsetTop + window.scrollY); } + + isCollapsed(): boolean { + return this.is_collapsed; + } } diff --git a/src/ui/windows/bankVisualizer.ts b/src/ui/windows/bankVisualizer.ts index 5233ece..b4c378c 100644 --- a/src/ui/windows/bankVisualizer.ts +++ b/src/ui/windows/bankVisualizer.ts @@ -32,4 +32,8 @@ export default class BankVisualizer extends WindowBox implements UiComponent { this.cpu_banks[0].setAttribute("stroke", "yellow"); } + + softReset(): void { + this.reset(); + } } diff --git a/src/ui/windows/instructionExplainer.ts b/src/ui/windows/instructionExplainer.ts index 10826c7..013d623 100644 --- a/src/ui/windows/instructionExplainer.ts +++ b/src/ui/windows/instructionExplainer.ts @@ -67,4 +67,8 @@ export default class InstructionExplainer extends WindowBox implements UiCompone reset(): void { this.container.querySelectorAll("#expl_box").forEach((e) => e.remove()); } + + softReset(): void { + this.reset(); + } } diff --git a/src/ui/windows/printout.ts b/src/ui/windows/printout.ts index 7e432ab..125a0f3 100644 --- a/src/ui/windows/printout.ts +++ b/src/ui/windows/printout.ts @@ -24,4 +24,8 @@ export default class Printout extends WindowBox implements UiComponent { reset(): void { this.text_box.textContent = ""; } + + softReset(): void { + this.reset(); + } } diff --git a/src/ui/windows/screen.ts b/src/ui/windows/screen.ts index 467e88c..b8330d2 100644 --- a/src/ui/windows/screen.ts +++ b/src/ui/windows/screen.ts @@ -15,6 +15,7 @@ export default class Screen extends WindowBox implements UiComponent { ctx: CanvasRenderingContext2D; scale: number; current_vram_bank: u2 = DEFAULT_VRAM_BANK; + constructor(element: HTMLElement, event: UiEventHandler, cpu_signals: UiCpuSignalHandler) { super(element, "TV", { collapsed: true, fit_content: true }); this.cpu_signals = cpu_signals; @@ -25,27 +26,31 @@ export default class Screen extends WindowBox implements UiComponent { this.screen.width = CANVAS_SIZE; this.screen.height = CANVAS_SIZE; const ctx = this.screen.getContext("2d"); - if (ctx === null) { - throw new Error("could not load screen"); - } + + if (ctx === null) throw new Error("could not load screen"); + this.ctx = ctx; this.container.appendChild(this.screen); - this.test_pattern(); + + this.renderTestPattern(); } - private test_pattern(): void { + private renderTestPattern(): void { for (let x = 0; x < 256; x++) { this.setPixel(x as u8, x as u8); } } reset(): void { - const ctx = this.screen.getContext("2d"); - if (ctx === null) { - throw new Error("todo"); + for (let i = 0; i < 256; i++) { + this.setPixel(i as u8, 0); } } + softReset(): void { + this.reset(); + } + initCpuEvents(c: CpuEventHandler): void { c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => { if (bank !== 1) return; @@ -68,6 +73,8 @@ export default class Screen extends WindowBox implements UiComponent { const y = Math.floor(address / 16) as u4; const point: [number, number] = [x * this.scale, y * this.scale]; + // TODO, come up with better color scheme. + // Probable a lookup table const RED_SCALE = 255 / 2 ** 3; const GREEN_SCALE = 255 / 2 ** 3; const BLUE_SCALE = 255 / 2 ** 2; diff --git a/src/util/elementMaker.ts b/src/util/elementMaker.ts index aa51212..19483ac 100644 --- a/src/util/elementMaker.ts +++ b/src/util/elementMaker.ts @@ -34,6 +34,12 @@ class ElementInProgress { return this; } + /** Set title */ + ti(title: string): ElementInProgress { + this.element.title = title; + return this; + } + /** Return created element */ fin(): E { return this.element; diff --git a/todo.md b/todo.md index 46f90c0..1847dfe 100644 --- a/todo.md +++ b/todo.md @@ -29,7 +29,9 @@ Limit size to printout text buffer Improve instruction explainer. Clearly show what is an instruction and what is a parameter -Ui showing CPU flag(s) (Carry) +Ui showing CPU flag(s) (Carry) and call stack + +Share programs from encoded url Responsive layout @@ -37,4 +39,4 @@ standardize names of all things Documentation with standard names -Example Programs +Example Programs (loaded as one of those encoded string url things)