rsemu 0.0.4

A multiplatform emulator in pure Rust, built bottom-up on a generic framework.
Documentation
<script setup>
// The controller map, lit up as it is used.
//
// Showing the live mask is not decoration: the seam is a *level*, not an
// event. The console samples $4016 whenever the guest strobes it, so a button
// stays held until the page clears it — which is exactly what makes a recorded
// session replayable, and exactly the thing that looks like a bug when a key
// sticks and nothing on screen says so.

import { BUTTONS } from "../session.js";

defineProps({
  held: { type: Number, default: 0 },
  live: { type: Boolean, default: false },
});

const emit = defineEmits(["press", "release"]);

/** Laid out the way the pad is, not the way the shift register is. */
const DPAD = [
  { bit: BUTTONS.up, glyph: "↑", name: "Up", area: "u" },
  { bit: BUTTONS.left, glyph: "←", name: "Left", area: "l" },
  { bit: BUTTONS.down, glyph: "↓", name: "Down", area: "d" },
  { bit: BUTTONS.right, glyph: "→", name: "Right", area: "r" },
];

const FACE = [
  { bit: BUTTONS.select, glyph: "Select", name: "Select", key: "Shift" },
  { bit: BUTTONS.start, glyph: "Start", name: "Start", key: "Enter" },
  { bit: BUTTONS.b, glyph: "B", name: "B", key: "X" },
  { bit: BUTTONS.a, glyph: "A", name: "A", key: "Z" },
];

function hex(mask) {
  return "0x" + mask.toString(16).padStart(2, "0");
}
</script>

<template>
  <div class="pad">
    <div class="cluster dpad">
      <button
        v-for="b in DPAD"
        :key="b.name"
        type="button"
        class="key"
        :class="[`area-${b.area}`, { on: (held & b.bit) !== 0 }]"
        :disabled="!live"
        :aria-pressed="(held & b.bit) !== 0"
        :aria-label="`${b.name} — arrow key`"
        @pointerdown.prevent="emit('press', b.bit)"
        @pointerup.prevent="emit('release', b.bit)"
        @pointerleave="emit('release', b.bit)"
        @pointercancel="emit('release', b.bit)"
      >
        {{ b.glyph }}
      </button>
    </div>

    <div class="cluster face">
      <button
        v-for="b in FACE"
        :key="b.name"
        type="button"
        class="key wide"
        :class="{ on: (held & b.bit) !== 0 }"
        :disabled="!live"
        :aria-pressed="(held & b.bit) !== 0"
        :aria-label="`${b.name} — ${b.key} key`"
        @pointerdown.prevent="emit('press', b.bit)"
        @pointerup.prevent="emit('release', b.bit)"
        @pointerleave="emit('release', b.bit)"
        @pointercancel="emit('release', b.bit)"
      >
        <span>{{ b.glyph }}</span>
        <kbd>{{ b.key }}</kbd>
      </button>
    </div>
  </div>

  <p class="hint">
    Arrow keys are the d-pad. The mask reaches the console's controller port at
    <code>$4016</code> as a level the guest samples when it strobes — currently
    <code class="mask">{{ hex(held) }}</code
    >.
  </p>
</template>

<style scoped>
.pad {
  display: flex;
  flex-wrap: wrap;
  gap: 1rem;
  align-items: center;
  justify-content: space-between;
}

.dpad {
  display: grid;
  grid-template-areas: ". u ." "l . r" ". d .";
  gap: 3px;
}

.area-u {
  grid-area: u;
}
.area-l {
  grid-area: l;
}
.area-d {
  grid-area: d;
}
.area-r {
  grid-area: r;
}

.face {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 4px;
  flex: 1 1 11rem;
}

.key {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 0.35rem;
  min-width: 2.1rem;
  min-height: 2.1rem;
  padding: 0.2rem 0.4rem;
  font: 600 0.8rem/1 var(--sans);
  color: var(--fg-dim);
  background: var(--panel-2);
  border: 1px solid var(--line-strong);
  border-radius: var(--radius-sm);
  cursor: pointer;
  user-select: none;
  touch-action: none;
  transition:
    background 90ms ease,
    color 90ms ease,
    border-color 90ms ease;
}

.key.wide {
  justify-content: space-between;
  padding-inline: 0.55rem;
}

.key:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}

.key.on {
  color: var(--accent-fg);
  background: var(--accent);
  border-color: var(--accent);
}

.key.on kbd {
  color: var(--accent-fg);
  background: transparent;
  border-color: currentColor;
}

kbd {
  pointer-events: none;
}

.mask {
  color: var(--accent);
}
</style>