Compare commits
No commits in common. "master" and "old-emu" have entirely different histories.
202
core/bus.c
|
@ -3,73 +3,104 @@
|
|||
|
||||
|
||||
|
||||
/********************************* Constants *********************************/
|
||||
|
||||
/* Memory access address masks by data type */
|
||||
static const uint32_t TYPE_MASKS[] = {
|
||||
0x07FFFFFF, /* S8 */
|
||||
0x07FFFFFF, /* U8 */
|
||||
0x07FFFFFE, /* S16 */
|
||||
0x07FFFFFE, /* U16 */
|
||||
0x07FFFFFC /* S32 */
|
||||
};
|
||||
|
||||
|
||||
|
||||
/*************************** Sub-Module Functions ****************************/
|
||||
/***************************** Utility Functions *****************************/
|
||||
|
||||
/* Read a typed value from a buffer in host memory */
|
||||
static int32_t busReadBuffer(uint8_t *data, int type) {
|
||||
static int32_t busReadBuffer(
|
||||
uint8_t *buffer, uint32_t size, uint32_t offset, int type) {
|
||||
|
||||
/* Processing by data type */
|
||||
/* There is no data */
|
||||
if (buffer == NULL)
|
||||
return 0;
|
||||
|
||||
/* Mirror the buffer across its address range */
|
||||
offset &= size - 1;
|
||||
|
||||
/* Processing by type */
|
||||
switch (type) {
|
||||
|
||||
/* Generic implementation */
|
||||
#ifndef VB_LITTLE_ENDIAN
|
||||
case VB_S8 : return ((int8_t *)data)[0];
|
||||
case VB_U8 : return data [0];
|
||||
case VB_S16: return (int32_t) ((int8_t *)data)[1] << 8 | data[0];
|
||||
case VB_U16: return (int32_t) data [1] << 8 | data[0];
|
||||
case VB_S32: return
|
||||
(int32_t) data[3] << 24 | (int32_t) data[2] << 16 |
|
||||
(int32_t) data[1] << 8 | data[0];
|
||||
case VB_S8 :
|
||||
return (int8_t) buffer[offset];
|
||||
case VB_U8:
|
||||
return buffer[offset];
|
||||
case VB_S16:
|
||||
return (int32_t) (int8_t)
|
||||
buffer[offset + 1] << 8 | buffer[offset];
|
||||
case VB_U16:
|
||||
offset &= 0xFFFFFFFE;
|
||||
return (int32_t) (int8_t)
|
||||
buffer[offset + 1] << 8 | buffer[offset];
|
||||
case VB_S32:
|
||||
offset &= 0xFFFFFFFC;
|
||||
return
|
||||
(int32_t) buffer[offset + 3] << 24 |
|
||||
(int32_t) buffer[offset + 2] << 16 |
|
||||
(int32_t) buffer[offset + 1] << 8 |
|
||||
buffer[offset ]
|
||||
;
|
||||
|
||||
/* Little-endian host */
|
||||
/* Little-endian implementation */
|
||||
#else
|
||||
case VB_S8 : return *(int8_t *) data;
|
||||
case VB_U8 : return * data;
|
||||
case VB_S16: return *(int16_t *) data;
|
||||
case VB_U16: return *(uint16_t *) data;
|
||||
case VB_S32: return *(int32_t *) data;
|
||||
case VB_S8 : return *(int8_t *)&buffer[offset ];
|
||||
case VB_U8 : return buffer[offset ];
|
||||
case VB_S16: return *(int16_t *)&buffer[offset & 0xFFFFFFFE];
|
||||
case VB_U16: return *(uint16_t *)&buffer[offset & 0xFFFFFFFE];
|
||||
case VB_S32: return *(int32_t *)&buffer[offset & 0xFFFFFFFC];
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
return 0; /* Unreachable */
|
||||
/* Invalid type */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write a typed value to a buffer in host memory */
|
||||
static void busWriteBuffer(uint8_t *data, int type, int32_t value) {
|
||||
static void busWriteBuffer(
|
||||
uint8_t *buffer, uint32_t size, uint32_t offset, int type, int32_t value) {
|
||||
|
||||
/* Processing by data type */
|
||||
/* There is no data */
|
||||
if (buffer == NULL)
|
||||
return;
|
||||
|
||||
/* Mirror the buffer across its address range */
|
||||
offset &= size - 1;
|
||||
|
||||
/* Processing by type */
|
||||
switch (type) {
|
||||
|
||||
/* Generic implementation */
|
||||
#ifndef VB_LITTLE_ENDIAN
|
||||
case VB_S32: data[3] = value >> 24;
|
||||
data[2] = value >> 16; /* Fallthrough */
|
||||
case VB_S16: /* Fallthrough */
|
||||
case VB_U16: data[1] = value >> 8; /* Fallthrough */
|
||||
case VB_S8 : /* Fallthrough */
|
||||
case VB_U8 : data[0] = value;
|
||||
case VB_S32:
|
||||
offset &= 0xFFFFFFFC;
|
||||
buffer[offset ] = value;
|
||||
buffer[offset + 1] = value >> 8;
|
||||
buffer[offset + 2] = value >> 16;
|
||||
buffer[offset + 3] = value >> 24;
|
||||
break;
|
||||
case VB_S16:
|
||||
case VB_U16:
|
||||
offset &= 0xFFFFFFFE;
|
||||
buffer[offset ] = value;
|
||||
buffer[offset + 1] = value >> 8;
|
||||
break;
|
||||
case VB_S8 :
|
||||
case VB_U8 :
|
||||
buffer[offset ] = value;
|
||||
|
||||
/* Little-endian host */
|
||||
/* Little-endian implementation */
|
||||
#else
|
||||
case VB_S8 : /* Fallthrough */
|
||||
case VB_U8 : * data = value; return;
|
||||
case VB_S16: /* Fallthrough */
|
||||
case VB_U16: *(uint16_t *) data = value; return;
|
||||
case VB_S32: *(int32_t *) data = value; return;
|
||||
case VB_S8 :
|
||||
case VB_U8 :
|
||||
buffer[offset ] = value;
|
||||
break;
|
||||
case VB_S16:
|
||||
case VB_U16:
|
||||
*(int16_t *)&buffer[offset & 0xFFFFFFFE] = value;
|
||||
break;
|
||||
case VB_S32:
|
||||
*(int32_t *)&buffer[offset & 0xFFFFFFFC] = value;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
@ -78,79 +109,54 @@ static void busWriteBuffer(uint8_t *data, int type, int32_t value) {
|
|||
|
||||
|
||||
|
||||
/***************************** Library Functions *****************************/
|
||||
/***************************** Module Functions ******************************/
|
||||
|
||||
/* Read a typed value from the simulation bus */
|
||||
static void busRead(VB *sim, uint32_t address, int type, int32_t *value) {
|
||||
|
||||
/* Working variables */
|
||||
address &= TYPE_MASKS[type];
|
||||
*value = 0;
|
||||
|
||||
/* Process by address range */
|
||||
switch (address >> 24) {
|
||||
case 0: break; /* VIP */
|
||||
case 1: break; /* VSU */
|
||||
case 2: break; /* Misc. I/O */
|
||||
case 3: break; /* Unmapped */
|
||||
case 4: break; /* Game Pak expansion */
|
||||
|
||||
case 5: /* WRAM */
|
||||
*value = busReadBuffer(&sim->wram[address & 0x0000FFFF], type);
|
||||
break;
|
||||
|
||||
case 6: /* Game Pak RAM */
|
||||
if (sim->cart.ram != NULL) {
|
||||
*value = busReadBuffer(
|
||||
&sim->cart.ram[address & sim->cart.ramMask], type);
|
||||
}
|
||||
break;
|
||||
|
||||
case 7: /* Game Pak ROM */
|
||||
if (sim->cart.rom != NULL) {
|
||||
*value = busReadBuffer(
|
||||
&sim->cart.rom[address & sim->cart.romMask], type);
|
||||
}
|
||||
break;
|
||||
static int32_t busRead(VB *sim, uint32_t address, int type) {
|
||||
|
||||
/* Processing by address region */
|
||||
switch (address >> 24 & 7) {
|
||||
case 0: return 0; /* VIP */
|
||||
case 1: return 0; /* VSU */
|
||||
case 2: return 0; /* Misc. hardware */
|
||||
case 3: return 0; /* Unmapped */
|
||||
case 4: return 0; /* Game pak expansion */
|
||||
case 5: return /* WRAM */
|
||||
busReadBuffer(sim->wram , 0x10000 , address, type);
|
||||
case 6: return /* Game pak RAM */
|
||||
busReadBuffer(sim->cart.ram, sim->cart.ramSize, address, type);
|
||||
case 7: return /* Game pak ROM */
|
||||
busReadBuffer(sim->cart.rom, sim->cart.romSize, address, type);
|
||||
}
|
||||
|
||||
/* Unreachable */
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write a typed value to the simulation bus */
|
||||
static void busWrite(VB*sim,uint32_t address,int type,int32_t value,int debug){
|
||||
(void) debug;
|
||||
|
||||
/* Working variables */
|
||||
address &= TYPE_MASKS[type];
|
||||
|
||||
/* Process by address range */
|
||||
switch (address >> 24) {
|
||||
/* Processing by address region */
|
||||
switch (address >> 24 & 7) {
|
||||
case 0: break; /* VIP */
|
||||
case 1: break; /* VSU */
|
||||
case 2: break; /* Misc. I/O */
|
||||
case 2: break; /* Misc. hardware */
|
||||
case 3: break; /* Unmapped */
|
||||
case 4: break; /* Game Pak expansion */
|
||||
|
||||
case 4: break; /* Game pak expansion */
|
||||
case 5: /* WRAM */
|
||||
busWriteBuffer(&sim->wram[address & 0x0000FFFF], type, value);
|
||||
busWriteBuffer(sim->wram ,0x10000 ,address,type,value);
|
||||
break;
|
||||
|
||||
case 6: /* Game Pak RAM */
|
||||
if (sim->cart.ram != NULL) {
|
||||
busWriteBuffer(
|
||||
&sim->cart.ram[address & sim->cart.ramMask], type, value);
|
||||
}
|
||||
case 6: /* Game pak RAM */
|
||||
busWriteBuffer(sim->cart.ram,sim->cart.ramSize,address,type,value);
|
||||
break;
|
||||
|
||||
case 7: /* Game Pak ROM */
|
||||
if (debug && sim->cart.rom != NULL) {
|
||||
busWriteBuffer(
|
||||
&sim->cart.rom[address & sim->cart.romMask], type, value);
|
||||
}
|
||||
case 7: /* Game pak ROM */
|
||||
busWriteBuffer(sim->cart.rom,sim->cart.romSize,address,type,value);
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
#endif /* VBAPI */
|
||||
|
|
3202
core/cpu.c
477
core/vb.c
|
@ -1,129 +1,41 @@
|
|||
#ifndef VBAPI
|
||||
#define VBAPI
|
||||
#endif
|
||||
#include <float.h>
|
||||
#include <vb.h>
|
||||
|
||||
|
||||
|
||||
/*********************************** Types ***********************************/
|
||||
/***************************** Utility Functions *****************************/
|
||||
|
||||
/* Simulation state */
|
||||
struct VB {
|
||||
|
||||
/* Game Pak */
|
||||
struct {
|
||||
uint8_t *ram; /* Save RAM */
|
||||
uint8_t *rom; /* Program ROM */
|
||||
uint32_t ramMask; /* Size of SRAM - 1 */
|
||||
uint32_t romMask; /* Size of ROM - 1 */
|
||||
} cart;
|
||||
|
||||
/* CPU */
|
||||
struct {
|
||||
|
||||
/* Cache Control Word */
|
||||
struct {
|
||||
uint8_t ice; /* Instruction Cache Enable */
|
||||
} chcw;
|
||||
|
||||
/* Exception Cause Register */
|
||||
struct {
|
||||
uint16_t eicc; /* Exception/Interrupt Cause Code */
|
||||
uint16_t fecc; /* Fatal Error Cause Code */
|
||||
} ecr;
|
||||
|
||||
/* Program Status Word */
|
||||
struct {
|
||||
uint8_t ae; /* Address Trap Enable */
|
||||
uint8_t cy; /* Carry */
|
||||
uint8_t ep; /* Exception Pending */
|
||||
uint8_t fiv; /* Floating Invalid */
|
||||
uint8_t fov; /* Floating Overflow */
|
||||
uint8_t fpr; /* Floading Precision */
|
||||
uint8_t fro; /* Floating Reserved Operand */
|
||||
uint8_t fud; /* Floading Underflow */
|
||||
uint8_t fzd; /* Floating Zero Divide */
|
||||
uint8_t i; /* Interrupt Level */
|
||||
uint8_t id; /* Interrupt Disable */
|
||||
uint8_t np; /* NMI Pending */
|
||||
uint8_t ov; /* Overflow */
|
||||
uint8_t s; /* Sign */
|
||||
uint8_t z; /* Zero */
|
||||
} psw;
|
||||
|
||||
/* Other registers */
|
||||
uint32_t adtre; /* Address Trap Register for Execution */
|
||||
uint32_t eipc; /* Exception/Interrupt PC */
|
||||
uint32_t eipsw; /* Exception/Interrupt PSW */
|
||||
uint32_t fepc; /* Fatal Error PC */
|
||||
uint32_t fepsw; /* Fatal Error PSW */
|
||||
uint32_t pc; /* Program Counter */
|
||||
int32_t program[32]; /* Program registers */
|
||||
uint32_t sr29; /* System register 29 */
|
||||
uint32_t sr31; /* System register 31 */
|
||||
|
||||
/* Working data */
|
||||
union {
|
||||
struct {
|
||||
uint32_t dest;
|
||||
uint64_t src;
|
||||
} bs; /* Arithmetic bit strings */
|
||||
struct {
|
||||
uint32_t address;
|
||||
int32_t value;
|
||||
} data; /* Data accesses */
|
||||
} aux;
|
||||
|
||||
/* Other state */
|
||||
uint32_t clocks; /* Master clocks to wait */
|
||||
uint16_t code[2]; /* Instruction code units */
|
||||
uint16_t exception; /* Exception cause code */
|
||||
int halt; /* CPU is halting */
|
||||
uint16_t irq; /* Interrupt request lines */
|
||||
int length; /* Instruction code length */
|
||||
uint32_t nextPC; /* Address of next instruction */
|
||||
int operation; /* Current operation ID */
|
||||
int step; /* Operation sub-task ID */
|
||||
} cpu;
|
||||
|
||||
/* Other system state */
|
||||
uint8_t wram[0x10000]; /* System RAM */
|
||||
|
||||
/* Application data */
|
||||
vbOnException onException; /* CPU exception */
|
||||
vbOnExecute onExecute; /* CPU instruction execute */
|
||||
vbOnFetch onFetch; /* CPU instruction fetch */
|
||||
vbOnRead onRead; /* CPU instruction read */
|
||||
vbOnWrite onWrite; /* CPU instruction write */
|
||||
void *tag; /* User data */
|
||||
};
|
||||
|
||||
|
||||
|
||||
/***************************** Library Functions *****************************/
|
||||
|
||||
/* Sign-extend an integer of variable width */
|
||||
static int32_t SignExtend(int32_t value, int32_t bits) {
|
||||
#ifndef VB_SIGNED_PROPAGATE
|
||||
value &= ~((uint32_t) 0xFFFFFFFF << bits);
|
||||
bits = (int32_t) 1 << (bits - (int32_t) 1);
|
||||
return (value ^ bits) - bits;
|
||||
#else
|
||||
return value << (32 - bits) >> (32 - bits);
|
||||
#endif
|
||||
/* Select the lesser of two unsigned numbers */
|
||||
static uint32_t Min(uint32_t a, uint32_t b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
/* Sign-extend an integer, masking upper bits if positive */
|
||||
#ifndef VB_SIGNED_PROPAGATE
|
||||
/* Generic implementation */
|
||||
static int32_t SignExtend(int32_t value, int bits) {
|
||||
return value & 1 << (bits - 1) ?
|
||||
value | ~0 << bits :
|
||||
value & ((1 << bits) - 1)
|
||||
;
|
||||
}
|
||||
#else
|
||||
/* Sign-propagating implementation */
|
||||
#define SignExtend(v, b) ((int32_t) (v) << (32 - (b)) >> (32 - (b)))
|
||||
#endif
|
||||
|
||||
|
||||
/******************************** Sub-Modules ********************************/
|
||||
|
||||
/**************************** Sub-Module Imports *****************************/
|
||||
|
||||
#include "bus.c"
|
||||
#include "cpu.c"
|
||||
|
||||
|
||||
|
||||
/***************************** Library Functions *****************************/
|
||||
/***************************** Module Functions ******************************/
|
||||
|
||||
/* Process a simulation for a given number of clocks */
|
||||
static int sysEmulate(VB *sim, uint32_t clocks) {
|
||||
|
@ -132,7 +44,7 @@ static int sysEmulate(VB *sim, uint32_t clocks) {
|
|||
;
|
||||
}
|
||||
|
||||
/* Determine how many clocks are guaranteed to process */
|
||||
/* Determine how many clocks can be simulated without a breakpoint */
|
||||
static uint32_t sysUntil(VB *sim, uint32_t clocks) {
|
||||
clocks = cpuUntil(sim, clocks);
|
||||
return clocks;
|
||||
|
@ -140,140 +52,127 @@ static uint32_t sysUntil(VB *sim, uint32_t clocks) {
|
|||
|
||||
|
||||
|
||||
/******************************* API Commands ********************************/
|
||||
/************************************ API ************************************/
|
||||
|
||||
/* Process one simulation */
|
||||
VBAPI int vbEmulate(VB *sim, uint32_t *clocks) {
|
||||
int brk; /* A callback requested a break */
|
||||
uint32_t until; /* Clocks guaranteed to process */
|
||||
while (*clocks != 0) {
|
||||
/* Process a simulation */
|
||||
int vbEmulate(VB *sim, uint32_t *clocks) {
|
||||
int brk; /* A break was requested */
|
||||
uint32_t until; /* Number of clocks during which no break will occur */
|
||||
|
||||
/* Process all clocks */
|
||||
for (brk = 0; *clocks != 0 && !brk; *clocks -= until) {
|
||||
until = sysUntil (sim, *clocks);
|
||||
brk = sysEmulate(sim, until );
|
||||
*clocks -= until;
|
||||
if (brk)
|
||||
return brk; /* TODO: return 1 */
|
||||
}
|
||||
return 0;
|
||||
|
||||
return brk;
|
||||
}
|
||||
|
||||
/* Process multiple simulations */
|
||||
VBAPI int vbEmulateEx(VB **sims, int count, uint32_t *clocks) {
|
||||
int brk; /* A callback requested a break */
|
||||
uint32_t until; /* Clocks guaranteed to process */
|
||||
int vbEmulateEx(VB **sims, int count, uint32_t *clocks) {
|
||||
int brk; /* A break was requested */
|
||||
uint32_t until; /* Number of clocks during which no break will occur */
|
||||
int x; /* Iterator */
|
||||
while (*clocks != 0) {
|
||||
|
||||
/* Process all clocks */
|
||||
for (brk = 0; *clocks != 0 && !brk; *clocks -= until) {
|
||||
until = *clocks;
|
||||
for (x = count - 1; x >= 0; x--)
|
||||
for (x = 0; x < count; x++)
|
||||
until = sysUntil (sims[x], until);
|
||||
|
||||
brk = 0;
|
||||
for (x = count - 1; x >= 0; x--)
|
||||
for (x = 0; x < count; x++)
|
||||
brk |= sysEmulate(sims[x], until);
|
||||
|
||||
*clocks -= until;
|
||||
if (brk)
|
||||
return brk; /* TODO: return 1 */
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Retrieve the game pack RAM buffer */
|
||||
VBAPI void* vbGetCartRAM(VB *sim, uint32_t *size) {
|
||||
return brk;
|
||||
}
|
||||
|
||||
/* Retrieve a current breakpoint handler */
|
||||
void* vbGetCallback(VB *sim, int id) {
|
||||
switch (id) {
|
||||
case VB_ONEXCEPTION: return *(void **)&sim->onException;
|
||||
case VB_ONEXECUTE : return *(void **)&sim->onExecute;
|
||||
case VB_ONFETCH : return *(void **)&sim->onFetch;
|
||||
case VB_ONREAD : return *(void **)&sim->onRead;
|
||||
case VB_ONWRITE : return *(void **)&sim->onWrite;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Retrieve the value of a register */
|
||||
int32_t vbGetRegister(VB *sim, int type, int id) {
|
||||
switch (type) {
|
||||
case VB_PROGRAM:
|
||||
return id < 0 || id > 31 ? 0 : sim->cpu.program[id];
|
||||
case VB_SYSTEM:
|
||||
return cpuGetSystemRegister(sim, id);
|
||||
case VB_OTHER:
|
||||
switch (id) {
|
||||
case VB_PC: return sim->cpu.pc;
|
||||
}
|
||||
}
|
||||
return 0; /* Invalid type */
|
||||
}
|
||||
|
||||
/* Retrieve a handle to the current cartridge ROM data */
|
||||
uint8_t* vbGetROM(VB *sim, uint32_t *size) {
|
||||
if (size != NULL)
|
||||
*size = sim->cart.ram == NULL ? 0 : sim->cart.ramMask + 1;
|
||||
return sim->cart.ram;
|
||||
}
|
||||
|
||||
/* Retrieve the game pack ROM buffer */
|
||||
VBAPI void* vbGetCartROM(VB *sim, uint32_t *size) {
|
||||
if (size != NULL)
|
||||
*size = sim->cart.rom == NULL ? 0 : sim->cart.romMask + 1;
|
||||
*size = sim->cart.romSize;
|
||||
return sim->cart.rom;
|
||||
}
|
||||
|
||||
/* Retrieve the exception callback handle */
|
||||
VBAPI vbOnException vbGetExceptionCallback(VB *sim) {
|
||||
return sim->onException;
|
||||
/* Retrieve a handle to the current cartridge RAM data */
|
||||
uint8_t* vbGetSRAM(VB *sim, uint32_t *size) {
|
||||
if (size != NULL)
|
||||
*size = sim->cart.ramSize;
|
||||
return sim->cart.ram;
|
||||
}
|
||||
|
||||
/* Retrieve the execute callback handle */
|
||||
VBAPI vbOnExecute vbGetExecuteCallback(VB *sim) {
|
||||
return sim->onExecute;
|
||||
}
|
||||
/* Prepare a simulation instance for use */
|
||||
void vbInit(VB *sim) {
|
||||
|
||||
/* Retrieve the fetch callback handle */
|
||||
VBAPI vbOnFetch vbGetFetchCallback(VB *sim) {
|
||||
return sim->onFetch;
|
||||
}
|
||||
|
||||
/* Retrieve the value of the program counter */
|
||||
VBAPI uint32_t vbGetProgramCounter(VB *sim) {
|
||||
return sim->cpu.pc;
|
||||
}
|
||||
|
||||
/* Retrieve the value in a program register */
|
||||
VBAPI int32_t vbGetProgramRegister(VB *sim, int index) {
|
||||
return index < 1 || index > 31 ? 0 : sim->cpu.program[index];
|
||||
}
|
||||
|
||||
/* Retrieve the read callback handle */
|
||||
VBAPI vbOnRead vbGetReadCallback(VB *sim) {
|
||||
return sim->onRead;
|
||||
}
|
||||
|
||||
/* Retrieve the value in a system register */
|
||||
VBAPI uint32_t vbGetSystemRegister(VB *sim, int index) {
|
||||
return index < 0 || index > 31 ? 0 : cpuGetSystemRegister(sim, index);
|
||||
}
|
||||
|
||||
/* Retrieve a simulation's userdata pointer */
|
||||
VBAPI void* vbGetUserData(VB *sim) {
|
||||
return sim->tag;
|
||||
}
|
||||
|
||||
/* Retrieve the write callback handle */
|
||||
VBAPI vbOnWrite vbGetWriteCallback(VB *sim) {
|
||||
return sim->onWrite;
|
||||
}
|
||||
|
||||
/* Initialize a simulation instance */
|
||||
VBAPI VB* vbInit(VB *sim) {
|
||||
sim->cart.ram = NULL;
|
||||
sim->cart.rom = NULL;
|
||||
/* Breakpoint handlers */
|
||||
sim->onException = NULL;
|
||||
sim->onExecute = NULL;
|
||||
sim->onFetch = NULL;
|
||||
sim->onRead = NULL;
|
||||
sim->onWrite = NULL;
|
||||
|
||||
/* Game pak */
|
||||
sim->cart.ram = NULL;
|
||||
sim->cart.ramSize = 0;
|
||||
sim->cart.rom = NULL;
|
||||
sim->cart.romSize = 0;
|
||||
|
||||
/* Hardware reset */
|
||||
vbReset(sim);
|
||||
return sim;
|
||||
}
|
||||
|
||||
/* Read a value from the memory bus */
|
||||
VBAPI int32_t vbRead(VB *sim, uint32_t address, int type) {
|
||||
int32_t value;
|
||||
if (type < 0 || type > 4)
|
||||
return 0;
|
||||
busRead(sim, address, type, &value);
|
||||
return value;
|
||||
/* Read a value from memory */
|
||||
int32_t vbRead(VB *sim, uint32_t address, int type) {
|
||||
return busRead(sim, address, type);
|
||||
}
|
||||
|
||||
/* Read multiple bytes from memory */
|
||||
void vbReadEx(VB *sim, uint32_t address, uint8_t *buffer, uint32_t length) {
|
||||
while (length--) *buffer++ = busRead(sim, address++, VB_U8);
|
||||
}
|
||||
|
||||
/* Simulate a hardware reset */
|
||||
VBAPI VB* vbReset(VB *sim) {
|
||||
uint32_t x; /* Iterator */
|
||||
void vbReset(VB *sim) {
|
||||
int x; /* Iterator */
|
||||
|
||||
/* WRAM (the hardware does not do this) */
|
||||
/* Reset WRAM (the hardware does not do this) */
|
||||
for (x = 0; x < 0x10000; x++)
|
||||
sim->wram[x] = 0x00;
|
||||
|
||||
/* CPU (normal) */
|
||||
sim->cpu.exception = 0;
|
||||
sim->cpu.halt = 0;
|
||||
sim->cpu.irq = 0;
|
||||
sim->cpu.pc = 0xFFFFFFF0;
|
||||
cpuSetSystemRegister(sim, VB_ECR, 0x0000FFF0, 1);
|
||||
cpuSetSystemRegister(sim, VB_PSW, 0x00008000, 1);
|
||||
for (x = 0; x < 5; x++)
|
||||
sim->cpu.irq[x] = 0;
|
||||
|
||||
/* CPU (extra, hardware does not do this) */
|
||||
/* CPU (extra, hardware doesn't do this) */
|
||||
sim->cpu.adtre = 0x00000000;
|
||||
sim->cpu.eipc = 0x00000000;
|
||||
sim->cpu.eipsw = 0x00000000;
|
||||
|
@ -285,107 +184,99 @@ VBAPI VB* vbReset(VB *sim) {
|
|||
for (x = 0; x < 32; x++)
|
||||
sim->cpu.program[x] = 0x00000000;
|
||||
|
||||
/* CPU (other) */
|
||||
/* CPU (internal) */
|
||||
sim->cpu.bitstring = 0;
|
||||
sim->cpu.clocks = 0;
|
||||
sim->cpu.nextPC = 0xFFFFFFF0;
|
||||
sim->cpu.operation = CPU_FETCH;
|
||||
sim->cpu.exception = 0;
|
||||
sim->cpu.stage = CPU_FETCH;
|
||||
sim->cpu.step = 0;
|
||||
|
||||
return sim;
|
||||
}
|
||||
|
||||
/* Specify a game pak RAM buffer */
|
||||
VBAPI int vbSetCartRAM(VB *sim, void *sram, uint32_t size) {
|
||||
if (sram != NULL) {
|
||||
if (size < 16 || size > 0x1000000 || (size & (size - 1)) != 0)
|
||||
return 1;
|
||||
sim->cart.ramMask = size - 1;
|
||||
/* Specify a breakpoint handler */
|
||||
void* vbSetCallback(VB *sim, int id, void *proc) {
|
||||
void *prev = vbGetCallback(sim, id);
|
||||
switch (id) {
|
||||
case VB_ONEXCEPTION: *(void **)&sim->onException = proc; break;
|
||||
case VB_ONEXECUTE : *(void **)&sim->onExecute = proc; break;
|
||||
case VB_ONFETCH : *(void **)&sim->onFetch = proc; break;
|
||||
case VB_ONREAD : *(void **)&sim->onRead = proc; break;
|
||||
case VB_ONWRITE : *(void **)&sim->onWrite = proc; break;
|
||||
}
|
||||
sim->cart.ram = sram;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* Specify a value for a register */
|
||||
int32_t vbSetRegister(VB *sim, int type, int id, int32_t value) {
|
||||
switch (type) {
|
||||
case VB_PROGRAM:
|
||||
return id < 1 || id > 31 ? 0 : (sim->cpu.program[id] = value);
|
||||
case VB_SYSTEM:
|
||||
return cpuSetSystemRegister(sim, id, value, 1);
|
||||
case VB_OTHER:
|
||||
switch (id) {
|
||||
case VB_PC:
|
||||
sim->cpu.bitstring = 0;
|
||||
sim->cpu.clocks = 0;
|
||||
sim->cpu.exception = 0;
|
||||
sim->cpu.stage = CPU_FETCH;
|
||||
return sim->cpu.pc = value & 0xFFFFFFFE;
|
||||
}
|
||||
}
|
||||
return 0; /* Invalid type or ID */
|
||||
}
|
||||
|
||||
/* Specify a cartridge ROM buffer */
|
||||
int vbSetROM(VB *sim, uint8_t *data, uint32_t size) {
|
||||
|
||||
/* Specifying no ROM */
|
||||
if (data == NULL) {
|
||||
sim->cart.rom = NULL;
|
||||
sim->cart.romSize = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Specify a game pak ROM buffer */
|
||||
VBAPI int vbSetCartROM(VB *sim, void *rom, uint32_t size) {
|
||||
if (rom != NULL) {
|
||||
if (size < 16 || size > 0x1000000 || (size & (size - 1)) != 0)
|
||||
return 1;
|
||||
sim->cart.romMask = size - 1;
|
||||
}
|
||||
sim->cart.rom = rom;
|
||||
/* Error checking */
|
||||
if (
|
||||
size < 4 ||
|
||||
size > 0x1000000 ||
|
||||
(size & (size - 1)) /* Power of 2 */
|
||||
) return 1;
|
||||
|
||||
/* Register the ROM data */
|
||||
sim->cart.rom = data;
|
||||
sim->cart.romSize = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Specify a new exception callback handle */
|
||||
VBAPI vbOnException vbSetExceptionCallback(VB *sim, vbOnException callback) {
|
||||
vbOnException prev = sim->onException;
|
||||
sim->onException = callback;
|
||||
return prev;
|
||||
}
|
||||
/* Specify a cartridge RAM buffer */
|
||||
int vbSetSRAM(VB *sim, uint8_t *data, uint32_t size) {
|
||||
|
||||
/* Specify a new execute callback handle */
|
||||
VBAPI vbOnExecute vbSetExecuteCallback(VB *sim, vbOnExecute callback) {
|
||||
vbOnExecute prev = sim->onExecute;
|
||||
sim->onExecute = callback;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* Specify a new fetch callback handle */
|
||||
VBAPI vbOnFetch vbSetFetchCallback(VB *sim, vbOnFetch callback) {
|
||||
vbOnFetch prev = sim->onFetch;
|
||||
sim->onFetch = callback;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* Specify a new value for the program counter */
|
||||
VBAPI uint32_t vbSetProgramCounter(VB *sim, uint32_t value) {
|
||||
sim->cpu.operation = CPU_FETCH;
|
||||
sim->cpu.pc = sim->cpu.nextPC = value & 0xFFFFFFFE;
|
||||
sim->cpu.step = 0;
|
||||
return sim->cpu.pc;
|
||||
}
|
||||
|
||||
/* Specify a new value for a program register */
|
||||
VBAPI int32_t vbSetProgramRegister(VB *sim, int index, int32_t value) {
|
||||
return index < 1 || index > 31 ? 0 : (sim->cpu.program[index] = value);
|
||||
}
|
||||
|
||||
/* Specify a new read callback handle */
|
||||
VBAPI vbOnRead vbSetReadCallback(VB *sim, vbOnRead callback) {
|
||||
vbOnRead prev = sim->onRead;
|
||||
sim->onRead = callback;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* Specify a new value for a system register */
|
||||
VBAPI uint32_t vbSetSystemRegister(VB *sim, int index, uint32_t value) {
|
||||
return index < 0 || index > 31 ? 0 :
|
||||
cpuSetSystemRegister(sim, index, value, 1);
|
||||
}
|
||||
|
||||
/* Specify a new write callback handle */
|
||||
VBAPI vbOnWrite vbSetWriteCallback(VB *sim, vbOnWrite callback) {
|
||||
vbOnWrite prev = sim->onWrite;
|
||||
sim->onWrite = callback;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* Determine the size of a simulation instance */
|
||||
VBAPI size_t vbSizeOf() {
|
||||
return sizeof (VB);
|
||||
}
|
||||
|
||||
/* Specify a simulation's userdata pointer */
|
||||
VBAPI void* vbSetUserData(VB *sim, void *tag) {
|
||||
void *prev = sim->tag;
|
||||
sim->tag = tag;
|
||||
return prev;
|
||||
}
|
||||
|
||||
/* Write a value to the memory bus */
|
||||
VBAPI int32_t vbWrite(VB *sim, uint32_t address, int type, int32_t value) {
|
||||
if (type < 0 || type > 4)
|
||||
/* Specifying no SRAM */
|
||||
if (data == NULL) {
|
||||
sim->cart.ram = NULL;
|
||||
sim->cart.ramSize = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Error checking */
|
||||
if (
|
||||
size < 4 ||
|
||||
size > 0x1000000 ||
|
||||
(size & (size - 1)) /* Power of 2 */
|
||||
) return 1;
|
||||
|
||||
/* Register the SRAM data */
|
||||
sim->cart.ram = data;
|
||||
sim->cart.ramSize = size;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* Write a value to memory */
|
||||
void vbWrite(VB *sim, uint32_t address, int type, int32_t value) {
|
||||
busWrite(sim, address, type, value, 1);
|
||||
return vbRead(sim, address, type);
|
||||
}
|
||||
|
||||
/* Write multiple values to memory */
|
||||
void vbWriteEx(VB *sim, uint32_t address, uint8_t *buffer, uint32_t length) {
|
||||
while (length--) busWrite(sim, address++, VB_U8, *buffer++, 1);
|
||||
}
|
||||
|
|
214
core/vb.h
|
@ -15,15 +15,38 @@ extern "C" {
|
|||
|
||||
|
||||
|
||||
/**************************** Compilation Control ****************************/
|
||||
|
||||
/*
|
||||
VB_LITTLE_ENDIAN
|
||||
|
||||
Accelerates simulated memory reads and writes on hosts with little-endian
|
||||
byte ordering. If left undefined, a generic implementation is used instead.
|
||||
*/
|
||||
|
||||
/*
|
||||
VB_SIGNED_PROPAGATE
|
||||
|
||||
Accelerates sign extension by assuming the >> operator propagates the sign
|
||||
bit for signed types and does not for unsigned types. If left undefined, a
|
||||
generic implementation is used instead.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
/********************************* Constants *********************************/
|
||||
|
||||
/* Callback IDs */
|
||||
#define VB_EXCEPTION 0
|
||||
#define VB_EXECUTE 1
|
||||
#define VB_FETCH 2
|
||||
#define VB_FRAME 3
|
||||
#define VB_READ 4
|
||||
#define VB_WRITE 5
|
||||
/* Data types */
|
||||
#define VB_S8 0
|
||||
#define VB_U8 1
|
||||
#define VB_S16 2
|
||||
#define VB_U16 3
|
||||
#define VB_S32 4
|
||||
|
||||
/* Register types */
|
||||
#define VB_PROGRAM 0
|
||||
#define VB_SYSTEM 1
|
||||
#define VB_OTHER 2
|
||||
|
||||
/* System registers */
|
||||
#define VB_ADTRE 25
|
||||
|
@ -37,62 +60,157 @@ extern "C" {
|
|||
#define VB_PSW 5
|
||||
#define VB_TKCW 7
|
||||
|
||||
/* Memory access data types */
|
||||
#define VB_S8 0
|
||||
#define VB_U8 1
|
||||
#define VB_S16 2
|
||||
#define VB_U16 3
|
||||
#define VB_S32 4
|
||||
/* Other registers */
|
||||
#define VB_PC 0
|
||||
|
||||
/* Callbacks */
|
||||
#define VB_ONEXCEPTION 0
|
||||
#define VB_ONEXECUTE 1
|
||||
#define VB_ONFETCH 2
|
||||
#define VB_ONREAD 3
|
||||
#define VB_ONWRITE 4
|
||||
|
||||
|
||||
|
||||
/*********************************** Types ***********************************/
|
||||
|
||||
/* Simulation state */
|
||||
/* Forward references */
|
||||
typedef struct VB VB;
|
||||
|
||||
/* Callbacks */
|
||||
typedef int (*vbOnException)(VB *sim, uint16_t *cause);
|
||||
typedef int (*vbOnExecute )(VB *sim, uint32_t address, const uint16_t *code, int length);
|
||||
typedef int (*vbOnFetch )(VB *sim, int fetch, uint32_t address, int32_t *value, uint32_t *cycles);
|
||||
typedef int (*vbOnRead )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles);
|
||||
typedef int (*vbOnWrite )(VB *sim, uint32_t address, int type, int32_t *value, uint32_t *cycles, int *cancel);
|
||||
/* Memory access descriptor */
|
||||
typedef struct {
|
||||
uint32_t address; /* Memory address */
|
||||
uint32_t clocks; /* Number of clocks taken */
|
||||
int32_t value; /* Data loaded/to store */
|
||||
uint8_t type; /* Data type */
|
||||
} VBAccess;
|
||||
|
||||
/* Exception descriptor */
|
||||
typedef struct {
|
||||
uint32_t address; /* Memory address of handler routine */
|
||||
uint16_t code; /* Cause code */
|
||||
uint8_t cancel; /* Set to cancel the exception */
|
||||
} VBException;
|
||||
|
||||
/* Instruction descriptor */
|
||||
typedef struct {
|
||||
uint32_t address; /* Memory address */
|
||||
uint16_t code[2]; /* Fetched code units */
|
||||
uint8_t size; /* Size in halfwords */
|
||||
} VBInstruction;
|
||||
|
||||
/* Breakpoint handlers */
|
||||
typedef int (*VBExceptionProc)(VB *, VBException *);
|
||||
typedef int (*VBExecuteProc )(VB *, VBInstruction *);
|
||||
typedef int (*VBFetchProc )(VB *, int, VBAccess *);
|
||||
typedef int (*VBMemoryProc )(VB *, VBAccess *);
|
||||
|
||||
/* Simulation state */
|
||||
struct VB {
|
||||
|
||||
/* Game pak */
|
||||
struct {
|
||||
uint8_t *ram; /* (S)RAM data */
|
||||
uint8_t *rom; /* ROM data */
|
||||
uint32_t ramSize; /* Size of RAM in bytes */
|
||||
uint32_t romSize; /* Size of ROM in bytes */
|
||||
} cart;
|
||||
|
||||
/* CPU */
|
||||
struct {
|
||||
|
||||
/* Main registers */
|
||||
int32_t program[32]; /* Program registers */
|
||||
uint32_t pc; /* Program counter */
|
||||
|
||||
/* System registers */
|
||||
uint32_t adtre; /* Address trap target address */
|
||||
uint32_t eipc; /* Exception return PC */
|
||||
uint32_t eipsw; /* Exception return PSW */
|
||||
uint32_t fepc; /* Duplexed exception return PC */
|
||||
uint32_t fepsw; /* Duplexed exception return PSW */
|
||||
uint32_t sr29; /* System register 29 */
|
||||
uint32_t sr31; /* System register 31 */
|
||||
|
||||
/* Exception cause register */
|
||||
struct {
|
||||
uint16_t eicc; /* Exception/interrupt cause code */
|
||||
uint16_t fecc; /* Duplexed exception cause code */
|
||||
} ecr;
|
||||
|
||||
/* Cache control word */
|
||||
struct {
|
||||
uint8_t ice; /* Instruction cache enable */
|
||||
} chcw;
|
||||
|
||||
/* Program status word */
|
||||
struct {
|
||||
uint8_t ae; /* Address trap enable */
|
||||
uint8_t cy; /* Carry */
|
||||
uint8_t ep; /* Exception pending */
|
||||
uint8_t fiv; /* Floating-point invalid operation */
|
||||
uint8_t fov; /* Floating-point overflow */
|
||||
uint8_t fpr; /* Floating point precision degradation */
|
||||
uint8_t fro; /* Floating-point reserved operand */
|
||||
uint8_t fud; /* Floating-point underflow */
|
||||
uint8_t fzd; /* Floating-point zero division */
|
||||
uint8_t i; /* Interrupt level */
|
||||
uint8_t id; /* Interrupt disable */
|
||||
uint8_t np; /* Duplexed exception pending */
|
||||
uint8_t ov; /* Overflow */
|
||||
uint8_t s; /* Sign */
|
||||
uint8_t z; /* Zero */
|
||||
} psw;
|
||||
|
||||
/* Instruction */
|
||||
struct {
|
||||
int32_t aux[9]; /* Auxiliary storage */
|
||||
void *def; /* Operation descriptor */
|
||||
uint16_t code[2]; /* Fetched code units */
|
||||
uint8_t size; /* Size in bytes */
|
||||
} inst;
|
||||
|
||||
/* Other state */
|
||||
uint32_t clocks; /* Clocks until next activity */
|
||||
uint32_t fpFlags; /* Floating-point exception flags */
|
||||
uint16_t exception; /* Current exception cause code */
|
||||
uint8_t irq[5]; /* Interrupt requests */
|
||||
uint8_t bitstring; /* Processing a bit string instruction */
|
||||
uint8_t stage; /* Pipeline stage handler */
|
||||
uint8_t step; /* General processing step index */
|
||||
} cpu;
|
||||
|
||||
/* Breakpoint hooks */
|
||||
VBExceptionProc onException; /* Exception raised */
|
||||
VBExecuteProc onExecute; /* Instruction execute */
|
||||
VBFetchProc onFetch; /* Bus read (fetch) */
|
||||
VBMemoryProc onRead; /* Bus read (execute) */
|
||||
VBMemoryProc onWrite; /* Bus write */
|
||||
|
||||
/* Other state */
|
||||
uint8_t wram[0x10000]; /* System memory */
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
/******************************* API Commands ********************************/
|
||||
/************************************ API ************************************/
|
||||
|
||||
VBAPI int vbEmulate (VB *sim, uint32_t *clocks);
|
||||
VBAPI int vbEmulateEx (VB **sims, int count, uint32_t *clocks);
|
||||
VBAPI void* vbGetCallback(VB *sim, int id);
|
||||
VBAPI void* vbGetCartRAM (VB *sim, uint32_t *size);
|
||||
VBAPI void* vbGetCartROM (VB *sim, uint32_t *size);
|
||||
VBAPI vbOnException vbGetExceptionCallback(VB *sim);
|
||||
VBAPI vbOnExecute vbGetExecuteCallback (VB *sim);
|
||||
VBAPI vbOnFetch vbGetFetchCallback (VB *sim);
|
||||
VBAPI uint32_t vbGetProgramCounter (VB *sim);
|
||||
VBAPI int32_t vbGetProgramRegister (VB *sim, int index);
|
||||
VBAPI vbOnRead vbGetReadCallback (VB *sim);
|
||||
VBAPI uint32_t vbGetSystemRegister (VB *sim, int index);
|
||||
VBAPI void* vbGetUserData (VB *sim);
|
||||
VBAPI vbOnWrite vbGetWriteCallback (VB *sim);
|
||||
VBAPI VB* vbInit (VB *sim);
|
||||
VBAPI int32_t vbGetRegister(VB *sim, int type, int id);
|
||||
VBAPI uint8_t* vbGetROM (VB *sim, uint32_t *size);
|
||||
VBAPI uint8_t* vbGetSRAM (VB *sim, uint32_t *size);
|
||||
VBAPI void vbInit (VB *sim);
|
||||
VBAPI int32_t vbRead (VB *sim, uint32_t address, int type);
|
||||
VBAPI VB* vbReset (VB *sim);
|
||||
VBAPI int vbSetCartRAM (VB *sim, void *sram, uint32_t size);
|
||||
VBAPI int vbSetCartROM (VB *sim, void *rom, uint32_t size);
|
||||
VBAPI vbOnException vbSetExceptionCallback(VB *sim, vbOnException callback);
|
||||
VBAPI vbOnExecute vbSetExecuteCallback (VB *sim, vbOnExecute callback);
|
||||
VBAPI vbOnFetch vbSetFetchCallback (VB *sim, vbOnFetch callback);
|
||||
VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
|
||||
VBAPI int32_t vbSetProgramRegister (VB *sim, int index, int32_t value);
|
||||
VBAPI vbOnRead vbSetReadCallback (VB *sim, vbOnRead callback);
|
||||
VBAPI uint32_t vbSetSystemRegister (VB *sim, int index, uint32_t value);
|
||||
VBAPI void* vbSetUserData (VB *sim, void *tag);
|
||||
VBAPI vbOnWrite vbSetWriteCallback (VB *sim, vbOnWrite callback);
|
||||
VBAPI size_t vbSizeOf ();
|
||||
VBAPI int32_t vbWrite (VB *sim, uint32_t address, int type, int32_t value);
|
||||
VBAPI void vbReadEx (VB *sim, uint32_t address, uint8_t *buffer, uint32_t length);
|
||||
VBAPI void vbReset (VB *sim);
|
||||
VBAPI void* vbSetCallback(VB *sim, int id, void *proc);
|
||||
VBAPI int32_t vbSetRegister(VB *sim, int type, int id, int32_t value);
|
||||
VBAPI int vbSetROM (VB *sim, uint8_t *data, uint32_t size);
|
||||
VBAPI int vbSetSRAM (VB *sim, uint8_t *data, uint32_t size);
|
||||
VBAPI void vbWrite (VB *sim, uint32_t address, int type, int32_t value);
|
||||
VBAPI void vbWriteEx (VB *sim, uint32_t address, uint8_t *buffer, uint32_t length);
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2024 Guy Perfect
|
||||
Copyright (C) 2023 Guy Perfect
|
||||
|
||||
This software is provided 'as-is', without any express or implied
|
||||
warranty. In no event will the authors be held liable for any damages
|
||||
|
|
6
makefile
|
@ -1,7 +1,7 @@
|
|||
.PHONY: help
|
||||
help:
|
||||
@echo
|
||||
@echo "Virtual Boy Emulator - October 10, 2024"
|
||||
@echo "Virtual Boy Emulator - March 12, 2023"
|
||||
@echo
|
||||
@echo "Target build environment is any Debian with the following packages:"
|
||||
@echo " emscripten"
|
||||
|
@ -41,14 +41,14 @@ core:
|
|||
# GCC compilation control
|
||||
@gcc core/vb.c -I core -c -o /dev/null \
|
||||
-Werror -std=c90 -Wall -Wextra -Wpedantic \
|
||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE -D VB_DIV_GENERIC
|
||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE
|
||||
# Clang generic
|
||||
@emcc core/vb.c -I core -c -o /dev/null \
|
||||
-Werror -std=c90 -Wall -Wextra -Wpedantic
|
||||
# Clang compilation control
|
||||
@emcc core/vb.c -I core -c -o /dev/null \
|
||||
-Werror -std=c90 -Wall -Wextra -Wpedantic \
|
||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE -D VB_DIV_GENERIC
|
||||
-D VB_LITTLE_ENDIAN -D VB_SIGNED_PROPAGATE
|
||||
|
||||
.PHONY: wasm
|
||||
wasm:
|
||||
|
|
|
@ -0,0 +1,691 @@
|
|||
import { Core } from /**/"./core/Core.js";
|
||||
import { Debugger } from /**/"./debugger/Debugger.js";
|
||||
import { Disassembler } from /**/"./core/Disassembler.js";
|
||||
import { Toolkit } from /**/"./toolkit/Toolkit.js";
|
||||
|
||||
// Front-end emulator application
|
||||
class App extends Toolkit.App {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(bundle) {
|
||||
super({
|
||||
style: {
|
||||
display : "grid",
|
||||
gridTemplateRows: "max-content auto"
|
||||
},
|
||||
visibility: true,
|
||||
visible : false
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.bundle = bundle;
|
||||
this.debugMode = true;
|
||||
this.dualMode = false;
|
||||
this.text = null;
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
||||
// Theme
|
||||
Object.assign(document.body.style, { margin:"0", overflow:"hidden" });
|
||||
this.stylesheet(/**/"web/theme/kiosk.css", false);
|
||||
this.stylesheet(/**/"web/theme/vbemu.css", false);
|
||||
this._theme = "auto";
|
||||
this.themes = {
|
||||
dark : this.stylesheet(/**/"web/theme/dark.css" ),
|
||||
light : this.stylesheet(/**/"web/theme/light.css" ),
|
||||
virtual: this.stylesheet(/**/"web/theme/virtual.css")
|
||||
};
|
||||
|
||||
// Watch for dark mode preference changes
|
||||
this.isDark = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
this.isDark.addEventListener("change", e=>this.onDark());
|
||||
this.onDark();
|
||||
|
||||
// Locales
|
||||
await this.addLocale(/**/"web/locale/en-US.json");
|
||||
for (let id of [].concat(navigator.languages, ["en-US"])) {
|
||||
if (this.setLocale(id))
|
||||
break;
|
||||
}
|
||||
this.setTitle("{app.title}", true);
|
||||
|
||||
// Element
|
||||
document.body.append(this.element);
|
||||
window.addEventListener("resize", e=>{
|
||||
this.element.style.height = window.innerHeight + "px";
|
||||
this.element.style.width = window.innerWidth + "px";
|
||||
});
|
||||
window.dispatchEvent(new Event("resize"));
|
||||
this.addEventListener("keydown", e=>this.onKeyDown(e));
|
||||
|
||||
// Menus
|
||||
this.menuBar = new Toolkit.MenuBar(this);
|
||||
this.menuBar.setLabel("{menu._}", true);
|
||||
this.add(this.menuBar);
|
||||
this.initFileMenu ();
|
||||
this.initEmulationMenu();
|
||||
this.initDebugMenu (0, this.debugMode);
|
||||
this.initDebugMenu (1, this.debugMode && this.dualMode);
|
||||
this.initThemeMenu ();
|
||||
|
||||
// Fallback for bubbled key events
|
||||
document.body.addEventListener("focusout", e=>this.onBlur(e));
|
||||
window .addEventListener("keydown" , e=>this.onKey (e));
|
||||
window .addEventListener("keyup" , e=>this.onKey (e));
|
||||
|
||||
// Temporary: Faux game mode display
|
||||
this.display = new Toolkit.Component(this, {
|
||||
class : "tk display",
|
||||
style : { position: "relative" },
|
||||
visible: !this.debugMode
|
||||
});
|
||||
this.image1 = new Toolkit.Component(this, { style: {
|
||||
background: "#000000",
|
||||
position : "absolute"
|
||||
}});
|
||||
this.display.add(this.image1);
|
||||
this.image2 = new Toolkit.Component(this, { style: {
|
||||
background: "#000000",
|
||||
position : "absolute"
|
||||
}});
|
||||
this.display.add(this.image2);
|
||||
this.display.addEventListener("resize", e=>this.onDisplay());
|
||||
this.add(this.display);
|
||||
|
||||
// Temporary: Faux debug mode display
|
||||
this.desktop = new Toolkit.Desktop(this, {
|
||||
visible: this.debugMode
|
||||
});
|
||||
this.add(this.desktop);
|
||||
|
||||
// Emulation core
|
||||
this.core = await new Core().init();
|
||||
let sims = (await this.core.create(2)).sims;
|
||||
this.core.onsubscription = (k,m)=>this.onSubscription(k, m);
|
||||
|
||||
// Debugging managers
|
||||
this.dasm = new Disassembler();
|
||||
this.debug = new Array(sims.length);
|
||||
for (let x = 0; x < sims.length; x++) {
|
||||
let dbg = this.debug[x] = new Debugger(this, sims[x], x);
|
||||
if (x == 0 && !this.dualMode) {
|
||||
dbg.cpu .substitute("#", "");
|
||||
dbg.memory.substitute("#", "");
|
||||
}
|
||||
this.desktop.add(dbg.cpu);
|
||||
this.desktop.add(dbg.memory);
|
||||
}
|
||||
|
||||
// Reveal the application
|
||||
this.visible = true;
|
||||
this.restoreFocus();
|
||||
|
||||
console.log(
|
||||
"CPU window shortcuts:\n" +
|
||||
" F11 Single step\n" +
|
||||
" F10 Run to next\n" +
|
||||
" Ctrl+B Toggle bytes column\n" +
|
||||
" Ctrl+F Fit columns\n" +
|
||||
" Ctrl+G Goto"
|
||||
);
|
||||
}
|
||||
|
||||
// Initialize File menu
|
||||
initFileMenu() {
|
||||
let bar = this.menuBar;
|
||||
let item = bar.file = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.file._}");
|
||||
bar.add(item);
|
||||
|
||||
let menu = item.menu = new Toolkit.Menu(this);
|
||||
|
||||
item = bar.file.loadROM0 = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.file.loadROM}");
|
||||
item.substitute("#", this.dualMode ? " 1" : "", false);
|
||||
item.addEventListener("action", e=>this.onLoadROM(0));
|
||||
menu.add(item);
|
||||
|
||||
item = bar.file.loadROM1 = new Toolkit.MenuItem(this,
|
||||
{ visible: this.dualMode });
|
||||
item.setText("{menu.file.loadROM}");
|
||||
item.substitute("#", " 2", false);
|
||||
item.addEventListener("action", e=>this.onLoadROM(1));
|
||||
menu.add(item);
|
||||
|
||||
item = bar.file.dualMode = new Toolkit.MenuItem(this,
|
||||
{ checked: this.dualMode, type: "checkbox" });
|
||||
item.setText("{menu.file.dualMode}");
|
||||
item.addEventListener("action", e=>this.onDualMode());
|
||||
menu.add(item);
|
||||
|
||||
item = bar.file.debugMode = new Toolkit.MenuItem(this,
|
||||
{ checked: this.debugMode, disabled: true, type: "checkbox" });
|
||||
item.setText("{menu.file.debugMode}");
|
||||
item.addEventListener("action", e=>this.onDebugMode());
|
||||
menu.add(item);
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
item = new Toolkit.MenuItem(this);
|
||||
item.setText("Export source...", false);
|
||||
item.addEventListener("action", ()=>this.bundle.save());
|
||||
menu.add(item);
|
||||
}
|
||||
|
||||
// Initialize Emulation menu
|
||||
initEmulationMenu() {
|
||||
let bar = this.menuBar;
|
||||
let item = bar.emulation = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.emulation._}");
|
||||
bar.add(item);
|
||||
|
||||
let menu = item.menu = new Toolkit.Menu(this);
|
||||
|
||||
item = bar.emulation.run =
|
||||
new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.emulation.run}");
|
||||
menu.add(item);
|
||||
|
||||
item = bar.emulation.reset0 = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.emulation.reset}");
|
||||
item.substitute("#", this.dualMode ? " 1" : "", false);
|
||||
item.addEventListener("action", e=>this.onReset(0));
|
||||
menu.add(item);
|
||||
|
||||
item = bar.emulation.reset1 = new Toolkit.MenuItem(this,
|
||||
{ visible: this.dualMode });
|
||||
item.setText("{menu.emulation.reset}");
|
||||
item.substitute("#", " 2", false);
|
||||
item.addEventListener("action", e=>this.onReset(1));
|
||||
menu.add(item);
|
||||
|
||||
item = bar.emulation.linkSims = new Toolkit.MenuItem(this,
|
||||
{ disabled: true, type: "checkbox", visible: this.dualMode });
|
||||
item.setText("{menu.emulation.linkSims}");
|
||||
menu.add(item);
|
||||
}
|
||||
|
||||
// Initialize Debug menu
|
||||
initDebugMenu(index, visible) {
|
||||
let bar = this.menuBar;
|
||||
let item = bar["debug" + index] = new Toolkit.MenuItem(this,
|
||||
{ visible: visible }), top = item;
|
||||
item.setText("{menu.debug._}");
|
||||
item.substitute("#",
|
||||
index == 0 ? this.dualMode ? "1" : "" : " 2", false);
|
||||
bar.add(item);
|
||||
|
||||
let menu = item.menu = new Toolkit.Menu(this);
|
||||
|
||||
item = top.console = new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.debug.console}");
|
||||
menu.add(item);
|
||||
|
||||
item = top.memory = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.debug.memory}");
|
||||
item.addEventListener("action",
|
||||
e=>this.showWindow(this.debug[index].memory));
|
||||
menu.add(item);
|
||||
|
||||
item = top.cpu = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.debug.cpu}");
|
||||
item.addEventListener("action",
|
||||
e=>this.showWindow(this.debug[index].cpu));
|
||||
menu.add(item);
|
||||
|
||||
item=top.breakpoints = new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.debug.breakpoints}");
|
||||
menu.add(item);
|
||||
|
||||
menu.addSeparator();
|
||||
|
||||
item = top.palettes = new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.debug.palettes}");
|
||||
menu.add(item);
|
||||
|
||||
item = top.characters = new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.debug.characters}");
|
||||
menu.add(item);
|
||||
|
||||
item = top.bgMaps = new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.debug.bgMaps}");
|
||||
menu.add(item);
|
||||
|
||||
item = top.backgrounds = new Toolkit.MenuItem(this, { disabled:true });
|
||||
item.setText("{menu.debug.backgrounds}");
|
||||
menu.add(item);
|
||||
|
||||
item = top.objects = new Toolkit.MenuItem(this, { disabled: true });
|
||||
item.setText("{menu.debug.objects}");
|
||||
menu.add(item);
|
||||
|
||||
item = top.frameBuffers = new Toolkit.MenuItem(this, {disabled: true});
|
||||
item.setText("{menu.debug.frameBuffers}");
|
||||
menu.add(item);
|
||||
}
|
||||
|
||||
// Initialize Theme menu
|
||||
initThemeMenu() {
|
||||
let bar = this.menuBar;
|
||||
let item = bar.theme = new Toolkit.MenuItem(this);
|
||||
item.setText("{menu.theme._}");
|
||||
bar.add(item);
|
||||
|
||||
let menu = item.menu = new Toolkit.Menu(this);
|
||||
|
||||
item = bar.theme.auto = new Toolkit.MenuItem(this,
|
||||
{ checked: true, type: "checkbox" });
|
||||
item.setText("{menu.theme.auto}");
|
||||
item.theme = "auto";
|
||||
item.addEventListener("action", e=>this.theme = "auto");
|
||||
menu.add(item);
|
||||
|
||||
item = bar.theme.light = new Toolkit.MenuItem(this,
|
||||
{ checked: false, type: "checkbox" });
|
||||
item.setText("{menu.theme.light}");
|
||||
item.theme = "light";
|
||||
item.addEventListener("action", e=>this.theme = "light");
|
||||
menu.add(item);
|
||||
|
||||
item = bar.theme.dark = new Toolkit.MenuItem(this,
|
||||
{ checked: false, type: "checkbox" });
|
||||
item.setText("{menu.theme.dark}");
|
||||
item.theme = "dark";
|
||||
item.addEventListener("action", e=>this.theme = "dark");
|
||||
menu.add(item);
|
||||
|
||||
item = bar.theme.light = new Toolkit.MenuItem(this,
|
||||
{ checked: false, type: "checkbox" });
|
||||
item.setText("{menu.theme.virtual}");
|
||||
item.theme = "virtual";
|
||||
item.addEventListener("action", e=>this.theme = "virtual");
|
||||
menu.add(item);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// All elements have lost focus
|
||||
onBlur(e) {
|
||||
if (
|
||||
e.relatedTarget == null ||
|
||||
e.relatedTarget == document.body
|
||||
) this.restoreFocus();
|
||||
}
|
||||
|
||||
// Dark mode preference changed
|
||||
onDark() {
|
||||
if (this._theme != "auto")
|
||||
return;
|
||||
let isDark = this.isDark.matches;
|
||||
this.themes.light.disabled = isDark;
|
||||
this.themes.dark .disabled = !isDark;
|
||||
}
|
||||
|
||||
// Game mode display resized
|
||||
onDisplay() {
|
||||
let bounds = this.display.element.getBoundingClientRect();
|
||||
let width = Math.max(1, bounds.width);
|
||||
let height = Math.max(1, bounds.height);
|
||||
let scale, x1, y1, x2, y2;
|
||||
|
||||
// Single mode
|
||||
if (!this.dualMode) {
|
||||
this.image2.visible = false;
|
||||
scale = Math.max(1, Math.min(
|
||||
Math.floor(width / 384),
|
||||
Math.floor(height / 224)
|
||||
));
|
||||
x1 = Math.max(0, Math.floor((width - 384 * scale) / 2));
|
||||
y1 = Math.max(0, Math.floor((height - 224 * scale) / 2));
|
||||
x2 = y2 = 0;
|
||||
}
|
||||
|
||||
// Dual mode
|
||||
else {
|
||||
this.image2.visible = true;
|
||||
|
||||
// Horizontal orientation
|
||||
if (true) {
|
||||
scale = Math.max(1, Math.min(
|
||||
Math.floor(width / 768),
|
||||
Math.floor(height / 224)
|
||||
));
|
||||
let gap = Math.max(0, width - 768 * scale);
|
||||
gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0);
|
||||
x1 = gap;
|
||||
x2 = Math.max(384 * scale, width - 384 * scale - gap);
|
||||
y1 = y2 = Math.max(0, Math.floor((height - 224 * scale) / 2));
|
||||
}
|
||||
|
||||
// Vertical orientation
|
||||
else {
|
||||
scale = Math.max(1, Math.min(
|
||||
Math.floor(width / 384),
|
||||
Math.floor(height / 448)
|
||||
));
|
||||
let gap = Math.max(0, height - 448 * scale);
|
||||
gap = gap < 0 ? 0 : Math.floor(gap / 3) + (gap%3==2 ? 1 : 0);
|
||||
x1 = x2 = Math.max(0, Math.floor((width - 384 * scale) / 2));
|
||||
y1 = gap;
|
||||
y2 = Math.max(224 * scale, height - 224 * scale - gap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
width = 384 * scale + "px";
|
||||
height = 224 * scale + "px";
|
||||
Object.assign(this.image1.element.style,
|
||||
{ left: x1+"px", top: y1+"px", width: width, height: height });
|
||||
Object.assign(this.image2.element.style,
|
||||
{ left: x2+"px", top: y2+"px", width: width, height: height });
|
||||
}
|
||||
|
||||
// File -> Debug mode
|
||||
onDebugMode() {
|
||||
this.debugMode =!this.debugMode;
|
||||
this.display.visible =!this.debugMode;
|
||||
this.desktop.visible = this.debugMode;
|
||||
this.configMenus();
|
||||
this.onDisplay();
|
||||
}
|
||||
|
||||
// Emulation -> Dual mode
|
||||
onDualMode() {
|
||||
this.setDualMode(!this.dualMode);
|
||||
this.configMenus();
|
||||
this.onDisplay();
|
||||
}
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Take no action
|
||||
if (!e.altKey || e.key != "F10" ||
|
||||
this.menuBar.contains(document.activeElement))
|
||||
return;
|
||||
|
||||
// Move focus into the menu bar
|
||||
this.menuBar.focus();
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// File -> Load ROM
|
||||
async onLoadROM(index) {
|
||||
|
||||
// Add a file picker to the document
|
||||
let file = document.createElement("input");
|
||||
file.type = "file";
|
||||
file.style.position = "absolute";
|
||||
file.style.visibility = "hidden";
|
||||
document.body.appendChild(file);
|
||||
|
||||
// Prompt the user to select a file
|
||||
await new Promise(resolve=>{
|
||||
file.addEventListener("input", resolve);
|
||||
file.click();
|
||||
});
|
||||
file.remove();
|
||||
|
||||
// No file was selected
|
||||
file = file.files[0];
|
||||
if (!file)
|
||||
return;
|
||||
|
||||
// Load the file
|
||||
let data = null;
|
||||
try { data = new Uint8Array(await file.arrayBuffer()); }
|
||||
catch {
|
||||
alert(this.localize("{menu.file.loadROMError}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to process the file as an ISX binary
|
||||
try { data = Debugger.isx(data).toROM(); } catch { }
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
data.length < 1024 ||
|
||||
data.length > 0x1000000 ||
|
||||
(data.length & data.length - 1)
|
||||
) {
|
||||
alert(this.localize("{menu.file.loadROMInvalid}"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the ROM into simulation memory
|
||||
let rep = await this.core.setROM(this.debug[index].sim, data,
|
||||
{ refresh: true });
|
||||
if (!rep.success) {
|
||||
alert(this.localize("{menu.file.loadROMError}"));
|
||||
return;
|
||||
}
|
||||
this.debug[index].followPC(0xFFFFFFF0);
|
||||
}
|
||||
|
||||
// Emulation -> Reset
|
||||
async onReset(index) {
|
||||
await this.core.reset(this.debug[index].sim, { refresh: true });
|
||||
this.debug[index].followPC(0xFFFFFFF0);
|
||||
}
|
||||
|
||||
// Core subscription
|
||||
onSubscription(key, msg) {
|
||||
let target = this.debug; // Handler object
|
||||
for (let x = 1; x < key.length - 1; x++)
|
||||
target = target[key[x]];
|
||||
target[key[key.length - 1]].call(target, msg);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify document title
|
||||
setTitle(title, localize) {
|
||||
this.setString("text", title, localize);
|
||||
}
|
||||
|
||||
// Specify the color theme
|
||||
get theme() { return this._theme; }
|
||||
set theme(theme) {
|
||||
switch (theme) {
|
||||
case "light": case "dark": case "virtual":
|
||||
this._theme = theme;
|
||||
for (let entry of Object.entries(this.themes))
|
||||
entry[1].disabled = entry[0] != theme;
|
||||
break;
|
||||
default:
|
||||
this._theme = "auto";
|
||||
this.themes["virtual"].disabled = true;
|
||||
this.onDark();
|
||||
}
|
||||
for (let item of this.menuBar.theme.menu.children)
|
||||
item.checked = item.theme == theme;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Configure components for automatic localization, or localize a message
|
||||
localize(a, b) {
|
||||
|
||||
// Default behavior
|
||||
if (a && a != this)
|
||||
return super.localize(a, b);
|
||||
|
||||
// Update localization strings
|
||||
if (this.text != null) {
|
||||
let text = this.text;
|
||||
document.title = !text[1] ? text[0] :
|
||||
this.localize(text[0], this.substitutions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Return focus to the most recent focused element
|
||||
restoreFocus() {
|
||||
|
||||
// Focus was successfully restored
|
||||
if (super.restoreFocus())
|
||||
return true;
|
||||
|
||||
// Select the foremost visible window
|
||||
let wnd = this.desktop.getActiveWindow();
|
||||
if (wnd) {
|
||||
wnd.focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Select the menu bar
|
||||
this.menuBar.focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Perform a Run Next command on one of the simulations
|
||||
runToNext(index, options) {
|
||||
let debugs = [ this.debug[index] ];
|
||||
if (this.dualMode)
|
||||
debugs.push(this.debug[index ^ 1]);
|
||||
|
||||
let ret = this.core.runToNext(debugs.map(d=>d.sim), options);
|
||||
|
||||
if (ret instanceof Promise) ret.then(msg=>{
|
||||
for (let x = 0; x < debugs.length; x++)
|
||||
debugs[x].followPC(msg.pcs[x]);
|
||||
});
|
||||
}
|
||||
|
||||
// Perform a Single Step command on one of the simulations
|
||||
singleStep(index, options) {
|
||||
let debugs = [ this.debug[index] ];
|
||||
if (this.dualMode)
|
||||
debugs.push(this.debug[index ^ 1]);
|
||||
|
||||
let ret = this.core.singleStep(debugs.map(d=>d.sim), options);
|
||||
|
||||
if (ret instanceof Promise) ret.then(msg=>{
|
||||
for (let x = 0; x < debugs.length; x++)
|
||||
debugs[x].followPC(msg.pcs[x]);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Configure menu item visibility
|
||||
configMenus() {
|
||||
let bar = this.menuBar;
|
||||
bar.file.debugMode .checked = this.debugMode;
|
||||
bar.file.loadROM1 .visible = this.dualMode;
|
||||
bar.emulation.reset1 .visible = this.dualMode;
|
||||
bar.emulation.linkSims.visible = this.dualMode;
|
||||
bar.debug0 .visible = this.debugMode;
|
||||
bar.debug1 .visible = this.debugMode && this.dualMode;
|
||||
}
|
||||
|
||||
// Specify whether dual mode is active
|
||||
setDualMode(dualMode) {
|
||||
|
||||
// Update state
|
||||
if (dualMode == this.dualMode)
|
||||
return;
|
||||
this.dualMode = dualMode;
|
||||
|
||||
// Working variables
|
||||
let index = dualMode ? " 1" : "";
|
||||
|
||||
// Update menus
|
||||
let bar = this.menuBar;
|
||||
bar.file.loadROM0 .substitute("#", index, false);
|
||||
bar.debug0 .substitute("#", index, false);
|
||||
bar.emulation.reset0.substitute("#", index, false);
|
||||
bar.file.dualMode .checked = this.dualMode;
|
||||
|
||||
// Update sim 1 debug windows
|
||||
let dbg = this.debug[0];
|
||||
dbg.cpu .substitute("#", index);
|
||||
dbg.memory.substitute("#", index);
|
||||
|
||||
// Re-show any sim 2 debug windows that were previously visible
|
||||
if (dualMode) {
|
||||
for (let wnd of this.desktop.children) {
|
||||
if (wnd.index == 1 && wnd.wasVisible)
|
||||
wnd.visible = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide any visible sim 2 debug windows
|
||||
else for (let wnd of this.desktop.children) {
|
||||
if (wnd.index == 0)
|
||||
continue;
|
||||
wnd.wasVisible = wnd.visible;
|
||||
wnd.visible = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Ensure a debugger window is visible
|
||||
showWindow(wnd) {
|
||||
let adjust = false;
|
||||
|
||||
// The window is already visible
|
||||
if (wnd.visible) {
|
||||
|
||||
// The window is already in the foreground
|
||||
if (wnd == this.desktop.getActiveWindow())
|
||||
;//adjust = true;
|
||||
|
||||
// Bring the window to the foreground
|
||||
else this.desktop.bringToFront(wnd);
|
||||
|
||||
}
|
||||
|
||||
// The window is not visible
|
||||
else {
|
||||
adjust = !wnd.shown;
|
||||
if (adjust && wnd.firstShow)
|
||||
wnd.firstShow();
|
||||
wnd.visible = true;
|
||||
this.desktop.bringToFront(wnd);
|
||||
}
|
||||
|
||||
// Adjust the window position
|
||||
if (adjust) {
|
||||
let bounds = this.desktop.element.getBoundingClientRect();
|
||||
wnd.left = Math.max(0, (bounds.width - wnd.outerWidth ) / 2);
|
||||
wnd.top = Math.max(0, (bounds.height - wnd.outerHeight) / 2);
|
||||
}
|
||||
|
||||
// Transfer focus into the window
|
||||
wnd.focus();
|
||||
}
|
||||
|
||||
// Install a stylesheet. Returns the resulting <link> element
|
||||
stylesheet(filename, disabled = true) {
|
||||
let ret = document.createElement("link");
|
||||
ret.href = filename;
|
||||
ret.rel = "stylesheet";
|
||||
ret.type = "text/css";
|
||||
ret.disabled = disabled;
|
||||
document.head.append(ret);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Program Methods /////////////////////////////
|
||||
|
||||
// Program entry point
|
||||
static main(bundle) {
|
||||
new App(bundle).init();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { App };
|
|
@ -0,0 +1,203 @@
|
|||
import java.awt.image.*;
|
||||
import java.io.*;
|
||||
import java.nio.charset.*;
|
||||
import java.util.*;
|
||||
import javax.imageio.*;
|
||||
|
||||
public class Bundle {
|
||||
|
||||
/////////////////////////////// BundledFile ///////////////////////////////
|
||||
|
||||
// Individual packaged resource file
|
||||
static class BundledFile implements Comparable<BundledFile> {
|
||||
|
||||
// Instance fields
|
||||
byte[] data; // File data loaded from disk
|
||||
File file; // Source file
|
||||
String filename; // Logical filename
|
||||
|
||||
// Constructor
|
||||
BundledFile(BundledFile parent, File file) {
|
||||
|
||||
// Configure instance fields
|
||||
this.file = file;
|
||||
filename = parent == null ? "" : parent.filename + file.getName();
|
||||
|
||||
// Load file data if file
|
||||
if (file.isFile()) {
|
||||
try (var stream = new FileInputStream(file)) {
|
||||
data = stream.readAllBytes();
|
||||
} catch (Exception e) { }
|
||||
}
|
||||
|
||||
// Update filename if directory
|
||||
else if (parent != null) filename += "/";
|
||||
}
|
||||
|
||||
// Comparator
|
||||
public int compareTo(BundledFile o) {
|
||||
return
|
||||
filename.equals("web/_boot.js") ? -1 :
|
||||
o.filename.equals("web/_boot.js") ? +1 :
|
||||
filename.compareTo(o.filename)
|
||||
;
|
||||
}
|
||||
|
||||
// Produce a list of child files or directories
|
||||
BundledFile[] listFiles(String name, boolean isDirectory) {
|
||||
|
||||
// Produce a filtered list of files
|
||||
var files = this.file.listFiles(f->{
|
||||
|
||||
// Does not satisfy the directory requirement
|
||||
if (f.isDirectory() != isDirectory)
|
||||
return false;
|
||||
|
||||
// Current directory is not root
|
||||
if (!filename.equals(""))
|
||||
return true;
|
||||
|
||||
// Filter specific files from being bundled
|
||||
String filename = f.getName();
|
||||
return !(
|
||||
filename.startsWith(".git" ) ||
|
||||
filename.startsWith(name + "_") &&
|
||||
filename.endsWith (".html" )
|
||||
);
|
||||
});
|
||||
|
||||
// Process all files for bundling
|
||||
var ret = new BundledFile[files.length];
|
||||
for (int x = 0; x < files.length; x++)
|
||||
ret[x] = new BundledFile(this, files[x]);
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Program Methods /////////////////////////////
|
||||
|
||||
// Program entry point
|
||||
public static void main(String[] args) {
|
||||
String name = name(args[0]);
|
||||
var files = listFiles(args[0]);
|
||||
var prepend = prepend(name, files);
|
||||
var bundle = bundle(prepend, files);
|
||||
var image = image(bundle);
|
||||
var url = url(image);
|
||||
patch(name, url);
|
||||
}
|
||||
|
||||
// Produce a buffer of the bundled files
|
||||
static byte[] bundle(byte[] prepend, BundledFile[] files) {
|
||||
try (var stream = new ByteArrayOutputStream()) {
|
||||
stream.write(prepend);
|
||||
stream.write(files[0].data); // web/_boot.js
|
||||
stream.write(0);
|
||||
for (int x = 1; x < files.length; x++)
|
||||
stream.write(files[x].data);
|
||||
return stream.toByteArray();
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
// Convert a bundle buffer into a PNG-encoded image buffer
|
||||
static byte[] image(byte[] bundle) {
|
||||
int width = (int) Math.ceil(Math.sqrt(bundle.length));
|
||||
int height = (bundle.length + width - 1) / width;
|
||||
var pixels = new int[width * height];
|
||||
|
||||
// Encode the buffer as a pixel array
|
||||
for (int x = 0; x < bundle.length; x++) {
|
||||
int b = bundle[x] & 0xFF;
|
||||
pixels[x] = 0xFF000000 | b << 16 | b << 8 | b;
|
||||
}
|
||||
|
||||
// Produce an image using the pixels
|
||||
var image = new BufferedImage(
|
||||
width, height, BufferedImage.TYPE_INT_RGB);
|
||||
image.setRGB(0, 0, width, height, pixels, 0, width);
|
||||
|
||||
// Encode the image as a PNG buffer
|
||||
try (var stream = new ByteArrayOutputStream()) {
|
||||
ImageIO.write(image, "png", stream);
|
||||
return stream.toByteArray();
|
||||
} catch (Exception e) { return null; }
|
||||
}
|
||||
|
||||
// List all files
|
||||
static BundledFile[] listFiles(String name) {
|
||||
var dirs = new ArrayList<BundledFile>();
|
||||
var files = new ArrayList<BundledFile>();
|
||||
|
||||
// Propagate down the file system tree
|
||||
dirs.add(new BundledFile(null, new File(".")));
|
||||
while (!dirs.isEmpty()) {
|
||||
var dir = dirs.remove(0);
|
||||
for (var sub : dir.listFiles(name, true ))
|
||||
dirs.add(sub );
|
||||
for (var file : dir.listFiles(name, false))
|
||||
files.add(file);
|
||||
}
|
||||
|
||||
// Return the set of files as a sorted array
|
||||
Collections.sort(files);
|
||||
return files.toArray(new BundledFile[files.size()]);
|
||||
}
|
||||
|
||||
// Generate a filename for the bundle
|
||||
static String name(String name) {
|
||||
var calendar = Calendar.getInstance();
|
||||
return String.format("%s_%04d%02d%02d",
|
||||
name,
|
||||
calendar.get(Calendar.YEAR),
|
||||
calendar.get(Calendar.MONTH) + 1,
|
||||
calendar.get(Calendar.DAY_OF_MONTH)
|
||||
);
|
||||
}
|
||||
|
||||
// Produce the output HTML from the template
|
||||
static void patch(String name, String url) {
|
||||
String markup = null;
|
||||
try (var stream = new FileInputStream("web/template.html")) {
|
||||
markup = new String(stream.readAllBytes(), StandardCharsets.UTF_8);
|
||||
} catch (Exception e) { }
|
||||
markup = markup.replace("src=\"\"", "src=\"" + url + "\"");
|
||||
try (var stream = new FileOutputStream(name + ".html")) {
|
||||
stream.write(markup.getBytes(StandardCharsets.UTF_8));
|
||||
} catch (Exception e) { }
|
||||
}
|
||||
|
||||
// Produce source data to prepend to web/_boot.js
|
||||
static byte[] prepend(String name, BundledFile[] files) {
|
||||
var ret = new StringBuilder();
|
||||
|
||||
// Arguments
|
||||
ret.append("let " +
|
||||
"buffer=arguments[0]," +
|
||||
"image=arguments[1]," +
|
||||
"name=\"" + name + "\"" +
|
||||
";");
|
||||
|
||||
// Bundle manifest
|
||||
ret.append("let manifest=[");
|
||||
for (var file : files) {
|
||||
if (file != files[0])
|
||||
ret.append(",");
|
||||
ret.append("[\"" +
|
||||
file.filename + "\", " + file.data.length + "]");
|
||||
}
|
||||
ret.append("];");
|
||||
|
||||
// Convert to byte array
|
||||
return ret.toString().getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
// Convert an image buffer to a data URL
|
||||
static String url(byte[] image) {
|
||||
return "data:image/png;base64," +
|
||||
Base64.getMimeEncoder(0, new byte[0]).encodeToString(image);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,310 @@
|
|||
// Running as an async function
|
||||
// Prepended by Bundle.java: buffer, image, manifest, name
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Bundle //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Resource manager for bundled files
|
||||
class Bundle extends Array {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// .ZIP support
|
||||
static CRC_LOOKUP = new Uint32Array(256);
|
||||
|
||||
// Text processing
|
||||
static DECODER = new TextDecoder();
|
||||
static ENCODER = new TextEncoder();
|
||||
|
||||
static initializer() {
|
||||
|
||||
// Generate the CRC32 lookup table
|
||||
for (let x = 0; x <= 255; x++) {
|
||||
let l = x;
|
||||
for (let j = 7; j >= 0; j--)
|
||||
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
|
||||
this.CRC_LOOKUP[x] = l;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(name, url, settings, isDebug) {
|
||||
super();
|
||||
this.isDebug = isDebug;
|
||||
this.name = name;
|
||||
this.settings = settings;
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Export all bundled resources to a .ZIP file
|
||||
save() {
|
||||
let centrals = new Array(this.length);
|
||||
let locals = new Array(this.length);
|
||||
let offset = 0;
|
||||
let size = 0;
|
||||
|
||||
// Encode file and directory entries
|
||||
for (let x = 0; x < this.length; x++) {
|
||||
let file = this[x];
|
||||
let sum = Bundle.crc32(file.data);
|
||||
locals [x] = file.toZipHeader(sum);
|
||||
centrals[x] = file.toZipHeader(sum, offset);
|
||||
offset += locals [x].length;
|
||||
size += centrals[x].length;
|
||||
}
|
||||
|
||||
// Encode end of central directory
|
||||
let end = [];
|
||||
Bundle.writeInt(end, 4, 0x06054B50); // Signature
|
||||
Bundle.writeInt(end, 2, 0); // Disk number
|
||||
Bundle.writeInt(end, 2, 0); // Central dir start disk
|
||||
Bundle.writeInt(end, 2, this.length); // # central dir this disk
|
||||
Bundle.writeInt(end, 2, this.length); // # central dir total
|
||||
Bundle.writeInt(end, 4, size); // Size of central dir
|
||||
Bundle.writeInt(end, 4, offset); // Offset of central dir
|
||||
Bundle.writeInt(end, 2, 0); // .ZIP comment length
|
||||
|
||||
// Prompt the user to save the resulting file
|
||||
let a = document.createElement("a");
|
||||
a.download = this.name + ".zip";
|
||||
a.href = URL.createObjectURL(new Blob(
|
||||
locals.concat(centrals).concat([Uint8Array.from(end)]),
|
||||
{ type: "application/zip" }
|
||||
));
|
||||
a.style.visibility = "hidden";
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Add a BundledFile to the collection
|
||||
add(file) {
|
||||
file.bundle = this;
|
||||
this.push(this[file.name] = file);
|
||||
}
|
||||
|
||||
// Write a byte array into an output buffer
|
||||
static writeBytes(data, bytes) {
|
||||
for (let b of bytes)
|
||||
data.push(b);
|
||||
}
|
||||
|
||||
// Write an integer into an output buffer
|
||||
static writeInt(data, size, value) {
|
||||
for (; size > 0; size--, value >>= 8)
|
||||
data.push(value & 0xFF);
|
||||
}
|
||||
|
||||
// Write a string of text as bytes into an output buffer
|
||||
static writeString(data, text) {
|
||||
this.writeBytes(data, this.ENCODER.encode(text));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Calculate the CRC32 checksum for a byte array
|
||||
static crc32(data) {
|
||||
let c = 0xFFFFFFFF;
|
||||
for (let x = 0; x < data.length; x++)
|
||||
c = ((c >>> 8) ^ this.CRC_LOOKUP[(c ^ data[x]) & 0xFF]);
|
||||
return ~c & 0xFFFFFFFF;
|
||||
}
|
||||
|
||||
}
|
||||
Bundle.initializer();
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// BundledFile //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Individual file in the bundled data
|
||||
class BundledFile {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// MIME types
|
||||
static MIMES = {
|
||||
".css" : "text/css;charset=UTF-8",
|
||||
".frag" : "text/plain;charset=UTF-8",
|
||||
".js" : "text/javascript;charset=UTF-8",
|
||||
".json" : "application/json;charset=UTF-8",
|
||||
".png" : "image/png",
|
||||
".svg" : "image/svg+xml;charset=UTF-8",
|
||||
".txt" : "text/plain;charset=UTF-8",
|
||||
".vert" : "text/plain;charset=UTF-8",
|
||||
".wasm" : "application/wasm",
|
||||
".woff2": "font/woff2"
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(name, buffer, offset, length) {
|
||||
|
||||
// Configure instance fields
|
||||
this.data = buffer.slice(offset, offset + length);
|
||||
this.name = name;
|
||||
|
||||
// Resolve the MIME type
|
||||
let index = name.lastIndexOf(".");
|
||||
this.mime = index != -1 && BundledFile.MIMES[name.substring(index)] ||
|
||||
"application/octet-stream";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Represent the file with a blob URL
|
||||
toBlobURL() {
|
||||
return this.blobURL || (this.blobURL = URL.createObjectURL(
|
||||
new Blob([ this.data ], { type: this.mime })));
|
||||
}
|
||||
|
||||
// Encode the file data as a data URL
|
||||
toDataURL() {
|
||||
return "data:" + this.mime + ";base64," + btoa(
|
||||
Array.from(this.data).map(b=>String.fromCharCode(b)).join(""));
|
||||
}
|
||||
|
||||
// Pre-process URLs in a bundled file's contents
|
||||
toProcURL(asDataURL = false) {
|
||||
|
||||
// The URL has already been computed
|
||||
if (this.url)
|
||||
return this.url;
|
||||
|
||||
// Working variables
|
||||
let content = this.toString();
|
||||
let pattern = /\/\*\*?\*\//g;
|
||||
let parts = content.split(pattern);
|
||||
let ret = [ parts.shift() ];
|
||||
|
||||
// Process all URLs prefixed with /**/ or /***/
|
||||
for (let part of parts) {
|
||||
let start = part.indexOf("\"");
|
||||
let end = part.indexOf("\"", start + 1);
|
||||
let filename = part.substring(start + 1, end);
|
||||
let asData = pattern.exec(content)[0] == "/***/";
|
||||
|
||||
// Relative to current file
|
||||
if (filename.startsWith(".")) {
|
||||
let path = this.name.split("/");
|
||||
path.pop(); // Current filename
|
||||
|
||||
// Navigate to the path of the target file
|
||||
for (let dir of filename.split("/")) {
|
||||
switch (dir) {
|
||||
case "..": path.pop(); // Fallthrough
|
||||
case "." : break;
|
||||
default : path.push(dir);
|
||||
}
|
||||
}
|
||||
|
||||
// Produce the fully-qualified filename
|
||||
filename = path.join("/");
|
||||
}
|
||||
|
||||
// Append the file as a data URL
|
||||
let file = this.bundle[filename];
|
||||
ret.push(
|
||||
part.substring(0, start + 1),
|
||||
file[
|
||||
file.mime.startsWith("text/javascript") ||
|
||||
file.mime.startsWith("text/css") ?
|
||||
"toProcURL" : asData ? "toDataURL" : "toBlobURL"
|
||||
](asData),
|
||||
part.substring(end)
|
||||
);
|
||||
}
|
||||
|
||||
// Represent the transformed source as a URL
|
||||
return this.url = asDataURL ?
|
||||
"data:" + this.mime + ";base64," + btoa(ret.join("")) :
|
||||
URL.createObjectURL(new Blob(ret, { type: this.mime }))
|
||||
;
|
||||
}
|
||||
|
||||
// Decode the file data as a UTF-8 string
|
||||
toString() {
|
||||
return Bundle.DECODER.decode(this.data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Produce a .ZIP header for export
|
||||
toZipHeader(crc32, offset) {
|
||||
let central = offset !== undefined;
|
||||
let ret = [];
|
||||
if (central) {
|
||||
Bundle.writeInt (ret, 4, 0x02014B50); // Signature
|
||||
Bundle.writeInt (ret, 2, 20); // Version created by
|
||||
} else
|
||||
Bundle.writeInt (ret, 4, 0x04034B50); // Signature
|
||||
Bundle.writeInt (ret, 2, 20); // Version required
|
||||
Bundle.writeInt (ret, 2, 0); // Bit flags
|
||||
Bundle.writeInt (ret, 2, 0); // Compression method
|
||||
Bundle.writeInt (ret, 2, 0); // Modified time
|
||||
Bundle.writeInt (ret, 2, 0); // Modified date
|
||||
Bundle.writeInt (ret, 4, crc32); // Checksum
|
||||
Bundle.writeInt (ret, 4, this.data.length); // Compressed size
|
||||
Bundle.writeInt (ret, 4, this.data.length); // Uncompressed size
|
||||
Bundle.writeInt (ret, 2, this.name.length); // Filename length
|
||||
Bundle.writeInt (ret, 2, 0); // Extra field length
|
||||
if (central) {
|
||||
Bundle.writeInt (ret, 2, 0); // File comment length
|
||||
Bundle.writeInt (ret, 2, 0); // Disk number start
|
||||
Bundle.writeInt (ret, 2, 0); // Internal attributes
|
||||
Bundle.writeInt (ret, 4, 0); // External attributes
|
||||
Bundle.writeInt (ret, 4, offset); // Relative offset
|
||||
}
|
||||
Bundle.writeString (ret, this.name); // Filename
|
||||
if (!central)
|
||||
Bundle.writeBytes(ret, this.data); // File data
|
||||
return Uint8Array.from(ret);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Boot Program //
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// Produce the application bundle
|
||||
let bundle = new Bundle(name, image.src, image.getAttribute("settings") || "",
|
||||
location.protocol != "file:" && location.hash == "#debug");
|
||||
for (let x=0,offset=buffer.indexOf(0)-manifest[0][1]; x<manifest.length; x++) {
|
||||
let entry = manifest[x];
|
||||
bundle.add(new BundledFile(entry[0], buffer, offset, entry[1]));
|
||||
offset += entry[1] + (x == 0 ? 1 : 0);
|
||||
}
|
||||
|
||||
// Begin program operations
|
||||
(await import(bundle.isDebug ?
|
||||
"./web/App.js" :
|
||||
bundle["web/App.js"].toProcURL()
|
||||
)).App.main(bundle);
|
|
@ -0,0 +1,88 @@
|
|||
"use strict";
|
||||
|
||||
// Dedicated audio output thread
|
||||
class AudioThread extends AudioWorkletProcessor {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Configure instance fields
|
||||
this.buffers = []; // Input sample buffer queue
|
||||
this.offset = 0; // Offset into oldest buffer
|
||||
|
||||
// Wait for initializer message from parent thread
|
||||
this.port.onmessage = m=>this.init(m.data);
|
||||
}
|
||||
|
||||
async init(main) {
|
||||
|
||||
// Configure message ports
|
||||
this.core = this.port;
|
||||
this.core.onmessage = m=>this.onCore(m.data);
|
||||
this.main = main;
|
||||
this.main.onmessage = m=>this.onMain(m.data);
|
||||
|
||||
// Notify main thread
|
||||
this.port.postMessage(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Produce output samples (called by the user agent)
|
||||
process(inputs, outputs, parameters) {
|
||||
let output = outputs[0];
|
||||
let length = output [0].length;
|
||||
let empty = null;
|
||||
|
||||
// Process all samples
|
||||
for (let x = 0; x < length;) {
|
||||
|
||||
// No bufferfed samples are available
|
||||
if (this.buffers.length == 0) {
|
||||
for (; x < length; x++)
|
||||
output[0] = output[1] = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Transfer samples from the oldest buffer
|
||||
let y, buffer = this.buffers[0];
|
||||
for (y = this.offset; x < length && y < buffer.length; x++, y+=2) {
|
||||
output[0][x] = buffer[y ];
|
||||
output[1][x] = buffer[y + 1];
|
||||
}
|
||||
|
||||
// Advance to the next buffer
|
||||
if ((this.offset = y) == buffer.length) {
|
||||
if (empty == null)
|
||||
empty = [];
|
||||
empty.push(this.buffers.shift());
|
||||
this.offset = 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Return emptied sample buffers to the core thread
|
||||
if (empty != null)
|
||||
this.core.postMessage(empty, empty.map(e=>e.buffer));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Message Methods /////////////////////////////
|
||||
|
||||
// Message received from core thread
|
||||
onCore(msg) {
|
||||
}
|
||||
|
||||
// Message received from main thread
|
||||
onMain(msg) {
|
||||
}
|
||||
|
||||
}
|
||||
registerProcessor("AudioThread", AudioThread);
|
|
@ -0,0 +1,292 @@
|
|||
// Interface between application and WebAssembly worker thread
|
||||
class Core {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
/* Register types */
|
||||
static VB_PROGRAM = 0;
|
||||
static VB_SYSTEM = 1;
|
||||
static VB_OTHER = 2;
|
||||
|
||||
/* System registers */
|
||||
static VB_ADTRE = 25;
|
||||
static VB_CHCW = 24;
|
||||
static VB_ECR = 4;
|
||||
static VB_EIPC = 0;
|
||||
static VB_EIPSW = 1;
|
||||
static VB_FEPC = 2;
|
||||
static VB_FEPSW = 3;
|
||||
static VB_PIR = 6;
|
||||
static VB_PSW = 5;
|
||||
static VB_TKCW = 7;
|
||||
|
||||
/* Other registers */
|
||||
static VB_PC = 0;
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor() {
|
||||
|
||||
// Configure instance fields
|
||||
this.promises = [];
|
||||
|
||||
}
|
||||
|
||||
async init(coreUrl, wasmUrl, audioUrl) {
|
||||
|
||||
// Open audio output stream
|
||||
this.audio = new AudioContext({
|
||||
latencyHint: "interactive",
|
||||
sampleRate : 41700
|
||||
});
|
||||
await this.audio.suspend();
|
||||
|
||||
// Launch the audio thread
|
||||
await this.audio.audioWorklet.addModule(
|
||||
Core.url(audioUrl, "AudioThread.js", /***/"./AudioThread.js"));
|
||||
let node = new AudioWorkletNode(this.audio, "AudioThread", {
|
||||
numberOfInputs : 0,
|
||||
numberOfOutputs : 1,
|
||||
outputChannelCount: [2]
|
||||
});
|
||||
node.connect(this.audio.destination);
|
||||
|
||||
// Attach a second MessagePort to the audio thread
|
||||
let channel = new MessageChannel();
|
||||
this.audio.port = channel.port1;
|
||||
await new Promise(resolve=>{
|
||||
node.port.onmessage = resolve;
|
||||
node.port.postMessage(channel.port2, [channel.port2]);
|
||||
});
|
||||
this.audio.port.onmessage = m=>this.onAudio(m.data);
|
||||
|
||||
// Launch the core thread
|
||||
this.core = new Worker(
|
||||
Core.url(wasmUrl, "CoreThread.js", /***/"./CoreThread.js"));
|
||||
await new Promise(resolve=>{
|
||||
this.core.onmessage = resolve;
|
||||
this.core.postMessage({
|
||||
audio : node.port,
|
||||
wasmUrl: Core.url(wasmUrl, "core.wasm", /***/"./core.wasm")
|
||||
}, [node.port]);
|
||||
});
|
||||
this.core.onmessage = m=>this.onCore(m.data);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Select a URL in the same path as the current script
|
||||
static url(arg, name, bundled) {
|
||||
|
||||
// The input argument was provided
|
||||
if (arg)
|
||||
return arg;
|
||||
|
||||
// Running from a bundle distribution
|
||||
if (bundled.startsWith("blob:") || bundled.startsWith("data:"))
|
||||
return bundled;
|
||||
|
||||
// Compute the URL for the given filename
|
||||
let url = new URL(import.meta.url).pathname;
|
||||
return url.substring(0, url.lastIndexOf("/") + 1) + name;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Message received from audio thread
|
||||
onAudio(msg) {
|
||||
}
|
||||
|
||||
// Message received from core thread
|
||||
onCore(msg) {
|
||||
|
||||
// Process subscriptions
|
||||
if (msg.subscriptions && this.onsubscription instanceof Function) {
|
||||
for (let sub of msg.subscriptions) {
|
||||
let key = sub.subscription;
|
||||
delete sub.subscription;
|
||||
this.onsubscription(key, sub, this);
|
||||
}
|
||||
delete msg.subscriptions;
|
||||
}
|
||||
|
||||
// The main thread is waiting on a reply
|
||||
if (msg.isReply) {
|
||||
delete msg.isReply;
|
||||
|
||||
// For "create", produce sim objects
|
||||
if (msg.isCreate) {
|
||||
delete msg.isCreate;
|
||||
msg.sims = msg.sims.map(s=>({ pointer: s }));
|
||||
}
|
||||
|
||||
// Notify the caller
|
||||
this.promises.shift()(msg);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Create and initialize simulations
|
||||
create(count, options) {
|
||||
return this.message({
|
||||
command: "create",
|
||||
count : count
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Delete a simulation
|
||||
delete(sim, options) {
|
||||
return this.message({
|
||||
command: "delete",
|
||||
sim : sim.pointer
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Retrieve the value of all CPU registers
|
||||
getAllRegisters(sim, options) {
|
||||
return this.message({
|
||||
command: "getAllRegisters",
|
||||
sim : sim.pointer
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Retrieve the value of a register
|
||||
getRegister(sim, type, id, options) {
|
||||
return this.message({
|
||||
command: "getRegister",
|
||||
id : id,
|
||||
sim : sim.pointer,
|
||||
type : type
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Read multiple bytes from memory
|
||||
read(sim, address, length, options) {
|
||||
return this.message({
|
||||
command: "read",
|
||||
address: address,
|
||||
length : length,
|
||||
sim : sim.pointer
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Refresh subscriptions
|
||||
refresh(subscriptions = null, options) {
|
||||
return this.message({
|
||||
command : "refresh",
|
||||
subscriptions: subscriptions
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Simulate a hardware reset
|
||||
reset(sim, options) {
|
||||
return this.message({
|
||||
command: "reset",
|
||||
sim : sim.pointer
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Execute until the next current instruction
|
||||
runToNext(sims, options) {
|
||||
return this.message({
|
||||
command: "runToNext",
|
||||
sims : Array.isArray(sims) ?
|
||||
sims.map(s=>s.pointer) : [ sims.pointer ]
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Specify a value for a register
|
||||
setRegister(sim, type, id, value, options) {
|
||||
return this.message({
|
||||
command: "setRegister",
|
||||
id : id,
|
||||
sim : sim.pointer,
|
||||
type : type,
|
||||
value : value
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Specify a cartridge ROM buffer
|
||||
setROM(sim, data, options = {}) {
|
||||
data = data.slice();
|
||||
return this.message({
|
||||
command: "setROM",
|
||||
data : data,
|
||||
reset : !("reset" in options) || !!options.reset,
|
||||
sim : sim.pointer
|
||||
}, [data.buffer], options);
|
||||
}
|
||||
|
||||
// Execute the current instruction
|
||||
singleStep(sims, options) {
|
||||
return this.message({
|
||||
command: "singleStep",
|
||||
sims : Array.isArray(sims) ?
|
||||
sims.map(s=>s.pointer) : [ sims.pointer ]
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Cancel a subscription
|
||||
unsubscribe(subscription, options) {
|
||||
return this.message({
|
||||
command : "unsubscribe",
|
||||
subscription: subscription
|
||||
}, [], options);
|
||||
}
|
||||
|
||||
// Write multiple bytes to memory
|
||||
write(sim, address, data, options) {
|
||||
data = data.slice();
|
||||
return this.message({
|
||||
address: address,
|
||||
command: "write",
|
||||
data : data,
|
||||
sim : sim.pointer
|
||||
}, [data.buffer], options);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Send a message to the core thread
|
||||
message(msg, transfers, options = {}) {
|
||||
|
||||
// Configure options
|
||||
if (!(options instanceof Object))
|
||||
options = { reply: options };
|
||||
if (!("reply" in options) || options.reply)
|
||||
msg.reply = true;
|
||||
if ("refresh" in options)
|
||||
msg.refresh = options.refresh;
|
||||
if ("subscription" in options)
|
||||
msg.subscription = options.subscription;
|
||||
if ("tag" in options)
|
||||
msg.tag = options.tag;
|
||||
|
||||
// Send the command to the core thread
|
||||
return msg.reply ?
|
||||
new Promise(resolve=>{
|
||||
this.promises.push(resolve);
|
||||
this.core.postMessage(msg, transfers);
|
||||
}) :
|
||||
this.core.postMessage(msg, transfers);
|
||||
;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Core };
|
|
@ -0,0 +1,338 @@
|
|||
"use strict";
|
||||
|
||||
/* Register types */
|
||||
const VB_PROGRAM = 0;
|
||||
const VB_SYSTEM = 1;
|
||||
const VB_OTHER = 2;
|
||||
|
||||
/* System registers */
|
||||
const VB_ADTRE = 25;
|
||||
const VB_CHCW = 24;
|
||||
const VB_ECR = 4;
|
||||
const VB_EIPC = 0;
|
||||
const VB_EIPSW = 1;
|
||||
const VB_FEPC = 2;
|
||||
const VB_FEPSW = 3;
|
||||
const VB_PIR = 6;
|
||||
const VB_PSW = 5;
|
||||
const VB_TKCW = 7;
|
||||
|
||||
/* Other registers */
|
||||
const VB_PC = 0;
|
||||
|
||||
// Dedicated emulation thread
|
||||
class CoreThread {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor() {
|
||||
|
||||
// Configure instance fields
|
||||
this.subscriptions = new Map();
|
||||
|
||||
// Wait for initializer message from parent thread
|
||||
onmessage = m=>this.init(m.data.audio, m.data.wasmUrl);
|
||||
}
|
||||
|
||||
async init(audio, wasmUrl) {
|
||||
|
||||
// Configure message ports
|
||||
this.audio = audio;
|
||||
this.audio.onmessage = m=>this.onAudio (m.data);
|
||||
this.main = globalThis;
|
||||
this.main .onmessage = m=>this.onMessage(m.data);
|
||||
|
||||
// Load and instantiate the WebAssembly module
|
||||
this.wasm = (await WebAssembly.instantiateStreaming(
|
||||
fetch(wasmUrl), {
|
||||
env: { emscripten_notify_memory_growth: ()=>this.onGrowth() }
|
||||
})).instance;
|
||||
this.onGrowth();
|
||||
this.pointerSize = this.PointerSize();
|
||||
this.pointerType = this.pointerSize == 8 ? Uint64Array : Uint32Array;
|
||||
|
||||
// Notify main thread
|
||||
this.main.postMessage(0);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Message received from audio thread
|
||||
onAudio(frames) {
|
||||
|
||||
// Audio processing was suspended
|
||||
if (frames == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for more frames
|
||||
this.audio.postMessage(0);
|
||||
}
|
||||
|
||||
// Emscripten has grown the linear memory
|
||||
onGrowth() {
|
||||
Object.assign(this, this.wasm.exports);
|
||||
}
|
||||
|
||||
// Message received from main thread
|
||||
onMessage(msg) {
|
||||
|
||||
// Subscribe to the command
|
||||
if (msg.subscription && msg.command != "refresh")
|
||||
this.subscriptions.set(CoreThread.key(msg.subscription), msg);
|
||||
|
||||
// Process the command
|
||||
let rep = this[msg.command](msg);
|
||||
|
||||
// Do not send a reply
|
||||
if (!msg.reply)
|
||||
return;
|
||||
|
||||
// Configure the reply
|
||||
if (!rep)
|
||||
rep = {};
|
||||
if (msg.reply)
|
||||
rep.isReply = true;
|
||||
if ("tag" in msg)
|
||||
rep.tag = msg.tag;
|
||||
|
||||
// Send the reply to the main thread
|
||||
let transfers = rep.transfers;
|
||||
if (transfers)
|
||||
delete rep.transfers;
|
||||
this.main.postMessage(rep, transfers || []);
|
||||
|
||||
// Refresh subscriptions
|
||||
if (msg.refresh && msg.command != "refresh") {
|
||||
let subs = {};
|
||||
if (Array.isArray(msg.refresh))
|
||||
subs.subscriptions = msg.refresh;
|
||||
this.refresh(subs);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
//////////////////////////////// Commands /////////////////////////////////
|
||||
|
||||
// Create and initialize a new simulation
|
||||
create(msg) {
|
||||
let sims = new Array(msg.count);
|
||||
for (let x = 0; x < msg.count; x++)
|
||||
sims[x] = this.Create();
|
||||
return {
|
||||
isCreate: true,
|
||||
sims : sims
|
||||
};
|
||||
}
|
||||
|
||||
// Delete all memory used by a simulation
|
||||
delete(msg) {
|
||||
this.Delete(msg.sim);
|
||||
}
|
||||
|
||||
// Retrieve the values of all CPU registers
|
||||
getAllRegisters(msg) {
|
||||
let program = new Int32Array (32);
|
||||
let system = new Uint32Array(32);
|
||||
for (let x = 0; x < 32; x++) {
|
||||
program[x] = this.vbGetRegister(msg.sim, 0, x);
|
||||
system [x] = this.vbGetRegister(msg.sim, 1, x);
|
||||
}
|
||||
return {
|
||||
pc : this.vbGetRegister(msg.sim, 2, 0) >>> 0,
|
||||
program : program,
|
||||
system : system,
|
||||
transfers: [ program.buffer, system.buffer ]
|
||||
};
|
||||
}
|
||||
|
||||
// Retrieve the value of a register
|
||||
getRegister(msg) {
|
||||
let value = this.vbGetRegister(msg.sim, msg.type, msg.id);
|
||||
if (msg.type != VB_PROGRAM)
|
||||
value >>>= 0;
|
||||
return { value: value };
|
||||
}
|
||||
|
||||
// Read multiple bytes from memory
|
||||
read(msg) {
|
||||
let buffer = this.malloc(msg.length);
|
||||
this.vbReadEx(msg.sim, msg.address, buffer.pointer, msg.length);
|
||||
let data = buffer.slice();
|
||||
this.free(buffer);
|
||||
return {
|
||||
address : msg.address,
|
||||
data : data,
|
||||
transfers: [data.buffer]
|
||||
};
|
||||
}
|
||||
|
||||
// Process subscriptions
|
||||
refresh(msg) {
|
||||
let subscriptions = [];
|
||||
let transfers = [];
|
||||
|
||||
// Select the key set to refresh
|
||||
let keys = Array.isArray(msg.subscriptions) ?
|
||||
msg.subscriptions.map(s=>CoreThread.key(s)) :
|
||||
this.subscriptions.keys()
|
||||
;
|
||||
|
||||
// Process all subscriptions
|
||||
for (let key of keys) {
|
||||
|
||||
// Process the subscription
|
||||
let sub = this.subscriptions.get(key);
|
||||
let rep = this[sub.command](sub);
|
||||
|
||||
// There is no result
|
||||
if (!rep)
|
||||
continue;
|
||||
|
||||
// Add the result to the response
|
||||
rep.subscription = sub.subscription;
|
||||
if ("tag" in sub)
|
||||
rep.tag = sub.tag;
|
||||
subscriptions.push(rep);
|
||||
|
||||
// Add the transfers to the response
|
||||
if (!rep.transfers)
|
||||
continue;
|
||||
transfers = transfers.concat(rep.transfers);
|
||||
delete rep.transfers;
|
||||
}
|
||||
|
||||
// Send the response to the main thread
|
||||
if (subscriptions.length == 0)
|
||||
return;
|
||||
this.main.postMessage({
|
||||
subscriptions: subscriptions.sort(CoreThread.REFRESH_ORDER)
|
||||
}, transfers);
|
||||
}
|
||||
|
||||
// Simulate a hardware reset
|
||||
reset(msg) {
|
||||
this.vbReset(msg.sim);
|
||||
}
|
||||
|
||||
// Execute until the next current instruction
|
||||
runToNext(msg) {
|
||||
let sims = this.malloc(msg.sims.length, true);
|
||||
for (let x = 0; x < msg.sims.length; x++)
|
||||
sims[x] = msg.sims[x];
|
||||
this.RunToNext(sims.pointer, msg.sims.length);
|
||||
this.free(sims);
|
||||
|
||||
let pcs = new Array(msg.sims.length);
|
||||
for (let x = 0; x < msg.sims.length; x++)
|
||||
pcs[x] = this.vbGetRegister(msg.sims[x], 2, 0) >>> 0;
|
||||
|
||||
return { pcs: pcs };
|
||||
}
|
||||
|
||||
// Specify a value for a register
|
||||
setRegister(msg) {
|
||||
let value = this.vbSetRegister(msg.sim, msg.type, msg.id, msg.value);
|
||||
if (msg.type != VB_PROGRAM)
|
||||
value >>>= 0;
|
||||
return { value: value };
|
||||
}
|
||||
|
||||
// Specify a cartridge ROM buffer
|
||||
setROM(msg) {
|
||||
let prev = this.vbGetROM(msg.sim, 0);
|
||||
let success = true;
|
||||
|
||||
// Specify a new ROM
|
||||
if (msg.data != null) {
|
||||
let data = this.malloc(msg.data.length);
|
||||
for (let x = 0; x < data.length; x++)
|
||||
data[x] = msg.data[x];
|
||||
success = !this.vbSetROM(msg.sim, data.pointer, data.length);
|
||||
}
|
||||
|
||||
// Operation was successful
|
||||
if (success) {
|
||||
|
||||
// Delete the previous ROM
|
||||
this.Free(prev);
|
||||
|
||||
// Reset the simulation
|
||||
if (msg.reset)
|
||||
this.vbReset(msg.sim);
|
||||
}
|
||||
|
||||
return { success: success };
|
||||
}
|
||||
|
||||
// Execute the current instruction
|
||||
singleStep(msg) {
|
||||
let sims = this.malloc(msg.sims.length, true);
|
||||
for (let x = 0; x < msg.sims.length; x++)
|
||||
sims[x] = msg.sims[x];
|
||||
this.SingleStep(sims.pointer, msg.sims.length);
|
||||
this.free(sims);
|
||||
|
||||
let pcs = new Array(msg.sims.length);
|
||||
for (let x = 0; x < msg.sims.length; x++)
|
||||
pcs[x] = this.vbGetRegister(msg.sims[x], 2, 0) >>> 0;
|
||||
|
||||
return { pcs: pcs };
|
||||
}
|
||||
|
||||
// Delete a subscription
|
||||
unsubscribe(msg) {
|
||||
this.subscriptions.delete(CoreThread.key(msg.subscription));
|
||||
}
|
||||
|
||||
// Write multiple bytes to memory
|
||||
write(msg) {
|
||||
let data = this.malloc(msg.data.length);
|
||||
for (let x = 0; x < data.length; x++)
|
||||
data[x] = msg.data[x];
|
||||
this.vbWriteEx(msg.sim, msg.address, data.pointer, data.length);
|
||||
this.free(data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Delete a byte array in WebAssembly memory
|
||||
free(buffer) {
|
||||
this.Free(buffer.pointer);
|
||||
}
|
||||
|
||||
// Format a subscription key as a string
|
||||
static key(subscription) {
|
||||
return subscription.map(k=>k.toString()).join("\n");
|
||||
}
|
||||
|
||||
// Allocate a byte array in WebAssembly memory
|
||||
malloc(length, pointers = false) {
|
||||
let size = pointers ? length * this.pointerSize : length;
|
||||
return this.map(this.Malloc(size), length, pointers);
|
||||
}
|
||||
|
||||
// Map a typed array into WebAssembly memory
|
||||
map(address, length, pointers = false) {
|
||||
let ret = new (pointers ? this.pointerType : Uint8Array)
|
||||
(this.memory.buffer, address, length);
|
||||
ret.pointer = address;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Comparator for subscriptions within the refresh command
|
||||
static REFRESH_ORDER(a, b) {
|
||||
a = a.subscription[0];
|
||||
b = b.subscription[0];
|
||||
return a < b ? -1 : a > b ? 1 : 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
new CoreThread();
|
|
@ -0,0 +1,546 @@
|
|||
// Machine code to human readable text converter
|
||||
class Disassembler {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Default settings
|
||||
static DEFAULTS = {
|
||||
condCL : "L", // Use C/NC or L/NL for conditions
|
||||
condEZ : "E", // Use E/NE or Z/NZ for conditions
|
||||
condNames : true, // Use condition names
|
||||
condUppercase: false, // Condition names uppercase
|
||||
hexPrefix : "0x", // Hexadecimal prefix
|
||||
hexSuffix : "", // Hexadecimal suffix
|
||||
hexUppercase : true, // Hexadecimal uppercase
|
||||
instUppercase: true, // Mnemonics uppercase
|
||||
jumpAddress : true, // Jump/branch shows target address
|
||||
memInside : false, // Use [reg1 + disp] notation
|
||||
opDestFirst : false, // Destination operand first
|
||||
proNames : true, // Use program register names
|
||||
proUppercase : false, // Program register names uppercase
|
||||
splitBcond : false, // BCOND condition as an operand
|
||||
splitSetf : true, // SETF condition as an operand
|
||||
sysNames : true, // Use system register names
|
||||
sysUppercase : false // System register names uppercase
|
||||
};
|
||||
|
||||
|
||||
|
||||
/////////////////////////// Disassembly Lookup ////////////////////////////
|
||||
|
||||
// Opcode descriptors
|
||||
static OPDEFS = [
|
||||
[ "MOV" , [ "opReg1" , "opReg2" ] ], // 000000
|
||||
[ "ADD" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "SUB" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "CMP" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "SHL" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "SHR" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "JMP" , [ "opReg1Ind" ] ],
|
||||
[ "SAR" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "MUL" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "DIV" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "MULU" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "DIVU" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "OR" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "AND" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "XOR" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "NOT" , [ "opReg1" , "opReg2" ] ],
|
||||
[ "MOV" , [ "opImm5S", "opReg2" ] ], // 010000
|
||||
[ "ADD" , [ "opImm5S", "opReg2" ] ],
|
||||
null, // SETF: special
|
||||
[ "CMP" , [ "opImm5S", "opReg2" ] ],
|
||||
[ "SHL" , [ "opImm5U", "opReg2" ] ],
|
||||
[ "SHR" , [ "opImm5U", "opReg2" ] ],
|
||||
[ "CLI" , [ ] ],
|
||||
[ "SAR" , [ "opImm5U", "opReg2" ] ],
|
||||
[ "TRAP" , [ "opImm5U" ] ],
|
||||
[ "RETI" , [ ] ],
|
||||
[ "HALT" , [ ] ],
|
||||
null, // Invalid
|
||||
[ "LDSR" , [ "opReg2" , "opSys" ] ],
|
||||
[ "STSR" , [ "opSys" , "opReg2" ] ],
|
||||
[ "SEI" , [ ] ],
|
||||
null, // Bit string: special
|
||||
null, // BCOND: special // 100000
|
||||
null, // BCOND: special
|
||||
null, // BCOND: special
|
||||
null, // BCOND: special
|
||||
null, // BCOND: special
|
||||
null, // BCOND: special
|
||||
null, // BCOND: special
|
||||
null, // BCOND: special
|
||||
[ "MOVEA", [ "opImm16U" , "opReg1", "opReg2" ] ],
|
||||
[ "ADDI" , [ "opImm16S" , "opReg1", "opReg2" ] ],
|
||||
[ "JR" , [ "opDisp26" ] ],
|
||||
[ "JAL" , [ "opDisp26" ] ],
|
||||
[ "ORI" , [ "opImm16U" , "opReg1", "opReg2" ] ],
|
||||
[ "ANDI" , [ "opImm16U" , "opReg1", "opReg2" ] ],
|
||||
[ "XORI" , [ "opImm16U" , "opReg1", "opReg2" ] ],
|
||||
[ "MOVHI", [ "opImm16U" , "opReg1", "opReg2" ] ],
|
||||
[ "LD.B" , [ "opReg1Disp", "opReg2" ] ], // 110000
|
||||
[ "LD.H" , [ "opReg1Disp", "opReg2" ] ],
|
||||
null, // Invalid
|
||||
[ "LD.W" , [ "opReg1Disp", "opReg2" ] ],
|
||||
[ "ST.B" , [ "opReg2" , "opReg1Disp" ] ],
|
||||
[ "ST.H" , [ "opReg2" , "opReg1Disp" ] ],
|
||||
null, // Invalid
|
||||
[ "ST.W" , [ "opReg2" , "opReg1Disp" ] ],
|
||||
[ "IN.B" , [ "opReg1Disp", "opReg2" ] ],
|
||||
[ "IN.H" , [ "opReg1Disp", "opReg2" ] ],
|
||||
[ "CAXI" , [ "opReg1Disp", "opReg2" ] ],
|
||||
[ "IN.W" , [ "opReg1Disp", "opReg2" ] ],
|
||||
[ "OUT.B", [ "opReg2" , "opReg1Disp" ] ],
|
||||
[ "OUT.H", [ "opReg2" , "opReg1Disp" ] ],
|
||||
null, // Floating-point/Nintendo: special
|
||||
[ "OUT.W", [ "opReg2" , "opReg1Disp" ] ]
|
||||
];
|
||||
|
||||
// Bit string sub-opcode descriptors
|
||||
static BITSTRING = [
|
||||
"SCH0BSU", "SCH0BSD", "SCH1BSU", "SCH1BSD",
|
||||
null , null , null , null ,
|
||||
"ORBSU" , "ANDBSU" , "XORBSU" , "MOVBSU" ,
|
||||
"ORNBSU" , "ANDNBSU", "XORNBSU", "NOTBSU" ,
|
||||
null , null , null , null ,
|
||||
null , null , null , null ,
|
||||
null , null , null , null ,
|
||||
null , null , null , null
|
||||
];
|
||||
|
||||
// Floating-point/Nintendo sub-opcode descriptors
|
||||
static FLOATENDO = [
|
||||
[ "CMPF.S" , [ "opReg1", "opReg2" ] ],
|
||||
null, // Invalid
|
||||
[ "CVT.WS" , [ "opReg1", "opReg2" ] ],
|
||||
[ "CVT.SW" , [ "opReg1", "opReg2" ] ],
|
||||
[ "ADDF.S" , [ "opReg1", "opReg2" ] ],
|
||||
[ "SUBF.S" , [ "opReg1", "opReg2" ] ],
|
||||
[ "MULF.S" , [ "opReg1", "opReg2" ] ],
|
||||
[ "DIVF.S" , [ "opReg1", "opReg2" ] ],
|
||||
[ "XB" , [ "opReg2" ] ],
|
||||
[ "XH" , [ "opReg2" ] ],
|
||||
[ "REV" , [ "opReg1", "opReg2" ] ],
|
||||
[ "TRNC.SW", [ "opReg1", "opReg2" ] ],
|
||||
[ "MPYHW" , [ "opReg1", "opReg2" ] ],
|
||||
null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null,
|
||||
null, null, null, null, null, null, null, null
|
||||
];
|
||||
|
||||
// Condition mnemonics
|
||||
static CONDITIONS = [
|
||||
"V" , "C" , "E" , "NH", "N", "T", "LT", "LE",
|
||||
"NV", "NC", "NE", "H" , "P", "F", "GE", "GT"
|
||||
];
|
||||
|
||||
// Program register names
|
||||
static REG_PROGRAM = [
|
||||
"r0" , "r1" , "hp" , "sp" , "gp" , "tp" , "r6" , "r7" ,
|
||||
"r8" , "r9" , "r10", "r11", "r12", "r13", "r14", "r15",
|
||||
"r16", "r17", "r18", "r19", "r20", "r21", "r22", "r23",
|
||||
"r24", "r25", "r26", "r27", "r28", "r29", "r30", "lp"
|
||||
];
|
||||
|
||||
// System register names
|
||||
static REG_SYSTEM = [
|
||||
"EIPC", "EIPSW", "FEPC", "FEPSW", "ECR", "PSW", "PIR", "TKCW",
|
||||
"8" , "9" , "10" , "11" , "12" , "13" , "14" , "15" ,
|
||||
"16" , "17" , "18" , "19" , "20" , "21" , "22" , "23" ,
|
||||
"CHCW", "ADTRE", "26" , "27" , "28" , "29" , "30" , "31"
|
||||
];
|
||||
|
||||
// Other register names
|
||||
static REG_OTHER = [ "PC", "PSW" ];
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Determine the bounds of a data buffer to represent all lines of output
|
||||
static dataBounds(address, line, length) {
|
||||
let before = 10; // Number of lines before the first line of output
|
||||
let max = 4; // Maximum number of bytes that can appear on a line
|
||||
|
||||
// The reference line is before the preferred earliest line
|
||||
if (line < -before) {
|
||||
length = (length - line) * max;
|
||||
}
|
||||
|
||||
// The reference line is before the first line
|
||||
else if (line < 0) {
|
||||
address -= (line + before) * max;
|
||||
length = (length + before) * max;
|
||||
}
|
||||
|
||||
// The reference line is at or after the first line
|
||||
else {
|
||||
address -= (line + before) * max;
|
||||
length = (Math.max(length, line) + before) * max;
|
||||
}
|
||||
|
||||
return {
|
||||
address: (address & ~1) >>> 0,
|
||||
length : length
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor() {
|
||||
Object.assign(this, Disassembler.DEFAULTS);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Disassemble a region of memory
|
||||
disassemble(data, dataAddress, refAddress, refLine, length, pc = null) {
|
||||
let pcOffset = pc === null ? -1 : pc - dataAddress >>> 0;
|
||||
|
||||
// Locate the offset of the first line of output in the buffer
|
||||
let offset = 0;
|
||||
for (let
|
||||
addr = dataAddress,
|
||||
circle = refLine > 0 ? new Array(refLine) : null,
|
||||
index = 0,
|
||||
more = [],
|
||||
remain = null
|
||||
;;) {
|
||||
|
||||
// Determine the size of the current line
|
||||
if (more.length == 0)
|
||||
this.more(more, data, offset);
|
||||
let size = more.shift();
|
||||
|
||||
// The current line contains the reference address
|
||||
if (refAddress - addr >>> 0 < size) {
|
||||
|
||||
// The next item in the buffer is the first line of output
|
||||
if (refLine > 0) {
|
||||
offset = circle[index];
|
||||
break;
|
||||
}
|
||||
|
||||
// This line is the first line of output
|
||||
if (refLine == 0)
|
||||
break;
|
||||
|
||||
// Count more lines for the first line of output
|
||||
remain = refLine;
|
||||
}
|
||||
|
||||
// Record the offset of the current instruction
|
||||
if (refLine > 0) {
|
||||
circle[index] = offset;
|
||||
index = (index + 1) % circle.length;
|
||||
}
|
||||
|
||||
// Advance to the next line
|
||||
let sizeToPC = pcOffset - offset >>> 0;
|
||||
if (offset != pcOffset && sizeToPC < size) {
|
||||
size = sizeToPC;
|
||||
more.splice();
|
||||
}
|
||||
addr = addr + size >>> 0;
|
||||
offset += size;
|
||||
if (remain !== null && ++remain == 0)
|
||||
break; // The next line is the first line of output
|
||||
}
|
||||
|
||||
// Process all lines of output
|
||||
let lines = new Array(length);
|
||||
for (let
|
||||
addr = dataAddress + offset,
|
||||
more = [],
|
||||
x = 0;
|
||||
x < length; x++
|
||||
) {
|
||||
|
||||
// Determine the size of the current line
|
||||
if (more.length == 0)
|
||||
this.more(more, data, offset, pcOffset);
|
||||
let size = more.shift();
|
||||
|
||||
// Add the line to the response
|
||||
lines[x] = this.format({
|
||||
rawAddress: addr,
|
||||
rawBytes : data.slice(offset, offset + size)
|
||||
});
|
||||
|
||||
// Advance to the next line
|
||||
let sizeToPC = pcOffset - offset >>> 0;
|
||||
if (offset != pcOffset && sizeToPC < size) {
|
||||
size = sizeToPC;
|
||||
more.splice();
|
||||
}
|
||||
addr = addr + size >>> 0;
|
||||
offset += size;
|
||||
}
|
||||
|
||||
return lines;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/////////////////////////// Formatting Methods ////////////////////////////
|
||||
|
||||
// Format a line as human-readable text
|
||||
format(line) {
|
||||
let canReverse = true;
|
||||
let opcode = line.rawBytes[1] >>> 2;
|
||||
let opdef;
|
||||
let code = [
|
||||
line.rawBytes[1] << 8 | line.rawBytes[0],
|
||||
line.rawBytes.length == 2 ? null :
|
||||
line.rawBytes[3] << 8 | line.rawBytes[2]
|
||||
];
|
||||
|
||||
// BCOND
|
||||
if ((opcode & 0b111000) == 0b100000) {
|
||||
let cond = code[0] >>> 9 & 15;
|
||||
opdef =
|
||||
cond == 13 ? [ "NOP", [ ] ] :
|
||||
this.splitBcond ? [ "BCOND", [ "opBCond", "opDisp9" ] ] :
|
||||
[
|
||||
cond == 5 ? "BR" : "B" + this.condition(cond, true),
|
||||
[ "opDisp9" ]
|
||||
]
|
||||
;
|
||||
canReverse = false;
|
||||
}
|
||||
|
||||
// Processing by opcode
|
||||
else switch (opcode) {
|
||||
|
||||
// SETF
|
||||
case 0b010010:
|
||||
opdef = !this.splitSetf ?
|
||||
[
|
||||
"SETF" + Disassembler.CONDITIONS[code[0] & 15],
|
||||
[ "opReg2" ]
|
||||
] :
|
||||
[ "SETF", [ "opCond", "opReg2" ] ]
|
||||
;
|
||||
break;
|
||||
|
||||
// Bit string
|
||||
case 0b011111:
|
||||
opdef = Disassembler.BITSTRING[code[0] & 31];
|
||||
if (opdef != null)
|
||||
opdef = [ opdef, [] ];
|
||||
break;
|
||||
|
||||
// Floating-point/Nintendo
|
||||
case 0b111110:
|
||||
opdef = Disassembler.FLOATENDO[code[1] >>> 10];
|
||||
break;
|
||||
|
||||
// All others
|
||||
default: opdef = Disassembler.OPDEFS[opcode];
|
||||
}
|
||||
|
||||
// The opcode is undefined
|
||||
if (opdef == null)
|
||||
opdef = [ "---", [] ];
|
||||
|
||||
// Format the line's display text
|
||||
line.address = this.hex(line.rawAddress, 8, false);
|
||||
line.bytes = new Array(line.rawBytes.length);
|
||||
line.mnemonic = this.instUppercase ? opdef[0] : opdef[0].toLowerCase();
|
||||
line.operands = new Array(opdef[1].length);
|
||||
for (let x = 0; x < line.bytes.length; x++)
|
||||
line.bytes[x] = this.hex(line.rawBytes[x], 2, false);
|
||||
for (let x = 0; x < line.operands.length; x++)
|
||||
line.operands[x] = this[opdef[1][x]](line, code);
|
||||
if (this.opDestFirst && canReverse)
|
||||
line.operands.reverse();
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
// Format a condition operand in a BCOND instruction
|
||||
opBCond(line, code) {
|
||||
return this.condition(code[0] >>> 9 & 15);
|
||||
}
|
||||
|
||||
// Format a condition operand in a SETF instruction
|
||||
opCond(line, code) {
|
||||
return this.condition(code[0] & 15);
|
||||
}
|
||||
|
||||
// Format a 9-bit displacement operand
|
||||
opDisp9(line, code) {
|
||||
let disp = code[0] << 23 >> 23;
|
||||
return this.jump(line.rawAddress, disp);
|
||||
}
|
||||
|
||||
// Format a 26-bit displacement operand
|
||||
opDisp26(line, code) {
|
||||
let disp = (code[0] << 16 | code[1]) << 6 >> 6;
|
||||
return this.jump(line.rawAddress, disp);
|
||||
}
|
||||
|
||||
// Format a 5-bit signed immediate operand
|
||||
opImm5S(line, code) {
|
||||
return (code[0] & 31) << 27 >> 27;
|
||||
}
|
||||
|
||||
// Format a 5-bit unsigned immediate operand
|
||||
opImm5U(line, code) {
|
||||
return code[0] & 31;
|
||||
}
|
||||
|
||||
// Format a 16-bit signed immediate operand
|
||||
opImm16S(line, code) {
|
||||
let ret = code[1] << 16 >> 16;
|
||||
return (
|
||||
ret < -256 ? "-" + this.hex(-ret) :
|
||||
ret > 256 ? this.hex( ret) :
|
||||
ret
|
||||
);
|
||||
}
|
||||
|
||||
// Format a 16-bit unsigned immediate operand
|
||||
opImm16U(line, code) {
|
||||
return this.hex(code[1], 4);
|
||||
}
|
||||
|
||||
// Format a Reg1 operand
|
||||
opReg1(line, code) {
|
||||
return this.programRegister(code[0] & 31);
|
||||
}
|
||||
|
||||
// Format a disp[reg1] operand
|
||||
opReg1Disp(line, code) {
|
||||
let disp = code[1] << 16 >> 16;
|
||||
let reg1 = this.programRegister(code[0] & 31);
|
||||
|
||||
// Do not print the displacement
|
||||
if (disp == 0)
|
||||
return "[" + reg1 + "]";
|
||||
|
||||
// Format the displacement amount
|
||||
disp =
|
||||
disp < -256 ? "-" + this.hex(-disp) :
|
||||
disp > 256 ? this.hex( disp) :
|
||||
disp.toString()
|
||||
;
|
||||
|
||||
// [reg1 + disp] notation
|
||||
if (this.memInside) {
|
||||
return "[" + reg1 + (disp.startsWith("-") ?
|
||||
" - " + disp.substring(1) :
|
||||
" + " + disp
|
||||
) + "]";
|
||||
}
|
||||
|
||||
// disp[reg1] notation
|
||||
return disp + "[" + reg1 + "]";
|
||||
}
|
||||
|
||||
// Format a [Reg1] operand
|
||||
opReg1Ind(line, code) {
|
||||
return "[" + this.programRegister(code[0] & 31) + "]";
|
||||
}
|
||||
|
||||
// Format a Reg2 operand
|
||||
opReg2(line, code) {
|
||||
return this.programRegister(code[0] >> 5 & 31);
|
||||
}
|
||||
|
||||
// Format a system register operand
|
||||
opSys(line, code) {
|
||||
return this.systemRegister(code[0] & 31);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Select the mnemonic for a condition
|
||||
condition(index, forceUppercase = false) {
|
||||
if (!this.condNames)
|
||||
return index.toString();
|
||||
let ret =
|
||||
index == 1 ? this.condCL :
|
||||
index == 2 ? this.condEZ :
|
||||
index == 9 ? "N" + this.condCL :
|
||||
index == 10 ? "N" + this.condEZ :
|
||||
Disassembler.CONDITIONS[index]
|
||||
;
|
||||
if (!forceUppercase && !this.condUppercase)
|
||||
ret = ret.toLowerCase();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Format a number as a hexadecimal string
|
||||
hex(value, digits = null, decorated = true) {
|
||||
value = value.toString(16);
|
||||
if (this.hexUppercase)
|
||||
value = value.toUpperCase();
|
||||
if (digits != null)
|
||||
value = value.padStart(digits, "0");
|
||||
if (decorated) {
|
||||
value = this.hexPrefix + value + this.hexSuffix;
|
||||
if (this.hexPrefix == "" && "0123456789".indexOf(value[0]) == -1)
|
||||
value = "0" + value;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// Format a jump or branch destination
|
||||
jump(address, disp) {
|
||||
return (
|
||||
this.jumpAddress ?
|
||||
this.hex(address + disp >>> 0, 8, false) :
|
||||
disp < -256 ? "-" + this.hex(-disp) :
|
||||
disp > 256 ? "+" + this.hex( disp) :
|
||||
disp.toString()
|
||||
);
|
||||
}
|
||||
|
||||
// Determine the number of bytes in the next line(s) of disassembly
|
||||
more(more, data, offset) {
|
||||
|
||||
// Error checking
|
||||
if (offset + 1 >= data.length)
|
||||
throw new Error("Disassembly error: Unexpected EoF");
|
||||
|
||||
// Determine the instruction's size from its opcode
|
||||
let opcode = data[offset + 1] >>> 2;
|
||||
more.push(
|
||||
opcode < 0b101000 || // 16-bit instruction
|
||||
opcode == 0b110010 || // Illegal opcode
|
||||
opcode == 0b110110 // Illegal opcode
|
||||
? 2 : 4);
|
||||
}
|
||||
|
||||
// Format a program register
|
||||
programRegister(index) {
|
||||
let ret = this.proNames ?
|
||||
Disassembler.REG_PROGRAM[index] : "r" + index;
|
||||
if (this.proUppercase)
|
||||
ret = ret.toUpperCase();
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Format a system register
|
||||
systemRegister(index) {
|
||||
let ret = this.sysNames ?
|
||||
Disassembler.REG_SYSTEM[index] : index.toString();
|
||||
if (!this.sysUppercase && this.sysNames)
|
||||
ret = ret.toLowerCase();
|
||||
return ret;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { Disassembler };
|
|
@ -0,0 +1,79 @@
|
|||
#undef VBAPI
|
||||
#include <stdlib.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <vb.h>
|
||||
|
||||
|
||||
|
||||
/////////////////////////////// Module Commands ///////////////////////////////
|
||||
|
||||
// Create and initialize a new simulation
|
||||
EMSCRIPTEN_KEEPALIVE VB* Create() {
|
||||
VB *sim = malloc(sizeof (VB));
|
||||
vbInit(sim);
|
||||
return sim;
|
||||
}
|
||||
|
||||
// Delete all memory used by a simulation
|
||||
EMSCRIPTEN_KEEPALIVE void Delete(VB *sim) {
|
||||
free(sim->cart.ram);
|
||||
free(sim->cart.rom);
|
||||
free(sim);
|
||||
}
|
||||
|
||||
// Proxy for free()
|
||||
EMSCRIPTEN_KEEPALIVE void Free(void *ptr) {
|
||||
free(ptr);
|
||||
}
|
||||
|
||||
// Proxy for malloc()
|
||||
EMSCRIPTEN_KEEPALIVE void* Malloc(int size) {
|
||||
return malloc(size);
|
||||
}
|
||||
|
||||
// Size in bytes of a pointer
|
||||
EMSCRIPTEN_KEEPALIVE int PointerSize() {
|
||||
return sizeof (void *);
|
||||
}
|
||||
|
||||
|
||||
|
||||
////////////////////////////// Debugger Commands //////////////////////////////
|
||||
|
||||
// Execute until the following instruction
|
||||
static uint32_t RunToNextAddress;
|
||||
static int RunToNextFetch(VB *sim, int fetch, VBAccess *access) {
|
||||
return access->address == RunToNextAddress;
|
||||
}
|
||||
static int RunToNextExecute(VB *sim, VBInstruction *inst) {
|
||||
RunToNextAddress = inst->address + inst->size;
|
||||
vbSetCallback(sim, VB_ONEXECUTE, NULL);
|
||||
vbSetCallback(sim, VB_ONFETCH, &RunToNextFetch);
|
||||
return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE void RunToNext(VB **sims, int count) {
|
||||
uint32_t clocks = 20000000; // 1s
|
||||
vbSetCallback(sims[0], VB_ONEXECUTE, &RunToNextExecute);
|
||||
vbEmulateEx (sims, count, &clocks);
|
||||
vbSetCallback(sims[0], VB_ONEXECUTE, NULL);
|
||||
vbSetCallback(sims[0], VB_ONFETCH , NULL);
|
||||
}
|
||||
|
||||
// Execute the current instruction
|
||||
static int SingleStepBreak;
|
||||
static int SingleStepFetch(VB *sim, int fetch, VBAccess *access) {
|
||||
if (fetch != 0)
|
||||
return 0;
|
||||
if (SingleStepBreak == 1)
|
||||
return 1;
|
||||
SingleStepBreak = 1;
|
||||
return 0;
|
||||
}
|
||||
EMSCRIPTEN_KEEPALIVE void SingleStep(VB **sims, int count) {
|
||||
uint32_t clocks = 20000000; // 1s
|
||||
SingleStepBreak = sims[0]->cpu.stage == 0 ? 0 : 1;
|
||||
vbSetCallback(sims[0], VB_ONFETCH, &SingleStepFetch);
|
||||
vbEmulateEx (sims, count, &clocks);
|
||||
vbSetCallback(sims[0], VB_ONEXECUTE, NULL);
|
||||
vbSetCallback(sims[0], VB_ONFETCH , NULL);
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
import { ISX } from /**/"./ISX.js";
|
||||
|
||||
// Debug mode UI manager
|
||||
class Debugger {
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Data type conversions
|
||||
static F32 = new Float32Array(1);
|
||||
static U32 = new Uint32Array(this.F32.buffer);
|
||||
|
||||
// Reinterpret a float32 as a u32
|
||||
static fxi(x) {
|
||||
this.F32[0] = x;
|
||||
return this.U32[0];
|
||||
}
|
||||
|
||||
// Process file data as ISM
|
||||
static isx(data) {
|
||||
return new ISX(data);
|
||||
}
|
||||
|
||||
// Reinterpret a u32 as a float32
|
||||
static ixf(x) {
|
||||
this.U32[0] = x;
|
||||
return this.F32[0];
|
||||
}
|
||||
|
||||
// Compute the number of lines scrolled by a WheelEvent
|
||||
static linesScrolled(e, lineHeight, pageLines, delta) {
|
||||
let ret = {
|
||||
delta: delta,
|
||||
lines: 0
|
||||
};
|
||||
|
||||
// No scrolling occurred
|
||||
if (e.deltaY == 0);
|
||||
|
||||
// Scrolling by pixel
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
|
||||
ret.delta += e.deltaY;
|
||||
ret.lines = Math.sign(ret.delta) *
|
||||
Math.floor(Math.abs(ret.delta) / lineHeight);
|
||||
ret.delta -= ret.lines * lineHeight;
|
||||
}
|
||||
|
||||
// Scrolling by line
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE)
|
||||
ret.lines = Math.trunc(e.deltaY);
|
||||
|
||||
// Scrolling by page
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE)
|
||||
ret.lines = Math.trunc(e.deltaY) * pageLines;
|
||||
|
||||
// Unknown scrolling mode
|
||||
else ret.lines = 3 * Math.sign(e.deltaY);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, sim, index) {
|
||||
|
||||
// Configure instance fields
|
||||
this.app = app;
|
||||
this.core = app.core;
|
||||
this.dasm = app.dasm;
|
||||
this.sim = sim;
|
||||
|
||||
// Configure debugger windows
|
||||
this.cpu = new Debugger.CPU (this, index);
|
||||
this.memory = new Debugger.Memory(this, index);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Disassembler configuration has changed
|
||||
dasmConfigured() {
|
||||
this.cpu .dasmConfigured();
|
||||
this.memory.dasmConfigured();
|
||||
}
|
||||
|
||||
// Ensure PC is visible in the disassembler
|
||||
followPC(pc = null) {
|
||||
this.cpu.disassembler.followPC(pc);
|
||||
}
|
||||
|
||||
// Format a number as hexadecimal
|
||||
hex(value, digits = null, decorated = true) {
|
||||
return this.dasm.hex(value, digits, decorated);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Register component classes
|
||||
(await import(/**/"./CPU.js" )).register(Debugger);
|
||||
(await import(/**/"./Memory.js")).register(Debugger);
|
||||
|
||||
export { Debugger };
|
|
@ -0,0 +1,177 @@
|
|||
// Debug manager for Intelligent Systems binaries
|
||||
class ISX {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
// Throws on decoding error
|
||||
constructor(data) {
|
||||
|
||||
// Configure instance fields
|
||||
this.data = data;
|
||||
this.offset = 0;
|
||||
this.ranges = [];
|
||||
this.symbols = [];
|
||||
this.codes = [];
|
||||
|
||||
// Skip any header that may be present
|
||||
if (data.length >= 32 && this.readInt(3) == 0x585349)
|
||||
this.offset = 32;
|
||||
else this.offset = 0;
|
||||
|
||||
// Process all records
|
||||
while (this.offset < this.data.length) {
|
||||
switch (this.readInt(1)) {
|
||||
|
||||
// Virtual Boy records
|
||||
case 0x11: this.code (); break;
|
||||
case 0x13: this.range (); break;
|
||||
case 0x14: this.symbol(); break;
|
||||
|
||||
// System records
|
||||
case 0x20:
|
||||
case 0x21:
|
||||
case 0x22:
|
||||
let length = this.readInt(4);
|
||||
this.offset += length;
|
||||
break;
|
||||
|
||||
// Other records
|
||||
default: throw "ISX decode error";
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup instance fields
|
||||
delete this.data;
|
||||
delete this.decoder;
|
||||
delete this.offset;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Produce a .vb format ROM file from the ISX code segments
|
||||
toROM() {
|
||||
let head = 0x00000000;
|
||||
let tail = 0x01000000;
|
||||
|
||||
// Inspect all code segments
|
||||
for (let code of this.codes) {
|
||||
let start = code.address & 0x00FFFFFF;
|
||||
let end = start + code.data.length;
|
||||
|
||||
// Segment begins in the first half of ROM
|
||||
if (start < 0x00800000) {
|
||||
|
||||
// Segment ends in the second half of ROM
|
||||
if (end > 0x00800000) {
|
||||
head = tail = 0;
|
||||
break;
|
||||
}
|
||||
|
||||
// Segment ends in the first half of ROM
|
||||
else if (end > head)
|
||||
head = end;
|
||||
}
|
||||
|
||||
// Segment begins in the second half of ROM
|
||||
else if (start < tail)
|
||||
tail = start;
|
||||
}
|
||||
|
||||
// Prepare the output buffer
|
||||
let min = head + 0x01000000 - tail;
|
||||
let size = 1;
|
||||
for (; size < min; size <<= 1);
|
||||
let rom = new Uint8Array(size);
|
||||
|
||||
// Output all code segments
|
||||
for (let code of this.codes) {
|
||||
let dest = code.address & rom.length - 1;
|
||||
for (let src = 0; src < code.data.length; src++, dest++)
|
||||
rom[dest] = code.data[src];
|
||||
}
|
||||
|
||||
return rom;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Process a code record
|
||||
code() {
|
||||
let address = this.readInt(4);
|
||||
let length = this.readInt(4);
|
||||
let data = this.readBytes(length);
|
||||
if (
|
||||
length == 0 ||
|
||||
length > 0x01000000 ||
|
||||
(address & 0x07000000) != 0x07000000 ||
|
||||
(address & 0x07000000) + length > 0x08000000
|
||||
) throw "ISX decode error";
|
||||
this.codes.push({
|
||||
address: address,
|
||||
data : data
|
||||
});
|
||||
}
|
||||
|
||||
// Process a range record
|
||||
range() {
|
||||
let count = this.readInt(2);
|
||||
while (count--) {
|
||||
let start = this.readInt(4);
|
||||
let end = this.readInt(4);
|
||||
let type = this.readInt(1);
|
||||
this.ranges.push({
|
||||
end : end,
|
||||
start: start,
|
||||
type : type
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Process a symbol record
|
||||
symbol() {
|
||||
let count = this.readInt(2);
|
||||
while (count--) {
|
||||
let length = this.readInt(1);
|
||||
let name = this.readString(length);
|
||||
let flags = this.readInt(2);
|
||||
let address = this.readInt(4);
|
||||
this.symbols.push({
|
||||
address: address,
|
||||
flags : flags,
|
||||
name : name
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Read a byte buffer
|
||||
readBytes(size) {
|
||||
if (this.offset + size > this.data.length)
|
||||
throw "ISX decode error";
|
||||
let ret = this.data.slice(this.offset, this.offset + size);
|
||||
this.offset += size;
|
||||
return ret;
|
||||
}
|
||||
|
||||
// Read an integer
|
||||
readInt(size) {
|
||||
if (this.offset + size > this.data.length)
|
||||
throw "ISX decode error";
|
||||
let ret = new Uint32Array(1);
|
||||
for (let shift = 0; size > 0; size--, shift += 8)
|
||||
ret[0] |= this.data[this.offset++] << shift;
|
||||
return ret[0];
|
||||
}
|
||||
|
||||
// Read a text string
|
||||
readString(size) {
|
||||
return (this.decoder = this.decoder || new TextDecoder()
|
||||
).decode(this.readBytes(size));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { ISX };
|
|
@ -0,0 +1,574 @@
|
|||
import { Toolkit } from /**/"../toolkit/Toolkit.js";
|
||||
let register = Debugger => Debugger.Memory =
|
||||
|
||||
// Debugger memory window
|
||||
class Memory extends Toolkit.Window {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(debug, index) {
|
||||
super(debug.app, {
|
||||
class: "tk window memory"
|
||||
});
|
||||
|
||||
// Configure instance fields
|
||||
this.data = null,
|
||||
this.dataAddress = null,
|
||||
this.debug = debug;
|
||||
this.delta = 0;
|
||||
this.editDigit = null;
|
||||
this.height = 300;
|
||||
this.index = index;
|
||||
this.lines = [];
|
||||
this.pending = false;
|
||||
this.shown = false;
|
||||
this.subscription = [ 0, index, "memory", "refresh" ];
|
||||
this.width = 400;
|
||||
|
||||
// Available buses
|
||||
this.buses = [
|
||||
{
|
||||
editAddress: 0x05000000,
|
||||
viewAddress: 0x05000000
|
||||
}
|
||||
];
|
||||
this.bus = this.buses[0];
|
||||
|
||||
// Window
|
||||
this.setTitle("{debug.memory._}", true);
|
||||
this.substitute("#", " " + (index + 1));
|
||||
if (index == 1)
|
||||
this.element.classList.add("two");
|
||||
this.addEventListener("close" , e=>this.visible = false);
|
||||
this.addEventListener("visibility", e=>this.onVisibility(e));
|
||||
|
||||
// Client area
|
||||
Object.assign(this.client.style, {
|
||||
display : "grid",
|
||||
gridTemplateRows: "max-content auto"
|
||||
});
|
||||
|
||||
// Bus drop-down
|
||||
this.drpBus = new Toolkit.DropDown(debug.app);
|
||||
this.drpBus.setLabel("{debug.memory.bus}", true);
|
||||
this.drpBus.setTitle("{debug.memory.bus}", true);
|
||||
this.drpBus.add("{debug.memory.busMemory}", true, this.buses[0]);
|
||||
this.drpBus.addEventListener("input", e=>this.busInput());
|
||||
this.add(this.drpBus);
|
||||
|
||||
// Hex editor
|
||||
this.hexEditor = new Toolkit.Component(debug.app, {
|
||||
class : "tk mono hex-editor",
|
||||
role : "application",
|
||||
tabIndex: "0",
|
||||
style : {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "repeat(17, max-content)",
|
||||
height : "100%",
|
||||
minWidth : "100%",
|
||||
overflow : "hidden",
|
||||
position : "relative",
|
||||
width : "max-content"
|
||||
}
|
||||
});
|
||||
this.hexEditor.localize = ()=>{
|
||||
this.hexEditor.localizeRoleDescription();
|
||||
this.hexEditor.localizeLabel();
|
||||
};
|
||||
this.hexEditor.setLabel("{debug.memory.hexEditor}", true);
|
||||
this.hexEditor.setRoleDescription("{debug.memory.hexEditor}", true);
|
||||
this.hexEditor.addEventListener("focusout", e=>this.commit ( ));
|
||||
this.hexEditor.addEventListener("keydown" , e=>this.hexKeyDown(e));
|
||||
this.hexEditor.addEventListener("resize" , e=>this.hexResize ( ));
|
||||
this.hexEditor.addEventListener("wheel" , e=>this.hexWheel (e));
|
||||
this.hexEditor.addEventListener(
|
||||
"pointerdown", e=>this.hexPointerDown(e));
|
||||
this.lastFocus = this.hexEditor;
|
||||
|
||||
// Label for measuring text dimensions
|
||||
this.sizer = new Toolkit.Label(debug.app, {
|
||||
class : "tk label mono",
|
||||
visible : false,
|
||||
visibility: true,
|
||||
style: {
|
||||
position: "absolute"
|
||||
}
|
||||
});
|
||||
this.sizer.setText("\u00a0", false); //
|
||||
this.hexEditor.append(this.sizer);
|
||||
|
||||
// Hex editor scroll pane
|
||||
this.scrHex = new Toolkit.ScrollPane(debug.app, {
|
||||
overflowX: "auto",
|
||||
overflowY: "hidden",
|
||||
view : this.hexEditor
|
||||
});
|
||||
this.add(this.scrHex);
|
||||
|
||||
// Hide the bus drop-down: Virtual Boy only has one bus
|
||||
this.drpBus.visible = false;
|
||||
this.client.style.gridTemplateRows = "auto";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Bus drop-down selection
|
||||
busInput() {
|
||||
|
||||
// An edit is in progress
|
||||
if (this.editDigit !== null)
|
||||
this.commit(false);
|
||||
|
||||
// Switch to the new bus
|
||||
this.bus = this.drpBus.value;
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
// Hex editor key press
|
||||
hexKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey)
|
||||
|
||||
// Processing by key, scroll lock off
|
||||
if (!e.getModifierState("ScrollLock")) switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress + 16);
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "ArrowLeft":
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress - 1);
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "ArrowRight":
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress + 1);
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "ArrowUp":
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress - 16);
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "PageDown":
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress + this.tall(true)*16);
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "PageUp":
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress - this.tall(true)*16);
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Processing by key, scroll lock on
|
||||
else switch (e.key) {
|
||||
case "ArrowDown":
|
||||
this.setViewAddress(this.bus.viewAddress + 16);
|
||||
this.fetch();
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "ArrowLeft":
|
||||
this.scrHex.scrollLeft -= this.scrHex.hscroll.unitIncrement;
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "ArrowRight":
|
||||
this.scrHex.scrollLeft += this.scrHex.hscroll.unitIncrement;
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "ArrowUp":
|
||||
this.setViewAddress(this.bus.viewAddress - 16);
|
||||
this.fetch();
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "PageDown":
|
||||
this.setViewAddress(this.bus.viewAddress + this.tall(true)*16);
|
||||
this.fetch();
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
case "PageUp":
|
||||
this.setViewAddress(this.bus.viewAddress - this.tall(true)*16);
|
||||
this.fetch();
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Processing by key, editing
|
||||
switch (e.key) {
|
||||
|
||||
case "0": case "1": case "2": case "3": case "4":
|
||||
case "5": case "6": case "7": case "8": case "9":
|
||||
case "a": case "A": case "b": case "B": case "c":
|
||||
case "C": case "d": case "D": case "e": case "E":
|
||||
case "f": case "F":
|
||||
let digit = parseInt(e.key, 16);
|
||||
if (this.editDigit === null) {
|
||||
this.editDigit = digit;
|
||||
this.setEditAddress(this.bus.editAddress);
|
||||
} else {
|
||||
this.editDigit = this.editDigit << 4 | digit;
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress + 1);
|
||||
}
|
||||
break;
|
||||
|
||||
// Commit the current edit
|
||||
case "Enter":
|
||||
if (this.editDigit === null)
|
||||
break;
|
||||
this.commit();
|
||||
this.setEditAddress(this.bus.editAddress + 1);
|
||||
break;
|
||||
|
||||
// Cancel the current edit
|
||||
case "Escape":
|
||||
if (this.editDigit === null)
|
||||
return;
|
||||
this.editDigit = null;
|
||||
this.setEditAddress(this.bus.editAddress);
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Hex editor pointer down
|
||||
hexPointerDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let cols = this.lines[0].lblBytes.map(l=>l.getBoundingClientRect());
|
||||
let y = Math.max(0, Math.floor((e.clientY-cols[0].y)/cols[0].height));
|
||||
let x = 15;
|
||||
|
||||
// Determine which column is closest to the touch point
|
||||
if (e.clientX < cols[15].right) {
|
||||
for (let l = 0; l < 15; l++) {
|
||||
if (e.clientX > (cols[l].right + cols[l + 1].x) / 2)
|
||||
continue;
|
||||
x = l;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Update the selection address
|
||||
let address = this.toAddress(this.bus.viewAddress + y * 16 + x);
|
||||
if (this.editDigit !== null && address != this.bus.editAddress)
|
||||
this.commit();
|
||||
this.setEditAddress(address);
|
||||
}
|
||||
|
||||
// Hex editor resized
|
||||
hexResize() {
|
||||
let tall = this.tall(false);
|
||||
let grew = this.lines.length < tall;
|
||||
|
||||
// Process all visible lines
|
||||
for (let y = this.lines.length; y < tall; y++) {
|
||||
let line = {
|
||||
lblAddress: document.createElement("div"),
|
||||
lblBytes : []
|
||||
};
|
||||
|
||||
// Address label
|
||||
line.lblAddress.className = "addr" + (y == 0 ? " first" : "");
|
||||
this.hexEditor.append(line.lblAddress);
|
||||
|
||||
// Byte labels
|
||||
for (let x = 0; x < 16; x++) {
|
||||
let lbl = line.lblBytes[x] = document.createElement("div");
|
||||
lbl.className = "byte b" + x + (y == 0 ? " first" : "");
|
||||
this.hexEditor.append(lbl);
|
||||
}
|
||||
|
||||
this.lines.push(line);
|
||||
}
|
||||
|
||||
// Remove lines that are no longer visible
|
||||
while (tall < this.lines.length) {
|
||||
let line = this.lines[tall];
|
||||
line.lblAddress.remove();
|
||||
for (let lbl of line.lblBytes)
|
||||
lbl.remove();
|
||||
this.lines.splice(tall, 1);
|
||||
}
|
||||
|
||||
// Configure components
|
||||
let lineHeight = this.sizer.element.getBoundingClientRect().height;
|
||||
this.scrHex.hscroll.unitIncrement = lineHeight;
|
||||
this.hexEditor.element.style.gridAutoRows = lineHeight + "px";
|
||||
|
||||
// Update components
|
||||
if (grew)
|
||||
this.fetch();
|
||||
else this.refresh();
|
||||
}
|
||||
|
||||
// Hex editor mouse wheel
|
||||
hexWheel(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey)
|
||||
return;
|
||||
|
||||
// Always handle the event
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Determine how many full lines were scrolled
|
||||
let scr = Debugger.linesScrolled(e,
|
||||
this.sizer.element.getBoundingClientRect().height,
|
||||
this.tall(true),
|
||||
this.delta
|
||||
);
|
||||
this.delta = scr.delta;
|
||||
scr.lines = Math.max(-3, Math.min(3, scr.lines));
|
||||
|
||||
// No lines were scrolled
|
||||
if (scr.lines == 0)
|
||||
return;
|
||||
|
||||
// Scroll the view
|
||||
this.setViewAddress(this.bus.viewAddress + scr.lines * 16);
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
// Window key press
|
||||
onKeyDown(e) {
|
||||
super.onKeyDown(e);
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || !e.ctrlKey || e.shiftKey)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
case "g": case "G": this.goto(); break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Window visibility
|
||||
onVisibility(e) {
|
||||
this.shown = this.shown || e.visible;
|
||||
if (!e.visible)
|
||||
this.debug.core.unsubscribe(this.subscription, false);
|
||||
else this.fetch();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Disassembler configuration has changed
|
||||
dasmConfigured() {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// Prompt the user to navigate to a new editing address
|
||||
goto() {
|
||||
|
||||
// Retrieve the value from the user
|
||||
let addr = prompt(this.app.localize("{debug.memory.goto}"));
|
||||
if (addr === null)
|
||||
return;
|
||||
addr = parseInt(addr.trim(), 16);
|
||||
if (
|
||||
!Number.isInteger(addr) ||
|
||||
addr < 0 ||
|
||||
addr > 4294967295
|
||||
) return;
|
||||
|
||||
// Commit an outstanding edit
|
||||
if (this.editDigit !== null && this.bus.editAddress != addr)
|
||||
this.commit();
|
||||
|
||||
// Navigate to the given address
|
||||
this.hexEditor.focus();
|
||||
this.setEditAddress(addr, 1/3);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Write the edited value to the simulation state
|
||||
commit(refresh = true) {
|
||||
|
||||
// Error checking
|
||||
if (this.editDigit === null)
|
||||
return;
|
||||
|
||||
// The edited value is in the bus's data buffer
|
||||
if (this.data != null) {
|
||||
let offset = this.toAddress(this.bus.editAddress-this.dataAddress);
|
||||
if (offset < this.data.length)
|
||||
this.data[offset] = this.editDigit;
|
||||
}
|
||||
|
||||
// Write one byte to the simulation state
|
||||
let data = new Uint8Array(1);
|
||||
data[0] = this.editDigit;
|
||||
this.editDigit = null;
|
||||
this.debug.core.write(this.debug.sim, this.bus.editAddress,
|
||||
data, { refresh: refresh });
|
||||
}
|
||||
|
||||
// Retrieve data from the simulation state
|
||||
async fetch() {
|
||||
|
||||
// Select the parameters for the simulation fetch
|
||||
let params = {
|
||||
address: this.toAddress(this.bus.viewAddress - 10 * 16),
|
||||
length : (this.tall(false) + 20) * 16
|
||||
};
|
||||
|
||||
// A communication with the core thread is already underway
|
||||
if (this.pending) {
|
||||
this.pending = params;
|
||||
this.refresh();
|
||||
return;
|
||||
}
|
||||
|
||||
// Retrieve data from the simulation state
|
||||
this.pending = params;
|
||||
for (let data=null, promise=null; this.pending instanceof Object;) {
|
||||
|
||||
// Wait for a transaction to complete
|
||||
if (promise != null) {
|
||||
this.pending = true;
|
||||
data = await promise;
|
||||
promise = null;
|
||||
}
|
||||
|
||||
// Initiate a new transaction
|
||||
if (this.pending instanceof Object) {
|
||||
params = this.pending;
|
||||
let options = {};
|
||||
if (this.isVisible())
|
||||
options.subscription = this.subscription;
|
||||
promise = this.debug.core.read(this.debug.sim,
|
||||
params.address, params.length, options);
|
||||
}
|
||||
|
||||
// Process the result of a transaction
|
||||
if (data != null) {
|
||||
this.refresh(data);
|
||||
data = null;
|
||||
}
|
||||
|
||||
};
|
||||
this.pending = false;
|
||||
}
|
||||
|
||||
// Update hex editor
|
||||
refresh(msg = null) {
|
||||
|
||||
// Receiving data from the simulation state
|
||||
if (msg != null) {
|
||||
this.data = msg.data;
|
||||
this.dataAddress = msg.address;
|
||||
}
|
||||
|
||||
// Process all lines
|
||||
for (let y = 0; y < this.lines.length; y++) {
|
||||
let address = this.toAddress(this.bus.viewAddress + y * 16);
|
||||
let line = this.lines[y];
|
||||
|
||||
// Address label
|
||||
line.lblAddress.innerText = this.debug.hex(address, 8, false);
|
||||
|
||||
// Process all bytes
|
||||
for (let x = 0; x < 16; x++) {
|
||||
let label = line.lblBytes[x];
|
||||
let text = "--";
|
||||
|
||||
// Currently editing this byte
|
||||
if (address+x==this.bus.editAddress && this.editDigit!==null) {
|
||||
text = this.debug.hex(this.editDigit, 1, false);
|
||||
}
|
||||
|
||||
// Bus data exists
|
||||
else if (this.data != null) {
|
||||
let offset = this.toAddress(address-this.dataAddress+x);
|
||||
|
||||
// The byte is contained in the bus data buffer
|
||||
if (offset >= 0 && offset < this.data.length)
|
||||
text = this.debug.hex(this.data[offset], 2, false);
|
||||
}
|
||||
|
||||
label.innerText = text;
|
||||
label.classList[address + x == this.bus.editAddress ?
|
||||
"add" : "remove"]("edit");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Specify the address of the hex editor's selection
|
||||
setEditAddress(address, auto = false) {
|
||||
let col = this.lines[0].lblBytes[address&15].getBoundingClientRect();
|
||||
let port = this.scrHex.viewport.element.getBoundingClientRect();
|
||||
let row = this.toAddress(address & ~15);
|
||||
let scr = this.scrHex.scrollLeft;
|
||||
let tall = this.tall(true, 0);
|
||||
|
||||
// Ensure the data row is fully visible
|
||||
if (this.toAddress(row - this.bus.viewAddress) >= tall * 16) {
|
||||
if (!auto) {
|
||||
this.setViewAddress(
|
||||
this.toAddress(this.bus.viewAddress - row) <=
|
||||
this.toAddress(row - (this.bus.viewAddress + tall * 16))
|
||||
? row : this.toAddress(row - (tall - 1) * 16));
|
||||
} else this.setViewAddress(row - Math.floor(tall * auto) * 16);
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
// Ensure the column is fully visible
|
||||
this.scrHex.scrollLeft =
|
||||
Math.min(
|
||||
Math.max(
|
||||
scr,
|
||||
scr + col.right - port.right
|
||||
),
|
||||
scr - port.x + col.x
|
||||
)
|
||||
;
|
||||
|
||||
// Refresh the display;
|
||||
this.bus.editAddress = this.toAddress(address);
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
// Specify the address of the hex editor's view
|
||||
setViewAddress(address) {
|
||||
this.bus.viewAddress = this.toAddress(address);
|
||||
}
|
||||
|
||||
// Measure the number of lines visible in the view
|
||||
tall(fully = null, plus = 1) {
|
||||
return Math.max(1, Math[fully===null ? "abs" : fully?"floor":"ceil"](
|
||||
this.scrHex.viewport.element.getBoundingClientRect().height /
|
||||
this.sizer .element.getBoundingClientRect().height
|
||||
)) + plus;
|
||||
}
|
||||
|
||||
// Ensure an address is in the proper range
|
||||
toAddress(address) {
|
||||
return address >>> 0;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,77 @@
|
|||
{
|
||||
"id" : "en-US",
|
||||
"name": "English (US)",
|
||||
|
||||
"app": {
|
||||
"title": "Virtual Boy Emulator"
|
||||
},
|
||||
|
||||
"menu._": "Main menu",
|
||||
|
||||
"menu.file": {
|
||||
"_" : "File",
|
||||
"loadROM" : "Load ROM{#}...",
|
||||
"loadROMError" : "Error loading ROM file",
|
||||
"loadROMInvalid": "The selected file is not a Virtual Boy ROM.",
|
||||
"dualMode" : "Dual mode",
|
||||
"debugMode" : "Debug mode"
|
||||
},
|
||||
|
||||
"menu.emulation": {
|
||||
"_" : "Emulation",
|
||||
"run" : "Run",
|
||||
"pause" : "Pause",
|
||||
"reset" : "Reset{#}",
|
||||
"linkSims": "Link sims"
|
||||
},
|
||||
|
||||
"menu.debug": {
|
||||
"_" : "Debug{#}",
|
||||
"backgrounds" : "Backgrounds",
|
||||
"bgMaps" : "BG maps",
|
||||
"breakpoints" : "Breakpoints",
|
||||
"characters" : "Characters",
|
||||
"console" : "Console",
|
||||
"cpu" : "CPU",
|
||||
"frameBuffers": "Frame buffers",
|
||||
"memory" : "Memory",
|
||||
"objects" : "Objects",
|
||||
"palettes" : "Palettes"
|
||||
},
|
||||
|
||||
"menu.theme": {
|
||||
"_" : "Theme",
|
||||
"auto" : "Auto",
|
||||
"dark" : "Dark",
|
||||
"light" : "Light",
|
||||
"virtual": "Virtual"
|
||||
},
|
||||
|
||||
"window": {
|
||||
"close": "Close"
|
||||
},
|
||||
|
||||
"debug.cpu": {
|
||||
"_" : "CPU{#}",
|
||||
"disassembler" : "Disassembler",
|
||||
"float" : "Float",
|
||||
"format" : "Format",
|
||||
"goto" : "Enter the address to seek to:",
|
||||
"hex" : "Hex",
|
||||
"infinity" : "Infinity",
|
||||
"programRegisters": "Program registers",
|
||||
"signed" : "Signed",
|
||||
"systemRegisters" : "System registers",
|
||||
"unsigned" : "Unsigned",
|
||||
"value" : "Value"
|
||||
},
|
||||
|
||||
"debug.memory": {
|
||||
"_" : "Memory{#}",
|
||||
"bus" : "Bus",
|
||||
"busMemory": "Memory",
|
||||
"goto" : "Enter the address to seek to:",
|
||||
"hexEditor": "Hex editor"
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<!DOCTYPE html><html lang="en"><head><meta charset="utf-8"><link rel="icon"href="data:;base64,iVBORw0KGgo="><title>Virtual Boy Emulator</title></head><body><img style="display:none;"onload="let a=this,b=a.width,c=a.height,d=document.createElement('canvas');a.remove();d.width=b;d.height=c;d=d.getContext('2d');d.drawImage(a,0,0);d=d.getImageData(0,0,b,c).data.filter((e,f)=>!(f&3));new(async()=>{}).constructor(Array.from(d.slice(0,d.indexOf(0))).map(e=>String.fromCharCode(e)).join(''))(d,a)"src=""></body></html>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458 2.6458" version="1.1">
|
||||
<g>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.26458px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 1.05832,1.653625 1.3229,-1.3229 v 0.66145 l -1.3229,1.3229 -0.79374,-0.79374 v -0.66145 z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 459 B |
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458 2.6458" version="1.1">
|
||||
<path style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:0.13229;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none" d="m 1.05832,1.653625 1.3229,-1.3229 v 0.66145 l -1.3229,1.3229 -0.79374,-0.79374 v -0.66145 z" />
|
||||
</svg>
|
After Width: | Height: | Size: 484 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
|
||||
<g>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="m 0.52916666,0.52916665 0.396875,0 0.52916664,0.52916665 0.5291667,-0.52916665 0.396875,0 0,0.396875 L 1.8520833,1.4552083 2.38125,1.984375 l 0,0.396875 -0.396875,0 L 1.4552083,1.8520834 0.92604166,2.38125 l -0.396875,0 0,-0.396875 L 1.0583333,1.4552083 0.52916666,0.92604165 Z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 652 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
|
||||
<g>
|
||||
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 367 B |
|
@ -0,0 +1,28 @@
|
|||
:root {
|
||||
--tk-control : #333333;
|
||||
--tk-control-active : #555555;
|
||||
--tk-control-border : #cccccc;
|
||||
--tk-control-highlight : #444444;
|
||||
--tk-control-shadow : #9b9b9b;
|
||||
--tk-control-text : #cccccc;
|
||||
--tk-desktop : #111111;
|
||||
--tk-selected : #008542;
|
||||
--tk-selected-blur : #325342;
|
||||
--tk-selected-blur-text : #ffffff;
|
||||
--tk-selected-text : #ffffff;
|
||||
--tk-splitter-focus : #008542c0;
|
||||
--tk-window : #222222;
|
||||
--tk-window-blur-close : #d9aeae;
|
||||
--tk-window-blur-close-text : #eeeeee;
|
||||
--tk-window-blur-title : #9fafb9;
|
||||
--tk-window-blur-title2 : #c0b0a0;
|
||||
--tk-window-blur-title-text : #444444;
|
||||
--tk-window-close : #ee9999;
|
||||
--tk-window-close-focus : #99ee99;
|
||||
--tk-window-close-focus-text: #333333;
|
||||
--tk-window-close-text : #ffffff;
|
||||
--tk-window-text : #cccccc;
|
||||
--tk-window-title : #80ccff;
|
||||
--tk-window-title2 : #ffb894;
|
||||
--tk-window-title-text : #000000;
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168">
|
||||
<g>
|
||||
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="1.3229167" y="0.79375005" />
|
||||
<rect style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="0.79375005" y="1.3229167" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 522 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104168" version="1.1">
|
||||
<rect style="fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="1.8555599" y="-1.1941016" transform="rotate(60)" />
|
||||
<rect style="fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="1.3229167" height="0.26458332" x="1.3263932" y="0.40035158" transform="rotate(30)" />
|
||||
<rect style="fill:#000000;stroke-width:0.264583;stroke-linecap:square" width="0.26458332" height="1.3229167" x="-1.5875" y="0.79374999" transform="scale(-1,1)" />
|
||||
</svg>
|
After Width: | Height: | Size: 710 B |
|
@ -0,0 +1,554 @@
|
|||
:root {
|
||||
--tk-font-dialog : "Roboto", sans-serif;
|
||||
--tk-font-mono : "Inconsolata SemiExpanded Medium", monospace;
|
||||
--tk-font-size : 12px;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Roboto";
|
||||
src : /**/url("./roboto.woff2") format("woff2");
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "Inconsolata SemiExpanded Medium";
|
||||
src : /**/url("./inconsolata.woff2") format("woff2");
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--tk-control);
|
||||
}
|
||||
|
||||
.tk {
|
||||
box-sizing : border-box;
|
||||
font-family: var(--tk-font-dialog);
|
||||
font-size : var(--tk-font-size);
|
||||
line-height: 1em;
|
||||
margin : 0;
|
||||
outline : none; /* User agent focus indicator */
|
||||
padding : 0;
|
||||
}
|
||||
|
||||
table.tk {
|
||||
border : none;
|
||||
border-spacing: 0;
|
||||
}
|
||||
|
||||
.tk.mono {
|
||||
font-family: var(--tk-font-mono);
|
||||
}
|
||||
|
||||
.tk::selection,
|
||||
.tk *::selection {
|
||||
background: var(--tk-selected);
|
||||
color : var(--tk-selected-text);
|
||||
}
|
||||
|
||||
.tk:not(:focus-within)::selection,
|
||||
.tk *:not(:focus-within)::selection {
|
||||
background: var(--tk-selected-blur);
|
||||
color : var(--tk-selected-blur-text);
|
||||
}
|
||||
|
||||
.tk.display {
|
||||
background: var(--tk-desktop);
|
||||
}
|
||||
|
||||
.tk.desktop {
|
||||
background: var(--tk-desktop);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************** Button ***********************************/
|
||||
|
||||
.tk.button {
|
||||
align-items : stretch;
|
||||
display : inline-grid;
|
||||
grid-template-columns: auto;
|
||||
justify-content : stretch;
|
||||
padding : 0 1px 1px 0;
|
||||
}
|
||||
|
||||
.tk.button .label {
|
||||
align-items : center;
|
||||
background : var(--tk-control);
|
||||
border : 1px solid var(--tk-control-border);
|
||||
box-shadow : 1px 1px 0 var(--tk-control-border);
|
||||
color : var(--tk-control-text);
|
||||
display : grid;
|
||||
grid-template-columns: auto;
|
||||
justify-content : center;
|
||||
padding : 2px;
|
||||
}
|
||||
|
||||
.tk.button:focus .label {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
|
||||
.tk.button.pushed {
|
||||
padding: 1px 0 0 1px;
|
||||
}
|
||||
|
||||
.tk.button.pushed .label {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.tk.button[aria-disabled="true"] .label {
|
||||
color : var(--tk-control-shadow);
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
box-shadow: 1px 1px 0 var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************* Checkbox **********************************/
|
||||
|
||||
.tk.checkbox {
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
||||
.tk.checkbox .box {
|
||||
border: 1px solid var(--tk-control-shadow);
|
||||
color : var(--tk-control-text);
|
||||
}
|
||||
|
||||
.tk.checkbox:focus .box {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
|
||||
.tk.checkbox .box:before {
|
||||
background : transparent;
|
||||
content : "";
|
||||
display : block;
|
||||
height : 10px;
|
||||
mask : /**/url("./check.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./check.svg") center no-repeat;
|
||||
width : 10px;
|
||||
}
|
||||
|
||||
.tk.checkbox[aria-checked="true"] .box:before {
|
||||
background: currentcolor;
|
||||
}
|
||||
|
||||
.tk.checkbox[aria-checked="mixed"] .box:before {
|
||||
background : currentcolor;
|
||||
mask : /**/url("./check2.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./check2.svg") center no-repeat;
|
||||
}
|
||||
|
||||
.tk.checkbox.pushed .box:before {
|
||||
background: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.checkbox[aria-disabled="true"] .box {
|
||||
background: var(--tk-control);
|
||||
color : var(--tk-control-shadow);
|
||||
}
|
||||
.tk.checkbox[aria-disabled="true"] .label {
|
||||
color: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************* DropDown **********************************/
|
||||
|
||||
.tk.drop-down {
|
||||
background: var(--tk-window);
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
color : var(--tk-window-text);
|
||||
padding : 2px;
|
||||
}
|
||||
|
||||
.tk.drop-down:focus {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
|
||||
.tk.drop-down[aria-disabled="true"] {
|
||||
color: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************** Menus ***********************************/
|
||||
|
||||
.tk.menu-bar {
|
||||
background : var(--tk-control);
|
||||
border-bottom: 1px solid var(--tk-control-border);
|
||||
color : var(--tk-control-text);
|
||||
cursor : default;
|
||||
padding : 2px;
|
||||
position : relative;
|
||||
}
|
||||
|
||||
.tk.menu {
|
||||
background: var(--tk-control);
|
||||
border : 1px solid var(--tk-control-border);
|
||||
box-shadow: 1px 1px 0 var(--tk-control-border);
|
||||
color : var(--tk-control-text);
|
||||
margin : -1px 0 0 1px;
|
||||
padding : 2px;
|
||||
}
|
||||
|
||||
.tk.menu-item[aria-disabled="true"] {
|
||||
color: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.menu-item > * {
|
||||
align-items: center;
|
||||
border : 1px solid transparent;
|
||||
column-gap : 4px;
|
||||
display : flex;
|
||||
margin : 0 1px 1px 0;
|
||||
padding : 2px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tk.menu-item .icon {
|
||||
box-sizing: border-box;
|
||||
height : 1em;
|
||||
width : 1em;
|
||||
}
|
||||
|
||||
.tk.menu-item .icon:before {
|
||||
content: "";
|
||||
display: block;
|
||||
height : 100%;
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
.tk.menu-bar > .menu-item .icon,
|
||||
.tk.menu:not(.icons) > .menu-item .icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tk.menu-item.checkbox .icon {
|
||||
border: 1px solid currentcolor;
|
||||
}
|
||||
|
||||
.tk.menu-item.checkbox[aria-checked="true"] .icon:before {
|
||||
background : currentcolor;
|
||||
mask : /**/url("./check.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./check.svg") center no-repeat;
|
||||
}
|
||||
|
||||
.tk.menu-item .label {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.tk.menu-item:not([aria-expanded="true"],
|
||||
[aria-disabled="true"], .pushed):hover > *,
|
||||
.tk.menu-item:not([aria-expanded="true"], .pushed):focus > * {
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
box-shadow: 1px 1px 0 var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.menu-item:focus > * {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
|
||||
.tk.menu-item.pushed > *,
|
||||
.tk.menu-item[aria-expanded="true"] > * {
|
||||
background: var(--tk-control-active);
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
box-shadow: none;
|
||||
margin : 1px 0 0 1px;
|
||||
}
|
||||
|
||||
.tk.menu > [role="separator"] {
|
||||
border : solid var(--tk-control-shadow);
|
||||
border-width: 1px 0 0 0;
|
||||
margin : 4px 2px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*********************************** Radio ***********************************/
|
||||
|
||||
.tk.radio {
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
||||
.tk.radio .box {
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
border-radius: 50%;
|
||||
color : var(--tk-control-text);
|
||||
margin : 1px;
|
||||
}
|
||||
|
||||
.tk.radio:focus .box {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
|
||||
.tk.radio .box:before {
|
||||
background : transparent;
|
||||
border-radius: 50%;
|
||||
content : "";
|
||||
display : block;
|
||||
height : 4px;
|
||||
margin : 2px;
|
||||
width : 4px;
|
||||
}
|
||||
|
||||
.tk.radio[aria-checked="true"] .box:before {
|
||||
background: currentcolor;
|
||||
}
|
||||
|
||||
.tk.radio.pushed .box:before {
|
||||
background: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.radio[aria-disabled="true"] .box {
|
||||
background: var(--tk-control);
|
||||
color : var(--tk-control-shadow);
|
||||
}
|
||||
.tk.radio[aria-disabled="true"] .label {
|
||||
color: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************* ScrollBar *********************************/
|
||||
|
||||
.tk.scroll-bar {
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.tk.scroll-bar .unit-less,
|
||||
.tk.scroll-bar .unit-more {
|
||||
background: var(--tk-control);
|
||||
border : 0 solid var(--tk-control-shadow);
|
||||
color : var(--tk-control-text);
|
||||
height : 11px;
|
||||
width : 11px;
|
||||
}
|
||||
.tk.scroll-bar[aria-orientation="horizontal"] .unit-less {
|
||||
border-right-width: 1px;
|
||||
}
|
||||
.tk.scroll-bar[aria-orientation="horizontal"] .unit-more {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
.tk.scroll-bar[aria-orientation="vertical"] .unit-less {
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
.tk.scroll-bar[aria-orientation="vertical"] .unit-more {
|
||||
border-top-width: 1px;
|
||||
}
|
||||
|
||||
.tk.scroll-bar .unit-less:before,
|
||||
.tk.scroll-bar .unit-more:before {
|
||||
background : currentColor;
|
||||
content : "";
|
||||
display : block;
|
||||
height : 100%;
|
||||
mask : /**/url("./scroll.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./scroll.svg") center no-repeat;
|
||||
width : 100%;
|
||||
}
|
||||
|
||||
.tk.scroll-bar .unit-less.pushed:before,
|
||||
.tk.scroll-bar .unit-more.pushed:before {
|
||||
mask-size : 9px;
|
||||
-webkit-mask-size: 9px;
|
||||
}
|
||||
|
||||
.tk.scroll-bar[aria-orientation="horizontal"] .unit-less:before {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
.tk.scroll-bar[aria-orientation="horizontal"] .unit-more:before {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
.tk.scroll-bar[aria-orientation="vertical"] .unit-more:before {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.tk.scroll-bar .track {
|
||||
background: var(--tk-control-highlight);
|
||||
}
|
||||
|
||||
.tk.scroll-bar .thumb {
|
||||
background: var(--tk-control);
|
||||
box-shadow: 0 0 0 1px var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.scroll-bar .block-less.pushed,
|
||||
.tk.scroll-bar .block-more.pushed {
|
||||
background: var(--tk-control-shadow);
|
||||
opacity : 0.5;
|
||||
}
|
||||
|
||||
.tk.scroll-bar:focus .unit-less,
|
||||
.tk.scroll-bar:focus .unit-more,
|
||||
.tk.scroll-bar:focus .thumb {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
|
||||
.tk.scroll-bar[aria-disabled="true"] .unit-less,
|
||||
.tk.scroll-bar[aria-disabled="true"] .unit-more,
|
||||
.tk.scroll-bar.unneeded .unit-less,
|
||||
.tk.scroll-bar.unneeded .unit-more,
|
||||
.tk.scroll-bar[aria-disabled="true"] .thumb {
|
||||
color: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.scroll-bar.unneeded .thumb {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************************** ScrollPane *********************************/
|
||||
|
||||
.tk.scroll-pane {
|
||||
border: 1px solid var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.scroll-pane > .scroll-bar[aria-orientation="horizontal"] {
|
||||
border-width: 1px 1px 0 0;
|
||||
}
|
||||
.tk.scroll-pane:not(.vertical) > .scroll-bar[aria-orientation="horizontal"] {
|
||||
border-width: 1px 0 0 0;
|
||||
}
|
||||
.tk.scroll-pane > .scroll-bar[aria-orientation="vertical"] {
|
||||
border-width: 0 0 1px 1px;
|
||||
}
|
||||
.tk.scroll-pane:not(.horizontal) > .scroll-bar[aria-orientation="vertical"] {
|
||||
border-width: 0 0 0 1px;
|
||||
}
|
||||
|
||||
.tk.scroll-pane > .viewport,
|
||||
.tk.scroll-pane > .corner {
|
||||
background: var(--tk-control);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************* SplitPane *********************************/
|
||||
|
||||
.tk.split-pane > [role="separator"]:focus {
|
||||
background: var(--tk-splitter-focus);
|
||||
}
|
||||
|
||||
.tk.split-pane > .horizontal[role="separator"] {
|
||||
width: 3px;
|
||||
}
|
||||
.tk.split-pane > .vertical[role="separator"] {
|
||||
height: 3px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************** TextBox **********************************/
|
||||
|
||||
.tk.text-box {
|
||||
background : var(--tk-window);
|
||||
border : 1px solid var(--tk-control-border);
|
||||
color : var(--tk-window-text);
|
||||
line-height: 1em;
|
||||
height : calc(1em + 2px);
|
||||
padding : 0;
|
||||
margin : 0;
|
||||
min-width : 0;
|
||||
}
|
||||
|
||||
.tk.text-box.[aria-disabled="true"] {
|
||||
background: var(--tk-control-shadow);
|
||||
color : var(--tk-window-text);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/********************************** Windows **********************************/
|
||||
|
||||
.tk.window {
|
||||
background: var(--tk-control);
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
box-shadow: 1px 1px 0 var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.window:focus-within {
|
||||
border : 1px solid var(--tk-control-border);
|
||||
box-shadow: 1px 1px 0 var(--tk-control-border);
|
||||
}
|
||||
|
||||
.tk.window > .nw1 { left : -2px; top : -2px; width : 8px; height: 3px; }
|
||||
.tk.window > .nw2 { left : -2px; top : 1px; width : 3px; height: 5px; }
|
||||
.tk.window > .n { left : 6px; top : -2px; right : 6px; height: 3px; }
|
||||
.tk.window > .ne1 { right: -2px; top : -2px; width : 8px; height: 3px; }
|
||||
.tk.window > .ne2 { right: -2px; top : 1px; width : 3px; height: 5px; }
|
||||
.tk.window > .w { left : -2px; top : 6px; bottom: 6px; width : 3px; }
|
||||
.tk.window > .e { right: -2px; top : 6px; bottom: 6px; width : 3px; }
|
||||
.tk.window > .sw1 { left : -2px; bottom: -2px; width : 8px; height: 3px; }
|
||||
.tk.window > .sw2 { left : -2px; bottom: 1px; width : 3px; height: 5px; }
|
||||
.tk.window > .s { left : 6px; bottom: -2px; right : 6px; height: 3px; }
|
||||
.tk.window > .se1 { right: -2px; bottom: -2px; width : 8px; height: 3px; }
|
||||
.tk.window > .se2 { right: -2px; bottom: 1px; width : 3px; height: 5px; }
|
||||
|
||||
.tk.window > .title {
|
||||
align-items : center;
|
||||
background : var(--tk-window-blur-title);
|
||||
border-bottom: 1px solid var(--tk-control-shadow);
|
||||
color : var(--tk-window-blur-title-text);
|
||||
padding : 1px;
|
||||
user-select : none;
|
||||
}
|
||||
|
||||
.tk.window:focus-within > .title {
|
||||
background: var(--tk-window-title);
|
||||
color : var(--tk-window-title-text);
|
||||
}
|
||||
|
||||
.tk.window.two > .title {
|
||||
background: var(--tk-window-blur-title2);
|
||||
}
|
||||
|
||||
.tk.window.two:focus-within > .title {
|
||||
background: var(--tk-window-title2);
|
||||
}
|
||||
|
||||
.tk.window > .title .text {
|
||||
cursor : default;
|
||||
font-weight : bold;
|
||||
overflow : hidden;
|
||||
text-align : center;
|
||||
text-overflow: ellipsis;
|
||||
white-space : nowrap;
|
||||
}
|
||||
|
||||
.tk.window > .title .close-button {
|
||||
background: var(--tk-window-blur-close);
|
||||
border : 1px solid var(--tk-control-shadow);
|
||||
box-sizing: border-box;
|
||||
color : var(--tk-window-close-text);
|
||||
height : 13px;
|
||||
width : 13px;
|
||||
}
|
||||
|
||||
.tk.window:focus-within > .title .close-button {
|
||||
background: var(--tk-window-close);
|
||||
}
|
||||
|
||||
.tk.window > .title .close-button:focus {
|
||||
background: var(--tk-window-close-focus);
|
||||
color : var(--tk-window-close-focus-text);
|
||||
outline : 1px solid var(--tk-control);
|
||||
}
|
||||
|
||||
.tk.window > .title .close-button:before {
|
||||
background : currentcolor;
|
||||
content : "";
|
||||
display : block;
|
||||
height : 11px;
|
||||
mask : /**/url("./close.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./close.svg") center no-repeat;
|
||||
width : 11px;
|
||||
}
|
||||
|
||||
.tk.window > .title .close-button.pushed:before {
|
||||
mask-size : 9px;
|
||||
-webkit-mask-size: 9px;
|
||||
}
|
||||
|
||||
.tk.window > .client {
|
||||
overflow: hidden;
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
:root {
|
||||
--tk-control : #eeeeee;
|
||||
--tk-control-active : #cccccc;
|
||||
--tk-control-border : #000000;
|
||||
--tk-control-highlight : #f8f8f8;
|
||||
--tk-control-shadow : #6c6c6c;
|
||||
--tk-control-text : #000000;
|
||||
--tk-desktop : #cccccc;
|
||||
--tk-selected : #008542;
|
||||
--tk-selected-blur : #5e7d70;
|
||||
--tk-selected-blur-text : #ffffff;
|
||||
--tk-selected-text : #ffffff;
|
||||
--tk-splitter-focus : #008542c0;
|
||||
--tk-window : #ffffff;
|
||||
--tk-window-blur-close : #d9aeae;
|
||||
--tk-window-blur-close-text : #eeeeee;
|
||||
--tk-window-blur-title : #aac4d5;
|
||||
--tk-window-blur-title2 : #dbc4b8;
|
||||
--tk-window-blur-title-text : #444444;
|
||||
--tk-window-close : #ee9999;
|
||||
--tk-window-close-focus : #99ee99;
|
||||
--tk-window-close-focus-text: #333333;
|
||||
--tk-window-close-text : #ffffff;
|
||||
--tk-window-text : #000000;
|
||||
--tk-window-title : #80ccff;
|
||||
--tk-window-title2 : #ffb894;
|
||||
--tk-window-title-text : #000000;
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="10" height="10" viewBox="0 0 2.6458332 2.6458332" version="1.1">
|
||||
<g>
|
||||
<circle style="opacity:1;fill:#000000;stroke-width:0.264583;stroke-linecap:square" cx="1.3229166" cy="1.3229166" r="0.66145831" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 361 B |
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 2.9104166 2.9104167" version="1.1">
|
||||
<g>
|
||||
<path style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" d="M 1.4552083,0.66145833 0.52916666,1.5874999 V 2.2489583 L 1.4552083,1.3229166 2.38125,2.2489583 V 1.5874999 Z" />
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 484 B |
|
@ -0,0 +1,194 @@
|
|||
/******************************** CPU Window *********************************/
|
||||
|
||||
.tk.window.cpu .client {
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .scr-dasm {
|
||||
border-right-width: 0;
|
||||
box-shadow : 1px 0 0 var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.window.cpu .scr-system {
|
||||
border-width: 1px 1px 0 0;
|
||||
box-shadow : -0.5px 0.5px 0 0.5px var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.window.cpu .scr-program {
|
||||
border-width: 0 1px 1px 0;
|
||||
box-shadow : -0.5px -0.5px 0 0.5px var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler {
|
||||
background : var(--tk-window);
|
||||
color : var(--tk-window-text);
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler div {
|
||||
cursor : default;
|
||||
line-height: calc(1em + 2px);
|
||||
isolation : isolate;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler .address {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler .byte {
|
||||
margin-left: 0.5em;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler .operands {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler .byte.b0,
|
||||
.tk.window.cpu .disassembler .mnemonic,
|
||||
.tk.window.cpu .disassembler .operands {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler .pc {
|
||||
background: var(--tk-selected-blur);
|
||||
left : 1px;
|
||||
right : 1px;
|
||||
}
|
||||
.tk.window.cpu .disassembler:focus-within .pc {
|
||||
background: var(--tk-selected);
|
||||
}
|
||||
|
||||
.tk.window.cpu .disassembler .is-pc {
|
||||
color: var(--tk-selected-blur-text);
|
||||
}
|
||||
.tk.window.cpu .disassembler:focus-within .is-pc {
|
||||
color: var(--tk-selected-text);
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers {
|
||||
background: var(--tk-window);
|
||||
color : var(--tk-window-text);
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers > * {
|
||||
padding: 0 1px;
|
||||
}
|
||||
.tk.window.cpu .registers > *:first-child {
|
||||
padding-top: 1px;
|
||||
}
|
||||
.tk.window.cpu .registers > *:last-child {
|
||||
padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .expand .box {
|
||||
border : none;
|
||||
border-radius: 2px;
|
||||
margin : 0 1px 0 0;
|
||||
}
|
||||
.tk.window.cpu .registers .expand .box:before {
|
||||
background: transparent;
|
||||
content : "";
|
||||
display : block;
|
||||
height : 11px;
|
||||
width : 11px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .expand[role="checkbox"] .box:before {
|
||||
background : currentcolor;
|
||||
mask : /**/url("./expand.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./expand.svg") center no-repeat;
|
||||
}
|
||||
.tk.window.cpu .registers .expand[aria-checked="true"] .box:before {
|
||||
background : currentcolor;
|
||||
mask : /**/url("./collapse.svg") center no-repeat;
|
||||
-webkit-mask: /**/url("./collapse.svg") center no-repeat;
|
||||
}
|
||||
.tk.window.cpu .registers .expand:focus .box {
|
||||
background: var(--tk-control-active);
|
||||
}
|
||||
.tk.window.cpu .registers .main {
|
||||
column-gap: 0.5em;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .expansion {
|
||||
gap : 1px 1em;
|
||||
padding: 2px 2px 2px 1.4em;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .main {
|
||||
column-gap: 0.5em;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .text-box {
|
||||
background: transparent;
|
||||
border : none;
|
||||
padding : 0 1px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .text-box:focus {
|
||||
outline: 1px solid var(--tk-selected);
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .text-dec {
|
||||
column-gap: 2px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers .text-dec .label {
|
||||
text-align: center;
|
||||
min-width : 13px;
|
||||
}
|
||||
|
||||
.tk.window.cpu .registers *[aria-disabled="true"]:is(.label, .text-box) {
|
||||
color: var(--tk-control-shadow);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/******************************* Memory Window *******************************/
|
||||
|
||||
.tk.window.memory .client {
|
||||
gap : 1px;
|
||||
padding: 1px;
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor {
|
||||
align-items: center;
|
||||
background : var(--tk-window);
|
||||
color : var(--tk-window-text);
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor div {
|
||||
cursor : default;
|
||||
line-height: calc(1em + 2px);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor .addr {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor .byte {
|
||||
margin-left: 0.5em;
|
||||
text-align : center;
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor .b0,
|
||||
.tk.window.memory .hex-editor .b8 {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor .b15 {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.tk.window.memory .hex-editor .edit {
|
||||
background: var(--tk-selected-blur);
|
||||
color : var(--tk-selected-blur-text);
|
||||
outline : 1px solid var(--tk-selected-blur);
|
||||
}
|
||||
.tk.window.memory .hex-editor:focus-within .edit {
|
||||
background: var(--tk-selected);
|
||||
color : var(--tk-selected-text);
|
||||
outline : 1px solid var(--tk-selected);
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
:root {
|
||||
--tk-control : #000000;
|
||||
--tk-control-active : #550000;
|
||||
--tk-control-border : #ff0000;
|
||||
--tk-control-highlight : #550000;
|
||||
--tk-control-shadow : #aa0000;
|
||||
--tk-control-text : #ff0000;
|
||||
--tk-desktop : #000000;
|
||||
--tk-selected : #aa0000;
|
||||
--tk-selected-blur : #550000;
|
||||
--tk-selected-blur-text : #ff0000;
|
||||
--tk-selected-text : #000000;
|
||||
--tk-splitter-focus : #ff0000aa;
|
||||
--tk-window : #000000;
|
||||
--tk-window-blur-close : #000000;
|
||||
--tk-window-blur-close-text : #aa0000;
|
||||
--tk-window-blur-title : #000000;
|
||||
--tk-window-blur-title2 : #000000;
|
||||
--tk-window-blur-title-text : #aa0000;
|
||||
--tk-window-close : #550000;
|
||||
--tk-window-close-focus : #ff0000;
|
||||
--tk-window-close-focus-text: #550000;
|
||||
--tk-window-close-text : #ff0000;
|
||||
--tk-window-text : #ff0000;
|
||||
--tk-window-title : #550000;
|
||||
--tk-window-title2 : #550000;
|
||||
--tk-window-title-text : #ff0000;
|
||||
}
|
||||
|
||||
input, select {
|
||||
filter: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciPjxmaWx0ZXIgaWQ9InYiPjxmZUNvbG9yTWF0cml4IGluPSJTb3VyY2VHcmFwaGljIiB0eXBlPSJtYXRyaXgiIHZhbHVlcz0iMSAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMSAwIiAvPjwvZmlsdGVyPjwvc3ZnPg==#v");
|
||||
}
|
||||
|
||||
.tk.scroll-bar .unit-less,
|
||||
.tk.scroll-bar .unit-more,
|
||||
.tk.scroll-bar .thumb {
|
||||
background: #550000;
|
||||
}
|
||||
.tk.scroll-bar .track {
|
||||
background: #000000;
|
||||
}
|
||||
|
||||
.tk.scroll-bar:focus,
|
||||
.tk.scroll-bar:focus .unit-less,
|
||||
.tk.scroll-bar:focus .unit-more,
|
||||
.tk.scroll-bar:focus .thumb {
|
||||
background : #aa0000;
|
||||
border-color: #ff0000;
|
||||
color : #000000;
|
||||
}
|
||||
.tk.scroll-bar:focus .track {
|
||||
background: #550000;
|
||||
}
|
||||
.tk.scroll-bar:focus .thumb {
|
||||
box-shadow: 0 0 0 1px #ff0000;
|
||||
}
|
||||
|
||||
.tk.window {
|
||||
box-shadow: 1px 1px 0 #550000;
|
||||
}
|
||||
.tk.window:focus-within {
|
||||
box-shadow: 1px 1px 0 #aa0000;
|
||||
}
|
|
@ -0,0 +1,249 @@
|
|||
let register = Toolkit => Toolkit.App =
|
||||
|
||||
// Root application container and localization manager
|
||||
class App extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(options) {
|
||||
super(null, Object.assign({
|
||||
tabIndex: -1
|
||||
}, options));
|
||||
|
||||
// Configure instance fields
|
||||
this.components = new Set();
|
||||
this.dragElement = null;
|
||||
this.lastFocus = null;
|
||||
this.locale = null;
|
||||
this.locales = new Map();
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("focusin", e=>this.onFocus(e));
|
||||
this.addEventListener("keydown", e=>this.onKey (e));
|
||||
this.addEventListener("keyup" , e=>this.onKey (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Child element focus gained
|
||||
onFocus(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.target != document.activeElement)
|
||||
return;
|
||||
|
||||
// Target is self
|
||||
if (e.target == this.element)
|
||||
return this.restoreFocus();
|
||||
|
||||
// Ensure the child is not contained in a MenuBar
|
||||
for (let elm = e.target; elm != this.element; elm = elm.parentNode) {
|
||||
if (elm.getAttribute("role") == "menubar")
|
||||
return;
|
||||
}
|
||||
|
||||
// Track the (non-menu) element as the most recent focused component
|
||||
this.lastFocus = e.target;
|
||||
}
|
||||
|
||||
// Key press, key release
|
||||
onKey(e) {
|
||||
if (this.dragElement == null || e.rerouted)
|
||||
return;
|
||||
this.dragElement.dispatchEvent(Object.assign(new Event(e.type), {
|
||||
altKey : e.altKey,
|
||||
ctrlKey : e.ctrlKey,
|
||||
key : e.key,
|
||||
rerouted: true,
|
||||
shiftKey: e.shiftKey
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Install a locale from URL
|
||||
async addLocale(url) {
|
||||
let data;
|
||||
|
||||
// Load the file as JSON, using UTF-8 with or without a BOM
|
||||
try { data = JSON.parse(new TextDecoder().decode(
|
||||
await (await fetch(url)).arrayBuffer() )); }
|
||||
catch { return null; }
|
||||
|
||||
// Error checking
|
||||
if (!data.id || !data.name)
|
||||
return null;
|
||||
|
||||
// Flatten the object to keys
|
||||
let locale = new Map();
|
||||
let entries = Object.entries(data);
|
||||
let stack = [];
|
||||
while (entries.length != 0) {
|
||||
let entry = entries.shift();
|
||||
|
||||
// The value is a non-array object
|
||||
if (entry[1] instanceof Object && !Array.isArray(entry[1])) {
|
||||
entries = entries.concat(Object.entries(entry[1])
|
||||
.map(e=>[ entry[0] + "." + e[0], e[1] ]));
|
||||
}
|
||||
|
||||
// The value is a primitive or array
|
||||
else locale.set(entry[0].toLowerCase(), entry[1]);
|
||||
}
|
||||
|
||||
this.locales.set(data.id, locale);
|
||||
return data.id;
|
||||
}
|
||||
|
||||
// Specify a localization dictionary
|
||||
setLocale(id) {
|
||||
if (!this.locales.has(id))
|
||||
return false;
|
||||
this.locale = this.locales.get(id);
|
||||
for (let comp of this.components)
|
||||
comp.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Begin dragging on an element
|
||||
get drag() { return this.dragElement; }
|
||||
set drag(event) {
|
||||
|
||||
// Begin dragging
|
||||
if (event) {
|
||||
this.dragElement = event.currentTarget;
|
||||
this.dragPointer = event.pointerId;
|
||||
this.dragElement.setPointerCapture(event.pointerId);
|
||||
}
|
||||
|
||||
// End dragging
|
||||
else {
|
||||
if (this.dragElement)
|
||||
this.dragElement.releasePointerCapture(this.dragPointer);
|
||||
this.dragElement = null;
|
||||
this.dragPointer = null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Configure components for automatic localization, or localize a message
|
||||
localize(a, b) {
|
||||
return a instanceof Object ? this.localizeComponents(a, b) :
|
||||
this.localizeMessage(a, b);
|
||||
}
|
||||
|
||||
// Return focus to the most recent focused element
|
||||
restoreFocus() {
|
||||
|
||||
// Error checking
|
||||
if (!this.lastFocus)
|
||||
return false;
|
||||
|
||||
// Unable to restore focus
|
||||
if (!this.isVisible(this.lastFocus))
|
||||
return false;
|
||||
|
||||
// Transfer focus to the most recent element
|
||||
this.lastFocus.focus({ preventScroll: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Configure components for automatic localization
|
||||
localizeComponents(comps, add) {
|
||||
|
||||
// Process all components
|
||||
for (let comp of (Array.isArray(comps) ? comps : [comps])) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!(comp instanceof Toolkit.Component) ||
|
||||
!(comp.localize instanceof Function)
|
||||
) continue;
|
||||
|
||||
// Update the collection and component text
|
||||
this.components[add ? "add" : "delete"](comp);
|
||||
comp.localize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Localize a message
|
||||
localizeMessage(message, substs, circle = new Set()) {
|
||||
let parts = [];
|
||||
|
||||
// Separate the substitution keys from the literal text
|
||||
for (let x = 0;;) {
|
||||
|
||||
// Locate the start of the next substitution key
|
||||
let y = message.indexOf("{", x);
|
||||
let z = y == -1 ? -1 : message.indexOf("}", y + 1);
|
||||
|
||||
// No substitution key or malformed substitution expression
|
||||
if (z == -1) {
|
||||
parts.push(message.substring(z == -1 ? x : y));
|
||||
break;
|
||||
}
|
||||
|
||||
// Append the literal text and the substitution key
|
||||
parts.push(message.substring(x, y), message.substring(y + 1, z));
|
||||
x = z + 1;
|
||||
}
|
||||
|
||||
// Process all substitutions
|
||||
for (let x = 1; x < parts.length; x += 2) {
|
||||
let key = parts[x].toLowerCase();
|
||||
let value;
|
||||
|
||||
// The substitution key is already in the recursion chain
|
||||
if (circle.has(key)) {
|
||||
parts[x] = "{\u21ba" + key.toUpperCase() + "}";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve the substitution key from the argument
|
||||
if (substs && substs.has(key)) {
|
||||
value = substs.get(key);
|
||||
|
||||
// Do not recurse for this substitution
|
||||
if (!value[1]) {
|
||||
parts[x] = value[0];
|
||||
continue;
|
||||
}
|
||||
|
||||
// Substitution text
|
||||
value = value[0];
|
||||
}
|
||||
|
||||
// Resolve the substitution from the current locale
|
||||
else if (this.locale && this.locale.has(key))
|
||||
value = this.locale.get(key);
|
||||
|
||||
// A matching substitution key was not found
|
||||
else {
|
||||
parts[x] = "{\u00d7" + key.toUpperCase() + "}";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Perform recursive substitution
|
||||
circle.add(key);
|
||||
parts[x] = this.localizeMessage(value, substs, circle);
|
||||
circle.delete(key);
|
||||
}
|
||||
|
||||
return parts.join("");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,119 @@
|
|||
let register = Toolkit => Toolkit.Button =
|
||||
|
||||
// Push button
|
||||
class Button extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk button",
|
||||
role : "button",
|
||||
tabIndex: "0"
|
||||
}, options));
|
||||
|
||||
// Configure options
|
||||
if ("disabled" in options)
|
||||
this.disabled = options.disabled;
|
||||
this.doNotFocus = !("doNotFocus" in options) || options.doNotFocus;
|
||||
|
||||
// Display text
|
||||
this.content = new Toolkit.Label(app);
|
||||
this.add(this.content);
|
||||
|
||||
// Event handlers
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("pointermove", e=>this.onPointerMove(e));
|
||||
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
if (
|
||||
!(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) &&
|
||||
(e.key == " " || e.key == "Enter")
|
||||
) this.activate();
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
|
||||
// Gain focus
|
||||
if (
|
||||
!this.doNotFocus &&
|
||||
this.isFocusable() &&
|
||||
this.element != document.activeElement
|
||||
) this.element.focus();
|
||||
else e.preventDefault();
|
||||
|
||||
// Do not drag
|
||||
if (
|
||||
e.button != 0 ||
|
||||
this.disabled ||
|
||||
this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Begin dragging
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.classList.add("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Do not drag
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Process dragging
|
||||
this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Do not activate
|
||||
if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// End dragging
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.classList.remove("pushed");
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Activate the button if applicable
|
||||
if (this.isWithin(e))
|
||||
this.activate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Simulate a click on the button
|
||||
activate() {
|
||||
if (!this.disabled)
|
||||
this.element.dispatchEvent(new Event("action"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeText(this.content);
|
||||
this.localizeLabel();
|
||||
this.localizeTitle();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,157 @@
|
|||
let register = Toolkit => Toolkit.Checkbox =
|
||||
|
||||
// Check box
|
||||
class Checkbox extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk checkbox",
|
||||
role : "checkbox",
|
||||
tabIndex: "0"
|
||||
}, options, { style: Object.assign({
|
||||
alignItems : "center",
|
||||
display : "inline-grid",
|
||||
gridTemplateColumns: "max-content auto"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure element
|
||||
this.element.setAttribute("aria-checked", "false");
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("pointermove", e=>this.onPointerMove(e));
|
||||
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
|
||||
|
||||
// Icon area
|
||||
this.box = document.createElement("div");
|
||||
this.box.className = "tk box";
|
||||
this.append(this.box);
|
||||
|
||||
// Display text
|
||||
this.uiLabel = new Toolkit.Label(app);
|
||||
this.add(this.uiLabel);
|
||||
|
||||
// Configure options
|
||||
this.checked = options.checked;
|
||||
this.disabled = !!options.disabled;
|
||||
this.instant = !!options.instant;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
if (
|
||||
!(e.altKey || e.ctrlKey || e.shiftKey || this.disabled) &&
|
||||
(e.key == " " || e.key == "Enter")
|
||||
) this.setChecked(!this.checked);
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
|
||||
// Gain focus
|
||||
if (!this.disabled)
|
||||
this.element.focus();
|
||||
else e.preventDefault();
|
||||
|
||||
// Do not drag
|
||||
if (
|
||||
e.button != 0 ||
|
||||
this.disabled ||
|
||||
this.element.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Begin dragging
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
|
||||
// Use instant activation
|
||||
if (this.instant)
|
||||
return this.onPointerUp(e);
|
||||
|
||||
// Do not use instant activation
|
||||
this.element.classList.add("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Do not drag
|
||||
if (!this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Process dragging
|
||||
this.element.classList[this.isWithin(e) ? "add" : "remove"]("pushed");
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Do not activate
|
||||
if (e.button != 0 || !this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// End dragging
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.classList.remove("pushed");
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Activate the check box if applicable
|
||||
if (this.isWithin(e))
|
||||
this.setChecked(this.checked !== true);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The check box is checked
|
||||
get checked() {
|
||||
let ret = this.element.getAttribute("aria-checked");
|
||||
return ret == "mixed" ? ret : ret == "true";
|
||||
}
|
||||
set checked(checked) {
|
||||
checked = checked == "mixed" ? checked : !!checked;
|
||||
if (checked == this.checked)
|
||||
return;
|
||||
this.element.setAttribute("aria-checked", checked);
|
||||
}
|
||||
|
||||
// Specify the display text
|
||||
setText(text, localize) {
|
||||
this.uiLabel.setText(text, localize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.uiLabel.localize();
|
||||
this.localizeTitle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Specify the checked state
|
||||
setChecked(checked) {
|
||||
checked = !!checked;
|
||||
if (checked == this.checked)
|
||||
return;
|
||||
let previous = this.checked
|
||||
this.checked = checked;
|
||||
this.element.dispatchEvent(
|
||||
Object.assign(new Event("input"), { previous: previous }));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,473 @@
|
|||
let register = Toolkit => Toolkit.Component =
|
||||
|
||||
// Base class from which all toolkit components are derived
|
||||
class Component {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Non-attributes
|
||||
static NON_ATTRIBUTES = new Set([
|
||||
"checked", "disabled", "doNotFocus", "group", "hover", "max", "min",
|
||||
"name", "orientation", "overflowX", "overflowY", "tag", "text",
|
||||
"value", "view", "visibility", "visible"
|
||||
]);
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
|
||||
// Configure element
|
||||
this.element = document.createElement(options.tag || "div");
|
||||
this.element.component = this;
|
||||
for (let entry of Object.entries(options)) {
|
||||
if (
|
||||
Toolkit.Component.NON_ATTRIBUTES.has(entry[0]) ||
|
||||
entry[0] == "type" && options.tag != "input"
|
||||
) continue;
|
||||
if (entry[0] == "style" && entry[1] instanceof Object)
|
||||
Object.assign(this.element.style, entry[1]);
|
||||
else this.element.setAttribute(entry[0], entry[1]);
|
||||
}
|
||||
|
||||
// Configure instance fields
|
||||
this._isLocalized = false;
|
||||
this.app = app;
|
||||
this.display = options.style && options.style.display;
|
||||
this.style = this.element.style;
|
||||
this.text = null;
|
||||
this.visibility = !!options.visibility;
|
||||
this.visible = !("visible" in options) || options.visible;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a child component
|
||||
add(comp) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
!(comp instanceof Toolkit.Component) ||
|
||||
comp instanceof Toolkit.App ||
|
||||
comp.app != (this.app || this)
|
||||
) return false;
|
||||
|
||||
// No components have been added yet
|
||||
if (!this.children)
|
||||
this.children = [];
|
||||
|
||||
// The child already has a parent: remove it
|
||||
if (comp.parent) {
|
||||
comp.parent.children.splice(
|
||||
comp.parent.children.indexOf(comp), 1);
|
||||
}
|
||||
|
||||
// Add the component to self
|
||||
this.children.push(comp);
|
||||
this.append(comp.element);
|
||||
comp.parent = this;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Register an event listener on the element
|
||||
addEventListener(type, listener) {
|
||||
|
||||
// No event listeners have been registered yet
|
||||
if (!this.listeners)
|
||||
this.listeners = new Map();
|
||||
if (!this.listeners.has(type))
|
||||
this.listeners.set(type, []);
|
||||
|
||||
// The listener has already been registered for this event
|
||||
let listeners = this.listeners.get(type);
|
||||
if (listeners.indexOf(listener) != -1)
|
||||
return listener;
|
||||
|
||||
// Resize events are implemented by a ResizeObserver
|
||||
if (type == "resize") {
|
||||
if (!this.resizeObserver) {
|
||||
this.resizeObserver = new ResizeObserver(()=>
|
||||
this.element.dispatchEvent(new Event("resize")));
|
||||
this.resizeObserver.observe(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
// Visibility events are implemented by an IntersectionObserver
|
||||
else if (type == "visibility") {
|
||||
if (!this.visibilityObserver) {
|
||||
this.visibilityObserver = new IntersectionObserver(
|
||||
()=>this.element.dispatchEvent(Object.assign(
|
||||
new Event("visibility"),
|
||||
{ visible: this.isVisible() }
|
||||
)),
|
||||
{ root: document.body }
|
||||
);
|
||||
this.visibilityObserver.observe(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
// Register the listener with the element
|
||||
listeners.push(listener);
|
||||
this.element.addEventListener(type, listener);
|
||||
return listener;
|
||||
}
|
||||
|
||||
// Component cannot be interacted with
|
||||
get disabled() { return this.element.hasAttribute("disabled"); }
|
||||
set disabled(disabled) { this.setDisabled(disabled); }
|
||||
|
||||
// Move focus into the component
|
||||
focus() {
|
||||
this.element.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
// Specify whether the component is localized
|
||||
get isLocalized() { return this._isLocalized; }
|
||||
set isLocalized(isLocalized) {
|
||||
if (isLocalized == this._isLocalized)
|
||||
return;
|
||||
this._isLocalized = isLocalized;
|
||||
(this instanceof Toolkit.App ? this : this.app)
|
||||
.localize(this, isLocalized);
|
||||
}
|
||||
|
||||
// Determine whether an element is actually visible
|
||||
isVisible(element = this.element) {
|
||||
if (!document.body.contains(element))
|
||||
return false;
|
||||
for (; element instanceof Element; element = element.parentNode) {
|
||||
let style = getComputedStyle(element);
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Produce an ordered list of registered event listeners for an event type
|
||||
listEventListeners(type) {
|
||||
return this.listeners && this.listeners.has(type) &&
|
||||
this.listeners.get(type).list.slice() || [];
|
||||
}
|
||||
|
||||
// Remove a child component
|
||||
remove(comp) {
|
||||
if (comp.parent != this || !this.children)
|
||||
return false;
|
||||
let index = this.children.indexOf(comp);
|
||||
if (index == -1)
|
||||
return false;
|
||||
this.children.splice(index, 1);
|
||||
comp.element.remove();
|
||||
comp.parent = null;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Unregister an event listener from the element
|
||||
removeEventListener(type, listener) {
|
||||
|
||||
// Not listening to events of the specified type
|
||||
if (!this.listeners || !this.listeners.has(type))
|
||||
return listener;
|
||||
|
||||
// Listener is not registered
|
||||
let listeners = this.listeners.get(type);
|
||||
let index = listeners.indexOf(listener);
|
||||
if (index == -1)
|
||||
return listener;
|
||||
|
||||
// Unregister the listener
|
||||
this.element.removeEventListener(listener);
|
||||
listeners.splice(index, 1);
|
||||
|
||||
// Delete the ResizeObserver
|
||||
if (
|
||||
type == "resize" &&
|
||||
listeners.list.length == 0 &&
|
||||
this.resizeObserver
|
||||
) {
|
||||
this.resizeObserver.disconnect();
|
||||
delete this.resizeObserver;
|
||||
}
|
||||
|
||||
// Delete the IntersectionObserver
|
||||
else if (
|
||||
type == "visibility" &&
|
||||
listeners.list.length == 0 &&
|
||||
this.visibilityObserver
|
||||
) {
|
||||
this.visibilityObserver.disconnect();
|
||||
delete this.visibilityObserver;
|
||||
}
|
||||
|
||||
return listener;
|
||||
}
|
||||
|
||||
// Specify accessible name
|
||||
setLabel(text, localize) {
|
||||
|
||||
// Label is another component
|
||||
if (
|
||||
text instanceof Toolkit.Component ||
|
||||
text instanceof HTMLElement
|
||||
) {
|
||||
this.element.setAttribute("aria-labelledby",
|
||||
(text.element || text).id);
|
||||
this.setString("label", null, false);
|
||||
}
|
||||
|
||||
// Label is the given text
|
||||
else {
|
||||
this.element.removeAttribute("aria-labelledby");
|
||||
this.setString("label", text, localize);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify role description text
|
||||
setRoleDescription(text, localize) {
|
||||
this.setString("roleDescription", text, localize);
|
||||
}
|
||||
|
||||
// Specify inner text
|
||||
setText(text, localize) {
|
||||
this.setString("text", text, localize);
|
||||
}
|
||||
|
||||
// Specify tooltip text
|
||||
setTitle(text, localize) {
|
||||
this.setString("title", text, localize);
|
||||
}
|
||||
|
||||
// Specify substitution text
|
||||
substitute(key, text = null, recurse = false) {
|
||||
if (text === null) {
|
||||
if (this.substitutions.has(key))
|
||||
this.substitutions.delete(key);
|
||||
} else this.substitutions.set(key, [ text, recurse ]);
|
||||
if (this.localize instanceof Function)
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Determine whether the element wants to be visible
|
||||
get visible() {
|
||||
let style = this.element.style;
|
||||
return style.display != "none" && style.visibility != "hidden";
|
||||
}
|
||||
|
||||
// Specify whether the element is visible
|
||||
set visible(visible) {
|
||||
visible = !!visible;
|
||||
|
||||
// Visibility is not changing
|
||||
if (visible == this.visible)
|
||||
return;
|
||||
|
||||
let comps = [ this ].concat(
|
||||
Array.from(this.element.querySelectorAll("*"))
|
||||
.map(c=>c.component)
|
||||
).filter(c=>
|
||||
c instanceof Toolkit.Component &&
|
||||
c.listeners &&
|
||||
c.listeners.has("visibility")
|
||||
)
|
||||
;
|
||||
let prevs = comps.map(c=>c.isVisible());
|
||||
|
||||
// Allow the component to be shown
|
||||
if (visible) {
|
||||
if (!this.visibility) {
|
||||
if (this.display)
|
||||
this.element.style.display = this.display;
|
||||
else this.element.style.removeProperty("display");
|
||||
} else this.element.style.removeProperty("visibility");
|
||||
}
|
||||
|
||||
// Prevent the component from being shown
|
||||
else {
|
||||
this.element.style.setProperty(
|
||||
this.visibility ? "visibility" : "display",
|
||||
this.visibility ? "hidden" : "none"
|
||||
);
|
||||
}
|
||||
|
||||
for (let x = 0; x < comps.length; x++) {
|
||||
let comp = comps[x];
|
||||
visible = comp.isVisible();
|
||||
if (visible == prevs[x])
|
||||
continue;
|
||||
comp.element.dispatchEvent(Object.assign(
|
||||
new Event("visibility"),
|
||||
{ visible: visible }
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Add a child component to the primary client region of this component
|
||||
append(element) {
|
||||
this.element.append(element instanceof Toolkit.Component ?
|
||||
element.element : element);
|
||||
}
|
||||
|
||||
// Determine whether a component or element is a child of this component
|
||||
contains(child) {
|
||||
return this.element.contains(child instanceof Toolkit.Component ?
|
||||
child.element : child);
|
||||
}
|
||||
|
||||
// Generate a list of focusable descendant elements
|
||||
getFocusable(element = this.element) {
|
||||
let cache;
|
||||
return Array.from(element.querySelectorAll(
|
||||
"*:is(a[href],area,button,details,input,textarea,select," +
|
||||
"[tabindex='0']):not([disabled])"
|
||||
)).filter(e=>{
|
||||
for (; e instanceof Element; e = e.parentNode) {
|
||||
let style =
|
||||
(cache || (cache = new Map())).get(e) ||
|
||||
cache.set(e, getComputedStyle(e)).get(e)
|
||||
;
|
||||
if (style.display == "none" || style.visibility == "hidden")
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Specify the inner text of the primary client region of this component
|
||||
get innerText() { return this.element.textContent; }
|
||||
set innerText(text) { this.element.innerText = text; }
|
||||
|
||||
// Determine whether an element is focusable
|
||||
isFocusable(element = this.element) {
|
||||
return element.matches(
|
||||
":is(a[href],area,button,details,input,textarea,select," +
|
||||
"[tabindex='0'],[tabindex='-1']):not([disabled])"
|
||||
);
|
||||
}
|
||||
|
||||
// Determine whether a pointer event is within the element
|
||||
isWithin(e, element = this.element) {
|
||||
let bounds = element.getBoundingClientRect();
|
||||
return (
|
||||
e.clientX >= bounds.left && e.clientX < bounds.right &&
|
||||
e.clientY >= bounds.top && e.clientY < bounds.bottom
|
||||
);
|
||||
}
|
||||
|
||||
// Common processing for localizing the accessible name
|
||||
localizeLabel(element = this.element) {
|
||||
|
||||
// There is no label or the label is another element
|
||||
if (!this.label || element.hasAttribute("aria-labelledby")) {
|
||||
element.removeAttribute("aria-label");
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the label
|
||||
let text = this.label;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.setAttribute("aria-label", text);
|
||||
}
|
||||
|
||||
// Common processing for localizing the accessible role description
|
||||
localizeRoleDescription(element = this.element) {
|
||||
|
||||
// There is no role description
|
||||
if (!this.roleDescription) {
|
||||
element.removeAttribute("aria-roledescription");
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the role description
|
||||
let text = this.roleDescription;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.setAttribute("aria-roledescription", text);
|
||||
}
|
||||
|
||||
// Common processing for localizing inner text
|
||||
localizeText(element = this.element) {
|
||||
|
||||
// There is no title
|
||||
if (!this.text) {
|
||||
element.innerText = "";
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the text
|
||||
let text = this.text;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.innerText = text;
|
||||
}
|
||||
|
||||
// Common processing for localizing the tooltip text
|
||||
localizeTitle(element = this.element) {
|
||||
|
||||
// There is no title
|
||||
if (!this.title) {
|
||||
element.removeAttribute("title");
|
||||
return;
|
||||
}
|
||||
|
||||
// Localize the title
|
||||
let text = this.title;
|
||||
text = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
element.setAttribute("title", text);
|
||||
}
|
||||
|
||||
// Common handler for configuring whether the component is disabled
|
||||
setDisabled(disabled, element = this.element) {
|
||||
element[disabled ? "setAttribute" : "removeAttribute"]
|
||||
("disabled", "");
|
||||
element.setAttribute("aria-disabled", disabled ? "true" : "false");
|
||||
}
|
||||
|
||||
// Specify display text
|
||||
setString(key, value, localize = true) {
|
||||
|
||||
// There is no method to update the display text
|
||||
if (!(this.localize instanceof Function))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let app = this instanceof Toolkit.App ? this : this.app;
|
||||
|
||||
// Remove the string
|
||||
if (value === null) {
|
||||
if (app && this[key] != null && this[key][1])
|
||||
app.localize(this, false);
|
||||
this[key] = null;
|
||||
}
|
||||
|
||||
// Set or replace the string
|
||||
else {
|
||||
if (app && localize && (this[key] == null || !this[key][1]))
|
||||
app.localize(this, true);
|
||||
this[key] = [ value, localize ];
|
||||
}
|
||||
|
||||
// Update the display text
|
||||
this.localize();
|
||||
}
|
||||
|
||||
// Retrieve the substitutions map
|
||||
get substitutions() {
|
||||
if (!this._substitutions)
|
||||
this._substitutions = new Map();
|
||||
return this._substitutions;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,86 @@
|
|||
let register = Toolkit => Toolkit.Desktop =
|
||||
|
||||
// Layered window manager
|
||||
class Desktop extends Toolkit.Component {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Comparator for ordering child windows
|
||||
static CHILD_ORDER(a, b) {
|
||||
return b.element.style.zIndex - a.element.style.zIndex;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class: "tk desktop"
|
||||
}, options, { style: Object.assign({
|
||||
position: "relative",
|
||||
zIndex : "0"
|
||||
}, options.style || {})} ));
|
||||
|
||||
// Configure event listeners
|
||||
this.addEventListener("resize", e=>this.onResize());
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Element resized
|
||||
onResize() {
|
||||
|
||||
// The element is hidden: its size is indeterminate
|
||||
if (!this.isVisible())
|
||||
return;
|
||||
|
||||
// Don't allow children to be out-of-frame
|
||||
if (this.children != null) {
|
||||
for (let child of this.children) {
|
||||
child.left = child.left;
|
||||
child.top = child.top;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a child component
|
||||
add(comp) {
|
||||
super.add(comp);
|
||||
this.bringToFront(comp);
|
||||
}
|
||||
|
||||
// Retrieve the foremost visible window
|
||||
getActiveWindow() {
|
||||
if (this.children != null) {
|
||||
for (let child of this.children) {
|
||||
if (child.isVisible())
|
||||
return child;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Reorder children so that a particular child is in front
|
||||
bringToFront(child) {
|
||||
this.children.splice(this.children.indexOf(child), 1);
|
||||
this.children.push(child);
|
||||
let z = 1 - this.children.length;
|
||||
for (let child of this.children)
|
||||
child.element.style.zIndex = z++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,154 @@
|
|||
let register = Toolkit => Toolkit.DropDown =
|
||||
|
||||
class DropDown extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class: "tk drop-down",
|
||||
tag : "select"
|
||||
}, options));
|
||||
|
||||
// Configure instance fields
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add an item
|
||||
add(text, localize, value) {
|
||||
|
||||
// Record the item data
|
||||
this.items.push([ text, localize, value ]);
|
||||
|
||||
// Add an <option> element
|
||||
let option = document.createElement("option");
|
||||
this.element.append(option);
|
||||
option.innerText = !localize ? text :
|
||||
this.app.localize(text, this.substitutions);
|
||||
}
|
||||
|
||||
// Remove all items
|
||||
clear() {
|
||||
this.items.splice(0);
|
||||
this.element.replaceChildren();
|
||||
this.element.selectedIndex = -1;
|
||||
}
|
||||
|
||||
// Retrieve an item
|
||||
get(index) {
|
||||
|
||||
// Error checking
|
||||
if (index < 0 || index >= this.items.length)
|
||||
return null;
|
||||
|
||||
// Return the item as an item with properties
|
||||
let item = this.items[item];
|
||||
return {
|
||||
localize: item[1],
|
||||
text : item[0],
|
||||
value : item[2]
|
||||
};
|
||||
}
|
||||
|
||||
// Number of items in the list
|
||||
get length() { return this.items.length; }
|
||||
set length(v) { }
|
||||
|
||||
// Remove an item
|
||||
remove(index) {
|
||||
|
||||
// Error checking
|
||||
if (index < 0 || index >= this.length)
|
||||
return;
|
||||
|
||||
// Determine what selectedIndex will be after the operation
|
||||
let newIndex = index;
|
||||
if (
|
||||
newIndex <= this.selectedIndex ||
|
||||
newIndex == this.length - 1
|
||||
) newIndex--;
|
||||
if (
|
||||
newIndex == -1 &&
|
||||
this.length != 0
|
||||
) newIndex = 0;
|
||||
|
||||
// Remove the item
|
||||
this.items.splice(index, 1);
|
||||
this.element.options[index].remove();
|
||||
this.element.selectedIndex = newIndex;
|
||||
}
|
||||
|
||||
// Index of the currently selected item
|
||||
get selectedIndex() { return this.element.selectedIndex; }
|
||||
set selectedIndex(index) {
|
||||
if (index < -1 || index >= this.items.length)
|
||||
return this.element.selectedIndex;
|
||||
return this.element.selectedIndex = index;
|
||||
}
|
||||
|
||||
// Update an item
|
||||
set(index, text, localize) {
|
||||
|
||||
// Error checking
|
||||
if (index < 0 || index >= this.items.length)
|
||||
return;
|
||||
|
||||
// Replace the item data
|
||||
this.items[index] = [ text, localize ];
|
||||
|
||||
// Configure the <option> element
|
||||
this.element.options[index].innerText = !localize ? text :
|
||||
this.app.localize(text, this.substitutions);
|
||||
}
|
||||
|
||||
// Update the selectedIndex property, firing an event
|
||||
setSelectedIndex(index) {
|
||||
|
||||
// Error checking
|
||||
if (index < -1 || index >= this.items.length)
|
||||
return this.selectedIndex;
|
||||
|
||||
// Update the element and fire an event
|
||||
this.element.selectedIndex = index;
|
||||
this.element.dispatchEvent(new Event("input"));
|
||||
return index;
|
||||
}
|
||||
|
||||
// Currently selected value
|
||||
get value() {
|
||||
return this.selectedIndex == -1 ? null :
|
||||
this.items[this.selectedIndex][2];
|
||||
}
|
||||
set value(value) {
|
||||
let index = this.items.findIndex(i=>i[2] == value);
|
||||
if (index != -1)
|
||||
this.selectedIndex = index;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
|
||||
// Label and title
|
||||
this.localizeLabel();
|
||||
this.localizeTitle();
|
||||
|
||||
// Items
|
||||
for (let x = 0; x < this.items.length; x++) {
|
||||
let item = this.items[x];
|
||||
this.element.options[x].innerText = !item[1] ? item[0] :
|
||||
this.app.localize(item[0], this.substitutions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,46 @@
|
|||
let register = Toolkit => Toolkit.Label =
|
||||
|
||||
// Presentational text
|
||||
class Label extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}, autoId = false) {
|
||||
super(app, Object.assign({
|
||||
class: "tk label"
|
||||
}, options, { style: Object.assign({
|
||||
cursor : "default",
|
||||
userSelect: "none",
|
||||
whiteSpace: "nowrap"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
if (autoId)
|
||||
this.id = Toolkit.id();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify the display text
|
||||
setText(text, localize) {
|
||||
this.setString("text", text, localize);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
if (this.text != null) {
|
||||
let text = this.text;
|
||||
this.element.innerText = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,92 @@
|
|||
let register = Toolkit => Toolkit.Menu =
|
||||
|
||||
// Pop-up menu container
|
||||
class Menu extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class : "tk menu",
|
||||
id : Toolkit.id(),
|
||||
role : "menu",
|
||||
visibility: true,
|
||||
visible : false
|
||||
}, options, { style: Object.assign({
|
||||
display : "inline-flex",
|
||||
flexDirection: "column",
|
||||
position : "absolute",
|
||||
zIndex : "1"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
this.parent = null;
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("focusout", e=>this.onBlur(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Child blur
|
||||
onBlur(e) {
|
||||
if (this.parent instanceof Toolkit.MenuItem)
|
||||
this.parent.onBlur(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a child menu item
|
||||
add(item) {
|
||||
if (!(item instanceof Toolkit.MenuItem) || !super.add(item))
|
||||
return false;
|
||||
this.detectIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add a sepearator
|
||||
addSeparator() {
|
||||
let item = new Toolkit.Component(this.app, { role: "separator" });
|
||||
super.add(item);
|
||||
return item;
|
||||
}
|
||||
|
||||
// Remove the menu from its parent menu item or remove a child menu item
|
||||
remove() {
|
||||
|
||||
// Remove child
|
||||
if (arguments.length != 0) {
|
||||
if (!super.remove.apply(this, arguments))
|
||||
return false;
|
||||
this.detectIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove from parent
|
||||
this.parent = null;
|
||||
this.element.remove();
|
||||
this.element.removeAttribute("aria-labelledby");
|
||||
this.setVisible(false);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Show or hide the icons column
|
||||
detectIcons() {
|
||||
this.element.classList[
|
||||
this.children && this.children.find(i=>
|
||||
i.visible && i.type == "checkbox") ?
|
||||
"add" : "remove"
|
||||
]("icons");
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,106 @@
|
|||
let register = Toolkit => Toolkit.MenuBar =
|
||||
|
||||
// Application menu bar
|
||||
class MenuBar extends Toolkit.Menu {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
class : "tk menu-bar",
|
||||
role : "menubar",
|
||||
visibility: false,
|
||||
visible : true
|
||||
}, options, { style: Object.assign({
|
||||
display : "flex",
|
||||
flexDirection: "row",
|
||||
minWidth : "0",
|
||||
position : "inline"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure event handlers
|
||||
this.addEventListener("focusout", e=>this.onBlur (e));
|
||||
this.addEventListener("focusin" , e=>this.onFocus(e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Child blur
|
||||
onBlur(e) {
|
||||
if (
|
||||
this.contains(e.relatedTarget) ||
|
||||
!this.children || this.children.length == 0
|
||||
) return;
|
||||
this.children.forEach(i=>i.expanded = false);
|
||||
this.children[0].element.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
// Child focus
|
||||
onFocus(e) {
|
||||
if (!this.children || this.children.length == 0)
|
||||
return;
|
||||
this.children[0].element.setAttribute("tabindex", "-1");
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Add a menu item
|
||||
add(item) {
|
||||
super.add(item);
|
||||
if (item.menu)
|
||||
this.element.append(item.menu.element);
|
||||
this.children[0].element.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
// Move focus into the component
|
||||
focus() {
|
||||
if (this.children.length != 0)
|
||||
this.children[0].focus();
|
||||
}
|
||||
|
||||
// Remove a menu item
|
||||
remove(item) {
|
||||
|
||||
// Remove the menu item
|
||||
if (item.parent == this && item.menu)
|
||||
item.menu.remove();
|
||||
super.remove(item);
|
||||
|
||||
// Configure focusability
|
||||
if (this.children && !this.contains(document.activeElement)) {
|
||||
for (let x = 0; x < this.children.length; x++) {
|
||||
this.children[x].element
|
||||
.setAttribute("tabindex", x == 0 ? "0" : "-1");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Return focus to the application
|
||||
blur() {
|
||||
if (this.children) {
|
||||
for (let item of this.children) {
|
||||
item.expanded = false;
|
||||
item.element.blur();
|
||||
}
|
||||
}
|
||||
if (!this.parent || !this.parent.restoreFocus())
|
||||
this.focus();
|
||||
}
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeLabel(this.element);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,455 @@
|
|||
let register = Toolkit => Toolkit.MenuItem =
|
||||
|
||||
// Selection within a MenuBar or Menu
|
||||
class MenuItem extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk menu-item",
|
||||
id : Toolkit.id(),
|
||||
role : "menuitem",
|
||||
tabIndex: "-1"
|
||||
}, options));
|
||||
|
||||
// Configure instance fields
|
||||
this._expanded = false;
|
||||
this._menu = null;
|
||||
|
||||
// Element
|
||||
this.content = document.createElement("div");
|
||||
this.element.append(this.content);
|
||||
this.disabled = options.disabled;
|
||||
|
||||
// Icon column
|
||||
this.icon = document.createElement("div");
|
||||
this.icon.className = "icon";
|
||||
this.content.append(this.icon);
|
||||
|
||||
// Label column
|
||||
this.label = document.createElement("div");
|
||||
this.label.className = "label";
|
||||
this.content.append(this.label);
|
||||
|
||||
// Control type
|
||||
switch (options.type) {
|
||||
case "checkbox":
|
||||
this.type = "checkbox";
|
||||
this.checked = !!options.checked;
|
||||
break;
|
||||
default:
|
||||
this.type = "normal";
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
this.addEventListener("focusout" , e=>this.onBlur (e));
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown (e));
|
||||
this.addEventListener("pointerdown", e=>this.onPointerDown(e));
|
||||
this.addEventListener("pointermove", e=>this.onPointerMove(e));
|
||||
this.addEventListener("pointerup" , e=>this.onPointerUp (e));
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Focus lost
|
||||
onBlur(e) {
|
||||
if (this.menu && !this.contains(e.relatedTarget))
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Do not process the event
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let isBar = this.parent && this.parent instanceof Toolkit.MenuBar;
|
||||
let next = isBar ? "ArrowRight": "ArrowDown";
|
||||
let prev = isBar ? "ArrowLeft" : "ArrowUp";
|
||||
let siblings = this.parent && this.parent.children ?
|
||||
this.parent.children
|
||||
.filter(i=>i instanceof Toolkit.MenuItem && i.visible) :
|
||||
[ this ];
|
||||
let index = siblings.indexOf(this);
|
||||
let handled = false;
|
||||
|
||||
// Process by relative key code
|
||||
switch (e.key) {
|
||||
|
||||
// Select the next sibling
|
||||
case next:
|
||||
index = index == -1 ? 0 :
|
||||
(index + 1) % siblings.length;
|
||||
if (index < siblings.length) {
|
||||
let sibling = siblings[index];
|
||||
if (isBar && sibling.menu && this.expanded)
|
||||
sibling.expanded = true;
|
||||
sibling.focus();
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
// Select the previous sibling
|
||||
case prev:
|
||||
index = index == -1 ? 0 :
|
||||
(index + siblings.length - 1) % siblings.length;
|
||||
if (index < siblings.length) {
|
||||
let sibling = siblings[index];
|
||||
if (isBar && sibling.menu && this.expanded)
|
||||
sibling.expanded = true;
|
||||
sibling.focus();
|
||||
}
|
||||
handled = true;
|
||||
break;
|
||||
|
||||
}
|
||||
|
||||
// Process by absolute key code
|
||||
if (!handled) switch (e.key) {
|
||||
|
||||
// Activate the menu item with handling for checks and radios
|
||||
case " ":
|
||||
this.activate(false);
|
||||
break;
|
||||
|
||||
// Activate the menu item if in a MenuBar
|
||||
case "ArrowDown":
|
||||
if (isBar && this.menu)
|
||||
this.activate();
|
||||
break;
|
||||
|
||||
// Cycle through menu items in a MenuBar
|
||||
case "ArrowLeft":
|
||||
case "ArrowRight": {
|
||||
let root = this.getRoot();
|
||||
if (!(root instanceof Toolkit.MenuBar))
|
||||
break;
|
||||
let top = root.children.find(i=>i.expanded);
|
||||
if (top)
|
||||
return top.onKeyDown(e);
|
||||
break;
|
||||
}
|
||||
|
||||
// Select the last sibling
|
||||
case "End":
|
||||
if (siblings.length != 0)
|
||||
siblings[siblings.length - 1].focus();
|
||||
break;
|
||||
|
||||
// Activate the menu item
|
||||
case "Enter":
|
||||
this.activate();
|
||||
break;
|
||||
|
||||
// Deactivate the menu and return to the parent menu item
|
||||
case "Escape": {
|
||||
if (this.expanded)
|
||||
this.expanded = false;
|
||||
else if (this.parent &&
|
||||
this.parent.parent instanceof Toolkit.MenuItem) {
|
||||
this.parent.parent.expanded = false;
|
||||
this.parent.parent.focus();
|
||||
} else if (this.parent instanceof Toolkit.MenuBar)
|
||||
this.parent.blur();
|
||||
break;
|
||||
}
|
||||
|
||||
// Select the first sibling
|
||||
case "Home":
|
||||
if (siblings.length != 0)
|
||||
siblings[0].focus();
|
||||
break;
|
||||
|
||||
// Select the next menu item that begins with the typed character
|
||||
default: {
|
||||
if (e.key.length != 1)
|
||||
return;
|
||||
let key = e.key.toLowerCase();
|
||||
for (let x = 0; x < siblings.length; x++) {
|
||||
let sibling = siblings[(index + x + 1) % siblings.length];
|
||||
if (
|
||||
(sibling.content.textContent || " ")[0]
|
||||
.toLowerCase() == key
|
||||
) {
|
||||
if (sibling.menu)
|
||||
sibling.expanded = true;
|
||||
if (sibling.menu && sibling.menu.children &&
|
||||
sibling.menu.children[0])
|
||||
sibling.menu.children[0].focus();
|
||||
else sibling.focus();
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!handled)
|
||||
return;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer down
|
||||
onPointerDown(e) {
|
||||
this.focus();
|
||||
|
||||
// Do not process the event
|
||||
if (e.button != 0 || this.element.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
if (this.disabled)
|
||||
return Toolkit.handle(e);
|
||||
|
||||
// Does not contain a menu
|
||||
if (!this.menu) {
|
||||
this.element.setPointerCapture(e.pointerId);
|
||||
this.element.classList.add("pushed");
|
||||
}
|
||||
|
||||
// Does contain a menu
|
||||
else this.expanded = !this.expanded;
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer move
|
||||
onPointerMove(e) {
|
||||
|
||||
// Do not process the event
|
||||
if (this.disabled)
|
||||
return Toolkit.handle(e);
|
||||
|
||||
// Not dragging within element
|
||||
if (!this.element.hasPointerCapture(e.pointerId)) {
|
||||
let other = this.parent&&this.parent.children.find(i=>i.expanded);
|
||||
if (other && other != this) {
|
||||
this.expanded = true;
|
||||
this.focus();
|
||||
}
|
||||
}
|
||||
|
||||
// Dragging within element
|
||||
else this.element.classList
|
||||
[this.isWithin(e) ? "add" : "remove"]("pushed");
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Pointer up
|
||||
onPointerUp(e) {
|
||||
|
||||
// Do not process the event
|
||||
if (e.button != 0)
|
||||
return;
|
||||
if (this.disabled)
|
||||
return Toolkit.handle(e);
|
||||
|
||||
// Stop dragging
|
||||
if (this.element.hasPointerCapture(e.pointerId)) {
|
||||
this.element.releasePointerCapture(e.pointerId);
|
||||
this.element.classList.remove("pushed");
|
||||
}
|
||||
|
||||
// Activate the menu item
|
||||
if (!this.menu && this.isWithin(e))
|
||||
this.activate();
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Specify whether or not the checkbox is checked
|
||||
get checked() { return this._checked; }
|
||||
set checked(checked) {
|
||||
checked = !!checked;
|
||||
if (checked === this._checked || this._type != "checkbox")
|
||||
return;
|
||||
this._checked = checked;
|
||||
this.element.setAttribute("aria-checked", checked ? "true" : "false");
|
||||
}
|
||||
|
||||
// Determine whether a component or element is a child of this component
|
||||
contains(child) {
|
||||
return super.contains(child) || this.menu && this.menu.contains(child);
|
||||
}
|
||||
|
||||
// Enable or disable the control
|
||||
get disabled() { return this._disabled; }
|
||||
set disabled(disabled) {
|
||||
disabled = !!disabled;
|
||||
|
||||
// Error checking
|
||||
if (disabled === this._disabled)
|
||||
return;
|
||||
|
||||
// Enable or disable the control
|
||||
this._disabled = disabled;
|
||||
this.element[disabled ? "setAttribute" : "removeAttribute"]
|
||||
("aria-disabled", "true");
|
||||
if (disabled)
|
||||
this.expanded = false;
|
||||
}
|
||||
|
||||
// Expand or collapse the menu
|
||||
get expanded() { return this._expanded; }
|
||||
set expanded(expanded) {
|
||||
expanded = !!expanded;
|
||||
|
||||
// Error checking
|
||||
if (this._expanded == expanded || !this.menu)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this._expanded = expanded;
|
||||
|
||||
// Expand the menu
|
||||
if (expanded) {
|
||||
let bounds = this.element.getBoundingClientRect();
|
||||
Object.assign(this.menu.element.style, {
|
||||
left: bounds.left + "px",
|
||||
top : bounds.bottom + "px"
|
||||
});
|
||||
this.menu.visible = true;
|
||||
this.element.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
// Collapse the menu and all child sub-menus
|
||||
else {
|
||||
this.menu.visible = false;
|
||||
this.element.setAttribute("aria-expanded", "false");
|
||||
if (this.children)
|
||||
this.children.forEach(i=>i.expanded = false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Specify a new menu
|
||||
get menu() { return this._menu; }
|
||||
set menu(menu) {
|
||||
|
||||
// Error checking
|
||||
if (menu == this._menu || menu && menu.parent && menu.parent != this)
|
||||
return;
|
||||
|
||||
// Remove the current menu
|
||||
if (this._menu) {
|
||||
this.expanded = false;
|
||||
this._menu.remove();
|
||||
}
|
||||
|
||||
// Configure as regular menu item
|
||||
if (!menu) {
|
||||
this.element.removeAttribute("aria-expanded");
|
||||
this.element.removeAttribute("aria-haspopup");
|
||||
return;
|
||||
}
|
||||
|
||||
// Associate the menu with the item
|
||||
this._menu = menu;
|
||||
menu.parent = this;
|
||||
if (this.parent)
|
||||
this.element.after(menu.element);
|
||||
menu.element.setAttribute("aria-labelledby", this.element.id);
|
||||
}
|
||||
|
||||
// Specify display text
|
||||
setText(text, localize) {
|
||||
this.setString("text", text, localize);
|
||||
}
|
||||
|
||||
// Specify the menu item type
|
||||
get type() { return this._type; }
|
||||
set type(type) {
|
||||
switch (type) {
|
||||
case "checkbox":
|
||||
if (this._type == "checkbox")
|
||||
break;
|
||||
this._type = "checkbox";
|
||||
this.checked = null;
|
||||
this.element.classList.add("checkbox");
|
||||
break;
|
||||
default:
|
||||
if (this._type == "normal")
|
||||
break;
|
||||
this._type = "normal";
|
||||
this._checked = false;
|
||||
this.element.classList.remove("checkbox");
|
||||
this.element.removeAttribute("aria-checked");
|
||||
}
|
||||
}
|
||||
|
||||
// Specify whether the element is visible
|
||||
get visible() { return super.visible; }
|
||||
set visible(visible) {
|
||||
super.visible = visible = !!visible;
|
||||
if (this.parent instanceof Toolkit.Menu)
|
||||
this.parent.detectIcons();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
if (this.text != null) {
|
||||
let text = this.text;
|
||||
this.label.innerText = !text[1] ? text[0] :
|
||||
this.app.localize(text[0], this.substitutions);
|
||||
} else this.label.innerText = "";
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Activate the menu item
|
||||
activate(blur = true) {
|
||||
|
||||
// Error checking
|
||||
if (this.disabled)
|
||||
return;
|
||||
|
||||
// Expand the sub-menu
|
||||
if (this.menu) {
|
||||
this.expanded = true;
|
||||
if (this.menu.children && this.menu.children[0])
|
||||
this.menu.children[0].focus();
|
||||
else this.focus();
|
||||
}
|
||||
|
||||
// Activate the menu item
|
||||
else {
|
||||
this.element.dispatchEvent(Toolkit.event("action"));
|
||||
if (this.type == "normal" || blur) {
|
||||
let root = this.getRoot();
|
||||
if (root instanceof Toolkit.MenuBar)
|
||||
root.blur();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Locate the root Menu component
|
||||
getRoot() {
|
||||
for (let comp = this.parent; comp != this.app; comp = comp.parent) {
|
||||
if (
|
||||
comp instanceof Toolkit.Menu &&
|
||||
!(comp.parent instanceof Toolkit.MenuItem)
|
||||
) return comp;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,113 @@
|
|||
let register = Toolkit => Toolkit.Radio =
|
||||
|
||||
// Radio button group manager
|
||||
class Radio extends Toolkit.Checkbox {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options) {
|
||||
super(app, options = Object.assign({
|
||||
class: "tk radio",
|
||||
role : "radio"
|
||||
}, options || {}));
|
||||
|
||||
// Configure instance fields
|
||||
this._group = null;
|
||||
|
||||
// Configure options
|
||||
if ("group" in options)
|
||||
this.group = options.group;
|
||||
if ("checked" in options) {
|
||||
this.checked = false;
|
||||
this.checked = options.checked;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
let item = null;
|
||||
switch (e.key) {
|
||||
|
||||
// Activate the radio button
|
||||
case " ":
|
||||
case "Enter":
|
||||
this.checked = true;
|
||||
break;
|
||||
|
||||
// Focus the next button in the group
|
||||
case "ArrowDown":
|
||||
case "ArrowRight":
|
||||
if (this.group == null)
|
||||
return;
|
||||
item = this.group.next(this);
|
||||
break;
|
||||
|
||||
// Focus the previous button in the group
|
||||
case "ArrowLeft":
|
||||
case "ArrowUp":
|
||||
if (this.group == null)
|
||||
return;
|
||||
item = this.group.previous(this);
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
// Select and focus another item in the group
|
||||
if (item != null && item != this) {
|
||||
this.group.active = item;
|
||||
item.focus();
|
||||
item.element.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The check box is checked
|
||||
get checked() { return super.checked; }
|
||||
set checked(checked) {
|
||||
super.checked = checked;
|
||||
if (this.group != null && this != this.group.active && checked)
|
||||
this.group.active = this;
|
||||
}
|
||||
|
||||
// Managing radio button group
|
||||
get group() { return this._group; }
|
||||
set group(group) {
|
||||
if (group == this.group)
|
||||
return;
|
||||
if (group)
|
||||
group.add(this);
|
||||
this._group = group ? group : null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Specify the checked state
|
||||
setChecked(checked) {
|
||||
if (!checked || this.checked)
|
||||
return;
|
||||
this.checked = true;
|
||||
this.element.dispatchEvent(new Event("input"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,109 @@
|
|||
let register = Toolkit => Toolkit.RadioGroup =
|
||||
|
||||
// Radio button group manager
|
||||
class RadioGroup {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor() {
|
||||
this._active = null;
|
||||
this.items = [];
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// The current active radio button
|
||||
get active() { return this._active; }
|
||||
set active(item) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
item == this.active ||
|
||||
item != null && this.items.indexOf(item) == -1
|
||||
) return;
|
||||
|
||||
// De-select the current active item
|
||||
if (this.active != null) {
|
||||
this.active.checked = false;
|
||||
this.active.element.setAttribute("tabindex", "-1");
|
||||
}
|
||||
|
||||
// Select the new active item
|
||||
this._active = item ? item : null;
|
||||
if (item == null)
|
||||
return;
|
||||
if (!item.checked)
|
||||
item.checked = true;
|
||||
item.element.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
// Add a radio button item
|
||||
add(item) {
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
item == null ||
|
||||
this.items.indexOf(item) != -1
|
||||
) return;
|
||||
|
||||
// Remove the item from its current group
|
||||
if (item.group != null)
|
||||
item.group.remove(item);
|
||||
|
||||
// Add the item to this group
|
||||
this.items.push(item);
|
||||
item.group = this;
|
||||
item.element.setAttribute("tabindex",
|
||||
this.items.length == 0 ? "0" : "-1");
|
||||
return item;
|
||||
}
|
||||
|
||||
// Check whether an element is contained in the radio group
|
||||
contains(element) {
|
||||
if (element instanceof Toolkit.Component)
|
||||
element = element.element;
|
||||
return !!this.items.find(i=>i.element.contains(element));
|
||||
}
|
||||
|
||||
// Remove a radio button item
|
||||
remove(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
|
||||
// The item is not in the group
|
||||
if (index == -1)
|
||||
return null;
|
||||
|
||||
// Remove the item from the group
|
||||
this.items.splice(index, 1);
|
||||
item.element.setAttribute("tabindex", "0");
|
||||
if (this.active == item) {
|
||||
this.active = null;
|
||||
if (this.items.length != 0)
|
||||
this.items[0].element.setAttribute("tabindex", "0");
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Determine the next radio button in the group
|
||||
next(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
return index == -1 ? null :
|
||||
this.items[(index + 1) % this.items.length];
|
||||
}
|
||||
|
||||
// Determine the previous radio button in the group
|
||||
previous(item) {
|
||||
let index = this.items.indexOf(item);
|
||||
return index == -1 ? null :
|
||||
this.items[(index + this.items.length - 1) % this.items.length];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,380 @@
|
|||
let register = Toolkit => Toolkit.ScrollBar =
|
||||
|
||||
// Scrolling control
|
||||
class ScrollBar extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk scroll-bar",
|
||||
role : "scrollbar",
|
||||
tabIndex: "0"
|
||||
}, options, { style: Object.assign({
|
||||
display : "inline-grid",
|
||||
overflow: "hidden"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
this._blockIncrement = 50;
|
||||
this._controls = null;
|
||||
this._unitIncrement = 1;
|
||||
|
||||
// Unit decrement button
|
||||
this.unitLess = new Toolkit.Button(app,
|
||||
{ class: "unit-less", role: "", tabIndex: "" });
|
||||
this.unitLess.addEventListener("action",
|
||||
e=>this.value -= this.unitIncrement);
|
||||
this.add(this.unitLess);
|
||||
|
||||
// Component
|
||||
this.addEventListener("keydown" , e=>this.onKeyDown(e));
|
||||
this.addEventListener("pointerdown", e=>e.preventDefault());
|
||||
|
||||
// Track
|
||||
this.track = new Toolkit.Component(app, {
|
||||
class: "track",
|
||||
style: {
|
||||
display : "grid",
|
||||
overflow: "hidden"
|
||||
}
|
||||
});
|
||||
this.track.addEventListener("resize", e=>this.onResize());
|
||||
this.add(this.track);
|
||||
|
||||
// Unit increment button
|
||||
this.unitMore = new Toolkit.Button(app,
|
||||
{ class: "unit-more", role: "", tabIndex: "" });
|
||||
this.unitMore.addEventListener("action",
|
||||
e=>this.value += this.unitIncrement);
|
||||
this.add(this.unitMore);
|
||||
|
||||
// Block decrement track
|
||||
this.blockLess = new Toolkit.Button(app,
|
||||
{ class: "block-less", role: "", tabIndex: "" });
|
||||
this.blockLess.addEventListener("action",
|
||||
e=>this.value -= this.blockIncrement);
|
||||
this.track.add(this.blockLess);
|
||||
|
||||
// Scroll box
|
||||
this.thumb = document.createElement("div");
|
||||
this.thumb.className = "thumb";
|
||||
this.thumb.addEventListener("pointerdown", e=>this.onThumbDown(e));
|
||||
this.thumb.addEventListener("pointermove", e=>this.onThumbMove(e));
|
||||
this.thumb.addEventListener("pointerUp" , e=>this.onThumbUp (e));
|
||||
this.track.append(this.thumb);
|
||||
|
||||
// Block increment track
|
||||
this.blockMore = new Toolkit.Button(app,
|
||||
{ class: "block-more", role: "", tabIndex: "" });
|
||||
this.blockMore.addEventListener("action",
|
||||
e=>this.value += this.blockIncrement);
|
||||
this.track.add(this.blockMore);
|
||||
|
||||
// Configure options
|
||||
this.blockIncrement = !("blockIncrement" in options) ?
|
||||
this._blockIncrement : options.blockIncrement;
|
||||
this.orientation = !("orientation" in options) ?
|
||||
"horizontal" : options.orientation;
|
||||
this.unitIncrement = !("unitIncrement" in options) ?
|
||||
this._unitIncrement : options.unitIncrement;
|
||||
|
||||
// Configure min, max and value
|
||||
let min = "min" in options ? options.min : 0;
|
||||
let max = Math.max(min, "max" in options ? options.max : 100);
|
||||
let value = Math.max(min, Math.min(max,
|
||||
"value" in options ? options.value : 0));
|
||||
this.element.setAttribute("aria-valuemax", max);
|
||||
this.element.setAttribute("aria-valuemin", min);
|
||||
this.element.setAttribute("aria-valuenow", value);
|
||||
|
||||
// Display the element
|
||||
this.track.element.dispatchEvent(new Event("resize"));
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Take no action
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
switch (e.key) {
|
||||
|
||||
case "ArrowDown":
|
||||
if (this.orientation != "vertical")
|
||||
return;
|
||||
this.value += this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
if (this.orientation != "horizontal")
|
||||
return;
|
||||
this.value -= this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
if (this.orientation != "horizontal")
|
||||
return;
|
||||
this.value += this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "ArrowUp":
|
||||
if (this.orientation != "vertical")
|
||||
return;
|
||||
this.value -= this.unitIncrement;
|
||||
break;
|
||||
|
||||
case "PageDown":
|
||||
this.value += this.blockIncrement;
|
||||
break;
|
||||
|
||||
case "PageUp":
|
||||
this.value -= this.blockIncrement;
|
||||
break;
|
||||
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Track resized
|
||||
onResize() {
|
||||
let metrics = this.metrics();
|
||||
let add = metrics.horz ? "width" : "height";
|
||||
let remove = metrics.horz ? "height" : "width" ;
|
||||
|
||||
// Resize the widget elements
|
||||
this.blockLess.style.removeProperty(remove);
|
||||
this.blockLess.style.setProperty (add, metrics.pos + "px");
|
||||
this.thumb .style.removeProperty(remove);
|
||||
this.thumb .style.setProperty (add, metrics.thumb + "px");
|
||||
|
||||
// Indicate whether the entire view is visible
|
||||
this.element.classList
|
||||
[metrics.unneeded ? "add" : "remove"]("unneeded");
|
||||
}
|
||||
|
||||
// Thumb pointer down
|
||||
onThumbDown(e) {
|
||||
|
||||
// Prevent the user agent from focusing the element
|
||||
e.preventDefault();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
e.button != 0 ||
|
||||
this.disabled ||
|
||||
this.unneeded ||
|
||||
e.target.hasPointerCapture(e.pointerId)
|
||||
)
|
||||
return;
|
||||
|
||||
// Begin dragging
|
||||
this.drag = this.metrics();
|
||||
this.drag.start = this.drag.horz ? e.screenX : e.screenY;
|
||||
e.target.setPointerCapture(e.pointerId);
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Thumb pointer move
|
||||
onThumbMove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!e.target.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let add = this.drag.horz ? "width" : "height";
|
||||
let remove = this.drag.horz ? "height" : "width" ;
|
||||
let delta = (this.drag.horz?e.screenX:e.screenY) - this.drag.start;
|
||||
let max = this.drag.track - this.drag.thumb;
|
||||
let pos = Math.max(0, Math.min(max, this.drag.pos + delta));
|
||||
let value = Math.round(this.min + (this.max - this.min) * pos / (this.drag.track || 1));
|
||||
let scroll = value != this.value;
|
||||
|
||||
// Drag the thumb
|
||||
this.blockLess.style.removeProperty(remove);
|
||||
this.blockLess.style.setProperty (add, pos + "px");
|
||||
this.element.setAttribute("aria-valuenow", value);
|
||||
Toolkit.handle(e);
|
||||
|
||||
// Raise a scroll event
|
||||
if (scroll)
|
||||
this.event();
|
||||
|
||||
}
|
||||
|
||||
// Thumb pointer up
|
||||
onThumbUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || !e.target.hasPointerCapture(e))
|
||||
return;
|
||||
|
||||
// Stop dragging
|
||||
this.drag = null;
|
||||
e.target.releasePointerCapture(e.pointerId);
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Page adjustment amount
|
||||
get blockIncrement() { return this._blockIncrement; }
|
||||
set blockIncrement(amount) {
|
||||
amount = Math.max(1, amount);
|
||||
if (amount == this.blockIncrement)
|
||||
return;
|
||||
this._blockIncrement = amount;
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// Controlling target
|
||||
get controls() { return this._controls; }
|
||||
set controls(target) {
|
||||
if (target) {
|
||||
this.element.setAttribute("aria-controls",
|
||||
(target.element || target).id);
|
||||
} else this.element.removeAttribute("aria-controls");
|
||||
this._controls = target;
|
||||
}
|
||||
|
||||
// Component cannot be interacted with
|
||||
get disabled() { return super.disabled; }
|
||||
set disabled(disabled) {
|
||||
super .disabled = disabled;
|
||||
this.unitLess .disabled = disabled;
|
||||
this.blockLess.disabled = disabled;
|
||||
this.blockMore.disabled = disabled;
|
||||
this.unitMore .disabled = disabled;
|
||||
}
|
||||
|
||||
// Maximum value
|
||||
get max() {
|
||||
return this.blockIncrement +
|
||||
parseInt(this.element.getAttribute("aria-valuemax"));
|
||||
}
|
||||
set max(max) {
|
||||
if (max == this.max)
|
||||
return;
|
||||
if (max < this.min)
|
||||
this.element.setAttribute("aria-valuemin", max);
|
||||
if (max < this.value)
|
||||
this.element.setAttribute("aria-valuenow", max);
|
||||
this.element.setAttribute("aria-valuemax", max - this.blockIncrement);
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// Minimum value
|
||||
get min() { return parseInt(this.element.getAttribute("aria-valuemin")); }
|
||||
set min(min) {
|
||||
if (min == this.min)
|
||||
return;
|
||||
if (min > this.max)
|
||||
this.element.setAttribute("aria-valuemax",min-this.blockIncrement);
|
||||
if (min > this.value)
|
||||
this.element.setAttribute("aria-valuenow", min);
|
||||
this.element.setAttribute("aria-valuemin", min);
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// Layout direction
|
||||
get orientation() { return this.element.getAttribute("aria-orientation"); }
|
||||
set orientation(orientation) {
|
||||
|
||||
// Orientation is not changing
|
||||
if (orientation == this.orientation)
|
||||
return;
|
||||
|
||||
// Select which CSS properties to modify
|
||||
let add, remove;
|
||||
switch (orientation) {
|
||||
case "horizontal":
|
||||
add = "grid-template-columns";
|
||||
remove = "grid-template-rows";
|
||||
break;
|
||||
case "vertical":
|
||||
add = "grid-template-rows";
|
||||
remove = "grid-template-columns";
|
||||
break;
|
||||
default: return; // Invalid orientation
|
||||
}
|
||||
|
||||
// Configure the element
|
||||
this.element.style.removeProperty(remove);
|
||||
this.element.style.setProperty(add, "max-content auto max-content");
|
||||
this.element.setAttribute("aria-orientation", orientation);
|
||||
this.track .style.removeProperty(remove);
|
||||
this.track .style.setProperty(add, "max-content max-content auto");
|
||||
}
|
||||
|
||||
// Line adjustment amount
|
||||
get unitIncrement() { return this._unitIncrement; }
|
||||
set unitIncrement(amount) {
|
||||
amount = Math.max(1, amount);
|
||||
if (amount == this.unitIncrement)
|
||||
return;
|
||||
this._unitIncrement = amount;
|
||||
this.onResize();
|
||||
}
|
||||
|
||||
// The scroll bar is not needed
|
||||
get unneeded() { return this.element.classList.contains("unneeded"); }
|
||||
set unneeded(x) { }
|
||||
|
||||
// Current value
|
||||
get value() {return parseInt(this.element.getAttribute("aria-valuenow"));}
|
||||
set value(value) {
|
||||
value = Math.min(this.max - this.blockIncrement,
|
||||
Math.max(this.min, value));
|
||||
if (value == this.value)
|
||||
return;
|
||||
this.element.setAttribute("aria-valuenow", value);
|
||||
this.onResize();
|
||||
this.event();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Raise a scroll event
|
||||
event() {
|
||||
this.element.dispatchEvent(
|
||||
Object.assign(new Event("scroll"), { scroll: this.value }));
|
||||
}
|
||||
|
||||
// Compute pixel dimensions of inner elements
|
||||
metrics() {
|
||||
let horz = this.orientation == "horizontal";
|
||||
let track = this.track.element.getBoundingClientRect()
|
||||
[horz ? "width" : "height"];
|
||||
let block = this.blockIncrement;
|
||||
let range = this.max - this.min || 1;
|
||||
let thumb = block >= range ? track :
|
||||
Math.min(track, Math.max(4, Math.round(block * track / range)));
|
||||
let pos = Math.round((this.value - this.min) * track / range);
|
||||
return {
|
||||
block : block,
|
||||
horz : horz,
|
||||
pos : pos,
|
||||
range : range,
|
||||
thumb : thumb,
|
||||
track : track,
|
||||
unneeded: block >= range
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,317 @@
|
|||
let register = Toolkit => Toolkit.ScrollPane =
|
||||
|
||||
// Scrolling container for larger internal elements
|
||||
class ScrollPane extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class: "tk scroll-pane"
|
||||
}, options, { style: Object.assign({
|
||||
display : "inline-grid",
|
||||
gridAutoRows: "auto max-content",
|
||||
overflow : "hidden",
|
||||
position : "relative"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure options
|
||||
this._overflowX = "auto";
|
||||
this._overflowY = "auto";
|
||||
if ("overflowX" in options)
|
||||
this.overflowX = options.overflowX;
|
||||
if ("overflowY" in options)
|
||||
this.overflowY = options.overflowY;
|
||||
|
||||
// Component
|
||||
this.addEventListener("wheel", e=>this.onWheel(e));
|
||||
|
||||
// Viewport
|
||||
this.viewport = new Toolkit.Component(app, {
|
||||
class: "viewport",
|
||||
style: {
|
||||
overflow: "hidden"
|
||||
}
|
||||
});
|
||||
this.viewport.element.id = Toolkit.id();
|
||||
this.viewport.addEventListener("keydown", e=>this.onKeyDown (e));
|
||||
this.viewport.addEventListener("resize" , e=>this.onResize ( ));
|
||||
this.viewport.addEventListener("scroll" , e=>this.onInnerScroll( ));
|
||||
this.viewport.addEventListener("visibility",
|
||||
e=>{ if (e.visible) this.onResize(); });
|
||||
this.add(this.viewport);
|
||||
|
||||
// View resize manager
|
||||
this.viewResizer = new ResizeObserver(()=>this.onResize());
|
||||
|
||||
// Vertical scroll bar
|
||||
this.vscroll = new Toolkit.ScrollBar(app, {
|
||||
orientation: "vertical",
|
||||
visibility : true
|
||||
});
|
||||
this.vscroll.controls = this.viewport;
|
||||
this.vscroll.addEventListener("scroll",
|
||||
e=>this.onOuterScroll(e, true));
|
||||
this.add(this.vscroll);
|
||||
|
||||
// Horizontal scroll bar
|
||||
this.hscroll = new Toolkit.ScrollBar(app, {
|
||||
orientation: "horizontal",
|
||||
visibility : true
|
||||
});
|
||||
this.hscroll.controls = this.viewport;
|
||||
this.hscroll.addEventListener("scroll",
|
||||
e=>this.onOuterScroll(e, false));
|
||||
this.add(this.hscroll);
|
||||
|
||||
// Corner mask (for when both scroll bars are visible)
|
||||
this.corner = new Toolkit.Component(app, { class: "corner" });
|
||||
this.add(this.corner);
|
||||
|
||||
// Configure view
|
||||
this.view = options.view || null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
|
||||
// Processing by key
|
||||
switch(e.key) {
|
||||
case "ArrowDown":
|
||||
this.viewport.element.scrollTop +=
|
||||
this.vscroll.unitIncrement;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
this.viewport.element.scrollLeft -=
|
||||
this.hscroll.unitIncrement;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
this.viewport.element.scrollLeft +=
|
||||
this.hscroll.unitIncrement;
|
||||
break;
|
||||
case "ArrowUp":
|
||||
this.viewport.element.scrollTop -=
|
||||
this.vscroll.unitIncrement;
|
||||
break;
|
||||
case "PageDown":
|
||||
this.viewport.element.scrollTop +=
|
||||
this.vscroll.blockIncrement;
|
||||
break;
|
||||
case "PageUp":
|
||||
this.viewport.element.scrollTop -=
|
||||
this.vscroll.blockIncrement;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Component resized
|
||||
onResize() {
|
||||
|
||||
// Error checking
|
||||
if (!this.viewport)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let viewport = this.viewport.element.getBoundingClientRect();
|
||||
let scrollHeight =
|
||||
this.hscroll.element.getBoundingClientRect().height;
|
||||
let scrollWidth =
|
||||
this.vscroll.element.getBoundingClientRect().width;
|
||||
let viewHeight = this.viewHeight;
|
||||
let viewWidth = this.viewWidth;
|
||||
let fullHeight =
|
||||
viewport.height + (this.hscroll.visible ? scrollHeight : 0);
|
||||
let fullWidth =
|
||||
viewport.width + (this.vscroll.visible ? scrollWidth : 0);
|
||||
|
||||
// Configure scroll bars
|
||||
this.hscroll.max = viewWidth;
|
||||
this.hscroll.blockIncrement = viewport.width;
|
||||
this.hscroll.value = this.viewport.element.scrollLeft;
|
||||
this.vscroll.max = viewHeight;
|
||||
this.vscroll.blockIncrement = viewport.height;
|
||||
this.vscroll.value = this.viewport.element.scrollTop;
|
||||
|
||||
// Determine whether the vertical scroll bar is visible
|
||||
let vert = false;
|
||||
if (
|
||||
this.overflowY == "scroll" ||
|
||||
this.overflowY == "auto" &&
|
||||
viewHeight > fullHeight &&
|
||||
scrollWidth <= viewWidth
|
||||
) {
|
||||
fullWidth -= scrollWidth;
|
||||
vert = true;
|
||||
}
|
||||
|
||||
// Determine whether the horizontal scroll bar is visible
|
||||
let horz = false;
|
||||
if (
|
||||
this.overflowX == "scroll" ||
|
||||
this.overflowX == "auto" &&
|
||||
viewWidth > fullWidth &&
|
||||
scrollHeight <= viewHeight
|
||||
) {
|
||||
fullHeight -= scrollHeight;
|
||||
horz = true;
|
||||
}
|
||||
|
||||
// The horizontal scroll bar necessitates the vertical scroll bar
|
||||
vert = vert ||
|
||||
this.overflowY == "auto" &&
|
||||
viewHeight > fullHeight &&
|
||||
scrollWidth < viewWidth
|
||||
;
|
||||
|
||||
// Configure scroll bar visibility
|
||||
this.setScrollBars(horz, vert);
|
||||
}
|
||||
|
||||
// View scrolled
|
||||
onInnerScroll() {
|
||||
this.hscroll.value = this.viewport.element.scrollLeft;
|
||||
this.vscroll.value = this.viewport.element.scrollTop;
|
||||
}
|
||||
|
||||
// Scroll bar scrolled
|
||||
onOuterScroll(e, vertical) {
|
||||
this.viewport.element[vertical?"scrollTop":"scrollLeft"] = e.scroll;
|
||||
}
|
||||
|
||||
// Mouse wheel scrolled
|
||||
onWheel(e) {
|
||||
let delta = e.deltaY;
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || delta == 0)
|
||||
return;
|
||||
|
||||
// Scrolling by pixel
|
||||
if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
|
||||
let max = this.vscroll.unitIncrement * 3;
|
||||
delta = Math.min(max, Math.max(-max, delta));
|
||||
}
|
||||
|
||||
// Scrolling by line
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_LINE) {
|
||||
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
|
||||
this.vscroll.unitIncrement;
|
||||
}
|
||||
|
||||
// Scrolling by page
|
||||
else if (e.deltaMode == WheelEvent.DOM_DELTA_PAGE) {
|
||||
delta = Math[delta < 0 ? "floor" : "ceil"](delta) *
|
||||
this.vscroll.blockIncrement;
|
||||
}
|
||||
|
||||
this.viewport.element.scrollTop += delta;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Horizontal scroll bar policy
|
||||
get overflowX() { return this._overflowX; }
|
||||
set overflowX(policy) {
|
||||
switch (policy) {
|
||||
case "auto":
|
||||
case "hidden":
|
||||
case "scroll":
|
||||
this._overflowX = policy;
|
||||
this.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
// Vertical scroll bar policy
|
||||
get overflowY() { return this._overflowY; }
|
||||
set overflowY(policy) {
|
||||
switch (policy) {
|
||||
case "auto":
|
||||
case "hidden":
|
||||
case "scroll":
|
||||
this._overflowY = policy;
|
||||
this.onResize();
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal scrolling position
|
||||
get scrollLeft() { return this.viewport.element.scrollLeft; }
|
||||
set scrollLeft(left) { this.viewport.element.scrollLeft = left; }
|
||||
|
||||
// Vertical scrolling position
|
||||
get scrollTop() { return this.viewport.element.scrollTop; }
|
||||
set scrollTop(top) { this.viewport.element.scrollTop = top; }
|
||||
|
||||
// Represented scrollable region
|
||||
get view() {
|
||||
let ret = this.viewport.element.querySelector("*");
|
||||
return ret && ret.component || ret || null;
|
||||
}
|
||||
set view(view) {
|
||||
view = view instanceof Toolkit.Component ? view.element : view;
|
||||
if (view == null) {
|
||||
view = this.viewport.element.querySelector("*");
|
||||
if (view)
|
||||
this.viewResizer.unobserve(view);
|
||||
this.viewport.element.replaceChildren();
|
||||
} else {
|
||||
this.viewport.element.replaceChildren(view);
|
||||
this.viewResizer.observe(view);
|
||||
}
|
||||
}
|
||||
|
||||
// Height of the view element
|
||||
set viewHeight(x) { }
|
||||
get viewHeight() {
|
||||
let view = this.view && this.view.element || this.view;
|
||||
return Math.min(
|
||||
this.viewport.element.scrollHeight,
|
||||
view ? view.clientHeight : 0
|
||||
);
|
||||
}
|
||||
|
||||
// width of the view element
|
||||
set viewWidth(x) { }
|
||||
get viewWidth() {
|
||||
let view = this.view && this.view.element || this.view;
|
||||
return Math.min(
|
||||
this.viewport.element.scrollWidth,
|
||||
view ? view.clientWidth : 0
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Configure scroll bar visibility
|
||||
setScrollBars(horz, vert) {
|
||||
this.element.style.gridTemplateColumns =
|
||||
"auto" + (vert ? " max-content" : "");
|
||||
this.hscroll.visible = horz;
|
||||
this.hscroll.element.style[horz ? "removeProperty" : "setProperty"]
|
||||
("position", "absolute");
|
||||
this.vscroll.visible = vert;
|
||||
this.vscroll.element.style[vert ? "removeProperty" : "setProperty"]
|
||||
("position", "absolute");
|
||||
this.corner.visible = vert && horz;
|
||||
this.element.classList[horz ? "add" : "remove"]("horizontal");
|
||||
this.element.classList[vert ? "add" : "remove"]("vertical");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,363 @@
|
|||
let register = Toolkit => Toolkit.SplitPane =
|
||||
|
||||
// Presentational text
|
||||
class SplitPane extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class: "tk split-pane"
|
||||
}, options, { style: Object.assign({
|
||||
display : "grid",
|
||||
gridTemplateColumns: "max-content max-content auto",
|
||||
overflow : "hidden"
|
||||
}, options.style || {}) }));
|
||||
|
||||
// Configure instance fields
|
||||
this.drag = null;
|
||||
this._orientation = "left";
|
||||
this._primary = null;
|
||||
this.resizer = new ResizeObserver(()=>this.priResize());
|
||||
this.restore = null;
|
||||
this._secondary = null;
|
||||
|
||||
// Primary placeholder
|
||||
this.noPrimary = document.createElement("div");
|
||||
this.noPrimary.id = Toolkit.id();
|
||||
|
||||
// Separator widget
|
||||
this.splitter = document.createElement("div");
|
||||
this.splitter.setAttribute("aria-controls", this.noPrimary.id);
|
||||
this.splitter.setAttribute("aria-valuemin", "0");
|
||||
this.splitter.setAttribute("role", "separator");
|
||||
this.splitter.setAttribute("tabindex", "0");
|
||||
this.splitter.addEventListener("keydown",
|
||||
e=>this.splitKeyDown (e));
|
||||
this.splitter.addEventListener("pointerdown",
|
||||
e=>this.splitPointerDown(e));
|
||||
this.splitter.addEventListener("pointermove",
|
||||
e=>this.splitPointerMove(e));
|
||||
this.splitter.addEventListener("pointerup" ,
|
||||
e=>this.splitPointerUp (e));
|
||||
|
||||
// Secondary placeholder
|
||||
this.noSecondary = document.createElement("div");
|
||||
|
||||
// Configure options
|
||||
if ("orientation" in options)
|
||||
this.orientation = options.orientation;
|
||||
if ("primary" in options)
|
||||
this.primary = options.primary;
|
||||
if ("secondary" in options)
|
||||
this.secondary = options.secondary;
|
||||
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Primary pane resized
|
||||
priResize() {
|
||||
let metrics = this.measure();
|
||||
this.splitter.setAttribute("aria-valuemax", metrics.max);
|
||||
this.splitter.setAttribute("aria-valuenow", metrics.value);
|
||||
}
|
||||
|
||||
// Splitter key press
|
||||
splitKeyDown(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
|
||||
// Drag is in progress
|
||||
if (this.drag) {
|
||||
if (e.key != "Escape")
|
||||
return;
|
||||
this.splitter.releasePointerCapture(this.drag.pointerId);
|
||||
this.value = this.drag.size;
|
||||
this.drag = null;
|
||||
Toolkit.handle(e);
|
||||
return;
|
||||
}
|
||||
|
||||
// Processing by key
|
||||
let edge = this.orientation;
|
||||
let horz = edge == "left" || edge == "right";
|
||||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
if (horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "top" ? +10 : -10;
|
||||
break;
|
||||
case "ArrowLeft":
|
||||
if (!horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "left" ? -10 : +10;
|
||||
break;
|
||||
case "ArrowRight":
|
||||
if (!horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "left" ? +10 : -10;
|
||||
break;
|
||||
case "ArrowUp":
|
||||
if (horz)
|
||||
return;
|
||||
this.restore = null;
|
||||
this.value += edge == "top" ? -10 : +10;
|
||||
break;
|
||||
case "End":
|
||||
this.restore = null;
|
||||
this.value = this.element.getBoundingClientRect()
|
||||
[horz ? "width" : "height"];
|
||||
break;
|
||||
case "Enter":
|
||||
if (this.restore !== null) {
|
||||
this.value = this.restore;
|
||||
this.restore = null;
|
||||
} else {
|
||||
this.restore = this.value;
|
||||
this.value = 0;
|
||||
}
|
||||
break;
|
||||
case "Home":
|
||||
this.restore = null;
|
||||
this.value = 0;
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Splitter pointer down
|
||||
splitPointerDown(e) {
|
||||
|
||||
// Do not obtain focus automatically
|
||||
e.preventDefault();
|
||||
|
||||
// Error checking
|
||||
if (
|
||||
e.altKey || e.ctrlKey || e.shiftKey || e.button != 0 ||
|
||||
this.disabled || this.splitter.hasPointerCapture(e.pointerId)
|
||||
) return;
|
||||
|
||||
// Begin dragging
|
||||
this.splitter.setPointerCapture(e.poinerId);
|
||||
let horz = this.orientation == "left" || this.orientation == "right";
|
||||
let prop = horz ? "width" : "height";
|
||||
this.drag = {
|
||||
pointerId: e.pointerId,
|
||||
size : (this._primary || this.noPrimary)
|
||||
.getBoundingClientRect()[prop],
|
||||
start : e[horz ? "clientX" : "clientY" ]
|
||||
};
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Splitter pointer move
|
||||
splitPointerMove(e) {
|
||||
|
||||
// Error checking
|
||||
if (!this.splitter.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let horz = this.orientation == "left" || this.orientation == "right";
|
||||
let delta = e[horz ? "clientX" : "clientY"] - this.drag.start;
|
||||
let scale = this.orientation == "bottom" ||
|
||||
this.orientation == "right" ? -1 : 1;
|
||||
|
||||
// Resize the primary component
|
||||
this.restore = null;
|
||||
this.value = Math.round(this.drag.size + scale * delta);
|
||||
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Splitter pointer up
|
||||
splitPointerUp(e) {
|
||||
|
||||
// Error checking
|
||||
if (e.button != 0 || !this.splitter.hasPointerCapture(e.pointerId))
|
||||
return;
|
||||
|
||||
// End dragging
|
||||
this.splitter.releasePointerCapture(e.pointerId);
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Edge containing the primary pane
|
||||
get orientation() { return this._orientation; }
|
||||
set orientation(orientation) {
|
||||
switch (orientation) {
|
||||
case "bottom":
|
||||
case "left":
|
||||
case "right":
|
||||
case "top":
|
||||
break;
|
||||
default: return;
|
||||
}
|
||||
if (orientation == this.orientation)
|
||||
return;
|
||||
this._orientation = orientation;
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
// Primary content pane
|
||||
get primary() {
|
||||
return !this._primary ? null :
|
||||
this._primary.component || this._primary;
|
||||
}
|
||||
set primary(element) {
|
||||
if (element instanceof Toolkit.Component)
|
||||
element = element.element;
|
||||
if (!(element instanceof HTMLElement) || element == this.element)
|
||||
return;
|
||||
this.resizer.unobserve(this._primary || this.noPrimary);
|
||||
this._primary = element || null;
|
||||
this.resizer.observe(element || this.noPrimary);
|
||||
this.splitter.setAttribute("aria-controls",
|
||||
(element || this.noPrimary).id);
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
// Secondary content pane
|
||||
get secondary() {
|
||||
return !this._secondary ? null :
|
||||
this._secondary.component || this._secondary;
|
||||
}
|
||||
set secondary(element) {
|
||||
if (element instanceof Toolkit.Component)
|
||||
element = element.element;
|
||||
if (!(element instanceof HTMLElement) || element == this.element)
|
||||
return;
|
||||
this._secondary = element || null;
|
||||
this.revalidate();
|
||||
}
|
||||
|
||||
// Current splitter position
|
||||
get value() {
|
||||
return Math.ceil(
|
||||
(this._primary || this.primary).getBoundingClientRect()
|
||||
[this.orientation == "left" || this.orientation == "right" ?
|
||||
"width" : "height"]
|
||||
);
|
||||
}
|
||||
set value(value) {
|
||||
value = Math.round(value);
|
||||
|
||||
// Error checking
|
||||
if (value == this.value)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let pri = this._primary || this.noPrimary;
|
||||
let sec = this._secondary || this.noSecondary;
|
||||
let prop = this.orientation == "left" || this.orientation == "right" ?
|
||||
"width" : "height";
|
||||
|
||||
// Resize the primary component
|
||||
pri.style[prop] = Math.max(0, value) + "px";
|
||||
|
||||
// Ensure the pane didn't become too large due to margin styles
|
||||
let propPri = pri .getBoundingClientRect()[prop];
|
||||
let propSec = sec .getBoundingClientRect()[prop];
|
||||
let propSplit = this.splitter.getBoundingClientRect()[prop];
|
||||
let propThis = this.element .getBoundingClientRect()[prop];
|
||||
if (propPri + propSec + propSplit > propThis) {
|
||||
pri.style[prop] = Math.max(0, Math.floor(
|
||||
propThis - propSec - propSplit)) + "px";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Measure the current bounds of the child elements
|
||||
measure() {
|
||||
let prop = this.orientation == "top" || this.orientation == "bottom" ?
|
||||
"height" : "width";
|
||||
let bndThis = this.element .getBoundingClientRect();
|
||||
let bndSplit = this.splitter.getBoundingClientRect();
|
||||
let bndPri = (this._primary || this.noPrimary)
|
||||
.getBoundingClientRect();
|
||||
return {
|
||||
max : bndThis[prop],
|
||||
property: prop,
|
||||
value : bndPri[prop]
|
||||
}
|
||||
}
|
||||
|
||||
// Arrange child components
|
||||
revalidate() {
|
||||
let horz = true;
|
||||
let children = [
|
||||
this._primary || this.noPrimary,
|
||||
this.splitter,
|
||||
this._secondary || this.noSecondary
|
||||
];
|
||||
|
||||
// Select styles by orientation
|
||||
switch (this.orientation) {
|
||||
case "bottom":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoColumns : "100%",
|
||||
gridTemplateRows: "auto max-content max-content"
|
||||
});
|
||||
horz = false;
|
||||
children.reverse();
|
||||
break;
|
||||
case "left":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoRows : "100%",
|
||||
gridTemplateColumns: "max-content max-content auto"
|
||||
});
|
||||
break;
|
||||
case "right":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoRows : "100%",
|
||||
gridTemplateColumns: "auto max-content max-content"
|
||||
});
|
||||
children.reverse();
|
||||
break;
|
||||
case "top":
|
||||
Object.assign(this.element.style, {
|
||||
gridAutoColumns : "100%",
|
||||
gridTemplateRows: "max-content max-content auto"
|
||||
});
|
||||
horz = false;
|
||||
break;
|
||||
}
|
||||
|
||||
// Update element
|
||||
if (horz) {
|
||||
this.element.style.removeProperty("grid-auto-columns");
|
||||
this.element.style.removeProperty("grid-template-rows");
|
||||
this.splitter.className = "tk horizontal";
|
||||
this.splitter.style.cursor = "ew-resize";
|
||||
} else {
|
||||
this.element.style.removeProperty("grid-auto-rows");
|
||||
this.element.style.removeProperty("grid-template-columns");
|
||||
this.splitter.className = "tk vertical";
|
||||
this.splitter.style.cursor = "ns-resize";
|
||||
}
|
||||
this.element.replaceChildren(... children);
|
||||
this.priResize();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,67 @@
|
|||
let register = Toolkit => Toolkit.TextBox =
|
||||
|
||||
// Check box
|
||||
class TextBox extends Toolkit.Component {
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, options = Object.assign({
|
||||
class : "tk text-box",
|
||||
tag : "input",
|
||||
type : "text"
|
||||
}, options));
|
||||
this.element.addEventListener("focusout", e=>this.commit ( ));
|
||||
this.element.addEventListener("keydown" , e=>this.onKeyDown(e));
|
||||
this.value = options.value || null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Key press
|
||||
onKeyDown(e) {
|
||||
if (e.altKey || e.ctrlKey || e.shiftKey || this.disabled)
|
||||
return;
|
||||
if (e.key != "Tab")
|
||||
e.stopPropagation();
|
||||
if (e.key == "Enter")
|
||||
this.commit();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Contained text
|
||||
get value() { return this.element.value; }
|
||||
set value(value) {
|
||||
value = value === undefined || value === null ? "" : value.toString();
|
||||
if (value == this.value)
|
||||
return;
|
||||
this.element.value = value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeLabel();
|
||||
this.localizeTitle();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Complete editing
|
||||
commit() {
|
||||
this.element.dispatchEvent(new Event("action"));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|
|
@ -0,0 +1,51 @@
|
|||
// Namespace container for toolkit classes
|
||||
class Toolkit {
|
||||
|
||||
// Next numeric ID
|
||||
static nextId = 0;
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Static Methods //////////////////////////////
|
||||
|
||||
// Produce a synthetic Event object
|
||||
static event(type, properties, bubbles = true) {
|
||||
return Object.assign(
|
||||
new Event(type, { bubbles: bubbles }),
|
||||
properties
|
||||
);
|
||||
}
|
||||
|
||||
// Finalize an event
|
||||
static handle(event) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
// Generate a unique DOM element ID
|
||||
static id() {
|
||||
return "tk" + this.nextId++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Register component classes
|
||||
(await import(/**/"./Component.js" )).register(Toolkit);
|
||||
(await import(/**/"./App.js" )).register(Toolkit);
|
||||
(await import(/**/"./Button.js" )).register(Toolkit);
|
||||
(await import(/**/"./Checkbox.js" )).register(Toolkit);
|
||||
(await import(/**/"./Desktop.js" )).register(Toolkit);
|
||||
(await import(/**/"./DropDown.js" )).register(Toolkit);
|
||||
(await import(/**/"./Label.js" )).register(Toolkit);
|
||||
(await import(/**/"./Menu.js" )).register(Toolkit);
|
||||
(await import(/**/"./MenuBar.js" )).register(Toolkit);
|
||||
(await import(/**/"./MenuItem.js" )).register(Toolkit);
|
||||
(await import(/**/"./ScrollBar.js" )).register(Toolkit);
|
||||
(await import(/**/"./ScrollPane.js")).register(Toolkit);
|
||||
(await import(/**/"./Radio.js" )).register(Toolkit);
|
||||
(await import(/**/"./RadioGroup.js")).register(Toolkit);
|
||||
(await import(/**/"./SplitPane.js" )).register(Toolkit);
|
||||
(await import(/**/"./TextBox.js" )).register(Toolkit);
|
||||
(await import(/**/"./Window.js" )).register(Toolkit);
|
||||
|
||||
export { Toolkit };
|
|
@ -0,0 +1,479 @@
|
|||
let register = Toolkit => Toolkit.Window =
|
||||
|
||||
class Window extends Toolkit.Component {
|
||||
|
||||
//////////////////////////////// Constants ////////////////////////////////
|
||||
|
||||
// Resize directions by dragging edge
|
||||
static RESIZES = {
|
||||
"nw": { left : true, top : true },
|
||||
"n" : { top : true },
|
||||
"ne": { right: true, top : true },
|
||||
"w" : { left : true },
|
||||
"e" : { right: true },
|
||||
"sw": { left : true, bottom: true },
|
||||
"s" : { bottom: true },
|
||||
"se": { right: true, bottom: true }
|
||||
};
|
||||
|
||||
|
||||
|
||||
///////////////////////// Initialization Methods //////////////////////////
|
||||
|
||||
constructor(app, options = {}) {
|
||||
super(app, Object.assign({
|
||||
"aria-modal": "false",
|
||||
class : "tk window",
|
||||
id : Toolkit.id(),
|
||||
role : "dialog",
|
||||
tabIndex : -1,
|
||||
visibility : true,
|
||||
visible : false
|
||||
}, options, { style: Object.assign({
|
||||
boxSizing : "border-box",
|
||||
display : "grid",
|
||||
gridTemplateRows: "max-content auto",
|
||||
position : "absolute"
|
||||
}, options.style || {})} ));
|
||||
|
||||
// Configure instance fields
|
||||
this.lastFocus = null;
|
||||
this.style = getComputedStyle(this.element);
|
||||
this.text = null;
|
||||
|
||||
// Configure event listeners
|
||||
this.addEventListener("focusin", e=>this.onFocus (e));
|
||||
this.addEventListener("keydown", e=>this.onKeyDown(e));
|
||||
|
||||
// Working variables
|
||||
let onBorderDown = e=>this.onBorderDown(e);
|
||||
let onBorderMove = e=>this.onBorderMove(e);
|
||||
let onBorderUp = e=>this.onBorderUp (e);
|
||||
let onDragKey = e=>this.onDragKey (e);
|
||||
|
||||
// Resizing borders
|
||||
for (let edge of [ "nw1", "nw2", "n", "ne1", "ne2",
|
||||
"w", "e", "sw1", "sw2", "s", "se1", "se2"]) {
|
||||
let border = document.createElement("div");
|
||||
border.className = edge;
|
||||
border.edge = edge.replace(/[12]/g, "");
|
||||
Object.assign(border.style, {
|
||||
cursor : border.edge + "-resize",
|
||||
position: "absolute"
|
||||
});
|
||||
border.addEventListener("keydown" , onDragKey );
|
||||
border.addEventListener("pointerdown", onBorderDown);
|
||||
border.addEventListener("pointermove", onBorderMove);
|
||||
border.addEventListener("pointerup" , onBorderUp );
|
||||
this.element.append(border);
|
||||
}
|
||||
|
||||
// Title bar
|
||||
this.titleBar = document.createElement("div");
|
||||
this.titleBar.className = "title";
|
||||
this.titleBar.id = Toolkit.id();
|
||||
Object.assign(this.titleBar.style, {
|
||||
display : "grid",
|
||||
gridTemplateColumns: "auto max-content",
|
||||
minWidth : "0",
|
||||
overflow : "hidden"
|
||||
});
|
||||
this.element.append(this.titleBar);
|
||||
this.titleBar.addEventListener("keydown" , e=>this.onDragKey (e));
|
||||
this.titleBar.addEventListener("pointerdown", e=>this.onTitleDown(e));
|
||||
this.titleBar.addEventListener("pointermove", e=>this.onTitleMove(e));
|
||||
this.titleBar.addEventListener("pointerup" , e=>this.onTitleUp (e));
|
||||
|
||||
// Title text
|
||||
this.title = document.createElement("div");
|
||||
this.title.className = "text";
|
||||
this.title.id = Toolkit.id();
|
||||
this.title.innerText = "\u00a0";
|
||||
this.titleBar.append(this.title);
|
||||
this.element.setAttribute("aria-labelledby", this.title.id);
|
||||
|
||||
// Close button
|
||||
this.close = new Toolkit.Button(app, {
|
||||
class : "close-button",
|
||||
doNotFocus: true
|
||||
});
|
||||
this.close.addEventListener("action",
|
||||
e=>this.element.dispatchEvent(new Event("close")));
|
||||
this.close.setLabel("{window.close}", true);
|
||||
this.close.setTitle("{window.close}", true);
|
||||
this.titleBar.append(this.close.element);
|
||||
|
||||
// Client area
|
||||
this.client = document.createElement("div");
|
||||
this.client.className = "client";
|
||||
Object.assign(this.client.style, {
|
||||
minHeight: "0",
|
||||
minWidth : "0",
|
||||
overflow : "hidden"
|
||||
});
|
||||
this.element.append(this.client);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Event Handlers //////////////////////////////
|
||||
|
||||
// Border pointer down
|
||||
onBorderDown(e) {
|
||||
|
||||
// Do not drag
|
||||
if (e.button != 0 || this.app.drag != null)
|
||||
return;
|
||||
|
||||
// Initiate dragging
|
||||
this.drag = {
|
||||
height: this.outerHeight,
|
||||
left : this.left,
|
||||
top : this.top,
|
||||
width : this.outerWidth,
|
||||
x : e.clientX,
|
||||
y : e.clientY
|
||||
};
|
||||
this.drag.bottom = this.drag.top + this.drag.height;
|
||||
this.drag.right = this.drag.left + this.drag.width;
|
||||
|
||||
// Configure event
|
||||
this.focus();
|
||||
this.app.drag = e;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Border pointer move
|
||||
onBorderMove(e) {
|
||||
|
||||
// Not dragging
|
||||
if (this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let resize = Toolkit.Window.RESIZES[e.target.edge];
|
||||
let dx = e.clientX - this.drag.x;
|
||||
let dy = e.clientY - this.drag.y;
|
||||
let style = getComputedStyle(this.element);
|
||||
let minHeight =
|
||||
this.client .getBoundingClientRect().top -
|
||||
this.titleBar.getBoundingClientRect().top +
|
||||
parseFloat(style.borderTop ) +
|
||||
parseFloat(style.borderBottom)
|
||||
;
|
||||
|
||||
// Output bounds
|
||||
let height = this.drag.height;
|
||||
let left = this.drag.left;
|
||||
let top = this.drag.top;
|
||||
let width = this.drag.width;
|
||||
|
||||
// Dragging left
|
||||
if (resize.left) {
|
||||
let bParent = this.parent.element.getBoundingClientRect();
|
||||
left += dx;
|
||||
left = Math.min(left, this.drag.right - 32);
|
||||
left = Math.min(left, bParent.width - 16);
|
||||
width = this.drag.width + this.drag.left - left;
|
||||
}
|
||||
|
||||
// Dragging top
|
||||
if (resize.top) {
|
||||
let bParent = this.parent.element.getBoundingClientRect();
|
||||
top += dy;
|
||||
top = Math.max(top, 0);
|
||||
top = Math.min(top, this.drag.bottom - minHeight);
|
||||
top = Math.min(top, bParent.height - minHeight);
|
||||
height = this.drag.height + this.drag.top - top;
|
||||
}
|
||||
|
||||
// Dragging right
|
||||
if (resize.right) {
|
||||
width += dx;
|
||||
width = Math.max(width, 32);
|
||||
width = Math.max(width, 16 - this.drag.left);
|
||||
}
|
||||
|
||||
// Dragging bottom
|
||||
if (resize.bottom) {
|
||||
height += dy;
|
||||
height = Math.max(height, minHeight);
|
||||
}
|
||||
|
||||
// Apply bounds
|
||||
this.element.style.height = height + "px";
|
||||
this.element.style.left = left + "px";
|
||||
this.element.style.top = top + "px";
|
||||
this.element.style.width = width + "px";
|
||||
|
||||
// Configure event
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Border pointer up
|
||||
onBorderUp(e, id) {
|
||||
|
||||
// Not dragging
|
||||
if (e.button != 0 || this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.drag = null;
|
||||
|
||||
// Configure event
|
||||
this.app.drag = null;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Key down while dragging
|
||||
onDragKey(e) {
|
||||
if (
|
||||
this.drag != null && e.key == "Escape" &&
|
||||
!e.ctrlKey && !e.altKey && !e.shiftKey
|
||||
) this.cancelDrag();
|
||||
}
|
||||
|
||||
// Focus gained
|
||||
onFocus(e) {
|
||||
|
||||
// The element receiving focus is self, or Close button from external
|
||||
if (
|
||||
e.target == this.element ||
|
||||
e.target == this.close.element && !this.contains(e.relatedTarget)
|
||||
) {
|
||||
let elm = this.lastFocus;
|
||||
if (!elm) {
|
||||
elm = this.getFocusable();
|
||||
elm = elm[Math.min(1, elm.length - 1)];
|
||||
}
|
||||
elm.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
// The element receiving focus is not self
|
||||
else if (e.target != this.close.element)
|
||||
this.lastFocus = e.target;
|
||||
|
||||
// Bring the window to the front among its siblings
|
||||
if (this.parent instanceof Toolkit.Desktop)
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
// Window key press
|
||||
onKeyDown(e) {
|
||||
|
||||
// Take no action
|
||||
if (e.altKey || e.ctrlKey || e.key != "Tab")
|
||||
return;
|
||||
|
||||
// Move focus to the next element in the sequence
|
||||
let focuses = this.getFocusable();
|
||||
let nowIndex = focuses.indexOf(document.activeElement) || 0;
|
||||
let nextIndex = nowIndex + focuses.length + (e.shiftKey ? -1 : 1);
|
||||
let target = focuses[nextIndex % focuses.length];
|
||||
Toolkit.handle(e);
|
||||
target.focus();
|
||||
}
|
||||
|
||||
// Title bar pointer down
|
||||
onTitleDown(e) {
|
||||
|
||||
// Do not drag
|
||||
if (e.button != 0 || this.app.drag != null)
|
||||
return;
|
||||
|
||||
// Initiate dragging
|
||||
this.drag = {
|
||||
height: this.outerHeight,
|
||||
left : this.left,
|
||||
top : this.top,
|
||||
width : this.outerWidth,
|
||||
x : e.clientX,
|
||||
y : e.clientY
|
||||
};
|
||||
|
||||
// Configure event
|
||||
this.focus();
|
||||
this.app.drag = e;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Title bar pointer move
|
||||
onTitleMove(e) {
|
||||
|
||||
// Not dragging
|
||||
if (this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Working variables
|
||||
let bParent = this.parent.element.getBoundingClientRect();
|
||||
|
||||
// Move horizontally
|
||||
let left = this.drag.left + e.clientX - this.drag.x;
|
||||
left = Math.min(left, bParent.width - 16);
|
||||
left = Math.max(left, 16 - this.drag.width);
|
||||
this.element.style.left = left + "px";
|
||||
|
||||
// Move vertically
|
||||
let top = this.drag.top + e.clientY - this.drag.y;
|
||||
top = Math.min(top, bParent.height - this.minHeight);
|
||||
top = Math.max(top, 0);
|
||||
this.element.style.top = top + "px";
|
||||
|
||||
// Configure event
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
// Title bar pointer up
|
||||
onTitleUp(e) {
|
||||
|
||||
// Not dragging
|
||||
if (e.button != 0 || this.app.drag != e.target)
|
||||
return;
|
||||
|
||||
// Configure instance fields
|
||||
this.drag = null;
|
||||
|
||||
// Configure event
|
||||
this.app.drag = null;
|
||||
Toolkit.handle(e);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Public Methods //////////////////////////////
|
||||
|
||||
// Bring the window to the front among its siblings
|
||||
bringToFront() {
|
||||
if (this.parent != null)
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
// Set focus on the component
|
||||
focus() {
|
||||
if (!this.contains(document.activeElement))
|
||||
(this.lastFocus || this.element).focus({ preventScroll: true });
|
||||
if (this.parent instanceof Toolkit.Desktop)
|
||||
this.parent.bringToFront(this);
|
||||
}
|
||||
|
||||
// Height of client
|
||||
get height() { return this.client.getBoundingClientRect().height; }
|
||||
set height(height) {
|
||||
this.element.style.height =
|
||||
this.outerHeight - this.height + Math.max(height, 0) + "px";
|
||||
}
|
||||
|
||||
// Position of window left edge
|
||||
get left() {
|
||||
return this.element.getBoundingClientRect().left - (
|
||||
this.parent == null ? 0 :
|
||||
this.parent.element.getBoundingClientRect().left
|
||||
);
|
||||
}
|
||||
set left(left) {
|
||||
if (this.parent != null) {
|
||||
left = Math.min(left,
|
||||
this.parent.element.getBoundingClientRect().width - 16);
|
||||
}
|
||||
left = Math.max(left, 16 - this.outerWidth);
|
||||
this.element.style.left = left + "px";
|
||||
}
|
||||
|
||||
// Height of entire window
|
||||
get outerHeight() { return this.element.getBoundingClientRect().height; }
|
||||
set outerHeight(height) {
|
||||
height = Math.max(height, this.minHeight);
|
||||
this.element.style.height = height + "px";
|
||||
}
|
||||
|
||||
// Width of entire window
|
||||
get outerWidth() { return this.element.getBoundingClientRect().width; }
|
||||
set outerWidth(width) {
|
||||
width = Math.max(width, 32);
|
||||
this.element.style.width = width + "px";
|
||||
let left = this.left;
|
||||
if (left + width < 16)
|
||||
this.element.style.left = 16 - width + "px";
|
||||
}
|
||||
|
||||
// Specify the window title
|
||||
setTitle(title, localize) {
|
||||
this.setString("text", title, localize);
|
||||
}
|
||||
|
||||
// Position of window top edge
|
||||
get top() {
|
||||
return this.element.getBoundingClientRect().top - (
|
||||
this.parent == null ? 0 :
|
||||
this.parent.element.getBoundingClientRect().top
|
||||
);
|
||||
}
|
||||
set top(top) {
|
||||
if (this.parent != null) {
|
||||
top = Math.min(top, -this.minHeight +
|
||||
this.parent.element.getBoundingClientRect().height);
|
||||
}
|
||||
top = Math.max(top, 0);
|
||||
this.element.style.top = top + "px";
|
||||
}
|
||||
|
||||
// Specify whether the element is visible
|
||||
get visible() { return super.visible; }
|
||||
set visible(visible) {
|
||||
let prevSetting = super.visible;
|
||||
let prevActual = this.isVisible();
|
||||
super.visible = visible;
|
||||
let nowActual = this.isVisible();
|
||||
if (!nowActual && this.contains(document.activeElement))
|
||||
document.activeElement.blur();
|
||||
}
|
||||
|
||||
// Width of client
|
||||
get width() { return this.client.getBoundingClientRect().width; }
|
||||
set width(width) {
|
||||
this.outerWidth = this.outerWidth - this.width + Math.max(width, 32);
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Package Methods /////////////////////////////
|
||||
|
||||
// Add a child component to the primary client region of this component
|
||||
append(element) {
|
||||
this.client.append(element);
|
||||
}
|
||||
|
||||
// Update localization strings
|
||||
localize() {
|
||||
this.localizeText(this.title);
|
||||
if ((this.title.textContent || "") == "")
|
||||
this.title.innerText = "\u00a0"; //
|
||||
this.close.localize();
|
||||
}
|
||||
|
||||
|
||||
|
||||
///////////////////////////// Private Methods /////////////////////////////
|
||||
|
||||
// Cancel a move or resize dragging operaiton
|
||||
cancelDrag() {
|
||||
this.app.drag = null;
|
||||
this.element.style.height = this.drag.height + "px";
|
||||
this.element.style.left = this.drag.left + "px";
|
||||
this.element.style.top = this.drag.top + "px";
|
||||
this.element.style.width = this.drag.width + "px";
|
||||
this.drag = null;
|
||||
}
|
||||
|
||||
// Minimum height of window
|
||||
get minHeight() {
|
||||
return (
|
||||
this.client .getBoundingClientRect().top -
|
||||
this.element.getBoundingClientRect().top +
|
||||
parseFloat(this.style.borderBottomWidth)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export { register };
|