All files / src/web paging-override.js

100% Statements 54/54
94.44% Branches 17/18
100% Functions 17/17
100% Lines 50/50

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139                            5x                 96x 96x                             533x 533x 533x 533x 533x 533x 533x       4x         104x       554x 554x 554x 554x 554x         41034x       102x 24x 24x 24x 24x       13967614x               10487x       28287x 253x 253x 25845x   28287x       654x 654x 94x 94x                 1871x       586x 586x 586x 558x 558x 558x       4x 68x 68x 68x 68x 68x 2x 2x   68x        
import {
    AndyView,
    bankName,
    describeBank,
    MainView,
    PagedView,
    peekViewed,
    ShadowView,
    viewDiffers,
    viewedBank,
    viewsFor,
} from "../paging.js";
import { PopupMenu, textButton } from "./dom-utils.js";
 
const FixedViews = {
    [PagedView]: { label: "paged", title: "Memory as the CPU has it paged now" },
    [AndyView]: { label: "ANDY", title: "The Master's 4K of private RAM, at &8000 to &8FFF" },
    [MainView]: { label: "main RAM", title: "Main RAM at &3000 to &7FFF, whatever ACCCON pages there" },
    [ShadowView]: { label: "shadow RAM", title: "Shadow RAM at &3000 to &7FFF, whatever ACCCON pages there" },
};
 
/** What `view` is called in a panel's picker, such as "shadow RAM" or "bank 4". */
export function viewLabel(view) {
    const bank = viewedBank(view);
    return bank === undefined ? FixedViews[view].label : `bank ${bankName(bank)}`;
}
 
/**
 * One panel's choice of which memory it shows where the paging has a choice, picked from a menu by its header's
 * title: what the CPU has paged, or a view of one bank or RAM (see paging.js). It reads through the view without
 * side effects, as peekmem does.
 */
export class PagingOverride {
    /**
     * @param {object} options
     * @param {import("./floating-panel.js").FloatingPanel} options.floating - its panel
     * @param {() => void} options.changed - called once the view has changed
     */
    constructor({ floating, changed }) {
        this._changed = changed;
        this._cpu = null;
        this._view = PagedView;
        this._disassembler = null;
        this._picker = textButton("paging-picker", "", "");
        floating.panel.querySelector(".panel-title").after(this._picker);
        this._menu = new PopupMenu({
            opener: this._picker,
            className: "paging-menu",
            floating,
            build: () => this._choices(),
        });
    }
 
    dismissMenu() {
        this._menu.show(false);
    }
 
    setCpu(cpu) {
        this._cpu = cpu;
        this._picker.hidden = viewsFor(cpu.model).length === 1;
        this._view = PagedView;
        this._disassembler = null;
        this._label();
    }
 
    /** @returns {import("../paging.js").PagingView} */
    get view() {
        return this._view;
    }
 
    set view(view) {
        if (view === this._view) return;
        this._view = view;
        this._disassembler = null;
        this._label();
        this._changed();
    }
 
    peek(address) {
        return peekViewed(this._cpu, this._view, address);
    }
 
    /**
     * Whether it shows something at `address` other than what the CPU would read there now; a watch offered there
     * takes no name, as watches read memory as paged.
     */
    differs(address) {
        return viewDiffers(this._cpu, this._view, address);
    }
 
    get disassembler() {
        if (!this._disassembler) {
            const { disassembler } = this._cpu;
            this._disassembler =
                this._view === PagedView ? disassembler : disassembler.reading((address) => this.peek(address));
        }
        return this._disassembler;
    }
 
    _describe(view) {
        const bank = viewedBank(view);
        if (bank === undefined) return FixedViews[view];
        const contents = describeBank(this._cpu, bank);
        return {
            label: viewLabel(view),
            contents,
            title: `Sideways bank ${bankName(bank)} at &8000 to &BFFF: ${contents}`,
        };
    }
 
    /** Shows the view's label again, for what its bank holds now. */
    relabel() {
        if (viewedBank(this._view) !== undefined) this._label();
    }
 
    _label() {
        const { label, contents, title } = this._describe(this._view);
        const text = contents ? `${label}: ${contents}` : label;
        if (this._picker.textContent === text) return;
        this._picker.textContent = text;
        this._picker.title = `${title}\nClick to show another, or name one in Go to: 8000\`4 or 4:8000`;
        this._picker.classList.toggle("overridden", this._view !== PagedView);
    }
 
    _choices() {
        return viewsFor(this._cpu.model).map((view) => {
            const { label, contents, title } = this._describe(view);
            const choice = textButton("paging-choice", contents ? `${view.toUpperCase()} ${contents}` : label, title);
            choice.setAttribute("role", "menuitemradio");
            choice.setAttribute("aria-checked", String(view === this._view));
            choice.addEventListener("click", () => {
                this._menu.show(false);
                this.view = view;
            });
            return choice;
        });
    }
}