2024 reboot
							
								
								
									
										204
									
								
								core/bus.c
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -3,104 +3,73 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/***************************** Utility Functions *****************************/
 | 
			
		||||
/********************************* 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 ****************************/
 | 
			
		||||
 | 
			
		||||
/* Read a typed value from a buffer in host memory */
 | 
			
		||||
static int32_t busReadBuffer(
 | 
			
		||||
    uint8_t *buffer, uint32_t size, uint32_t offset, int type) {
 | 
			
		||||
static int32_t busReadBuffer(uint8_t *data, int type) {
 | 
			
		||||
 | 
			
		||||
    /* There is no data */
 | 
			
		||||
    if (buffer == NULL)
 | 
			
		||||
        return 0;
 | 
			
		||||
 | 
			
		||||
    /* Mirror the buffer across its address range */
 | 
			
		||||
    offset &= size - 1;
 | 
			
		||||
 | 
			
		||||
    /* Processing by type */
 | 
			
		||||
    /* Processing by data type */
 | 
			
		||||
    switch (type) {
 | 
			
		||||
 | 
			
		||||
        /* Generic implementation */
 | 
			
		||||
        #ifndef VB_LITTLE_ENDIAN
 | 
			
		||||
            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    ]
 | 
			
		||||
                ;
 | 
			
		||||
            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];
 | 
			
		||||
 | 
			
		||||
        /* Little-endian implementation */
 | 
			
		||||
        /* Little-endian host */
 | 
			
		||||
        #else
 | 
			
		||||
            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];
 | 
			
		||||
            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;
 | 
			
		||||
        #endif
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Invalid type */
 | 
			
		||||
    return 0;
 | 
			
		||||
    return 0; /* Unreachable */
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Write a typed value to a buffer in host memory */
 | 
			
		||||
static void busWriteBuffer(
 | 
			
		||||
    uint8_t *buffer, uint32_t size, uint32_t offset, int type, int32_t value) {
 | 
			
		||||
static void busWriteBuffer(uint8_t *data, int type, int32_t value) {
 | 
			
		||||
 | 
			
		||||
    /* There is no data */
 | 
			
		||||
    if (buffer == NULL)
 | 
			
		||||
        return;
 | 
			
		||||
 | 
			
		||||
    /* Mirror the buffer across its address range */
 | 
			
		||||
    offset &= size - 1;
 | 
			
		||||
 | 
			
		||||
    /* Processing by type */
 | 
			
		||||
    /* Processing by data type */
 | 
			
		||||
    switch (type) {
 | 
			
		||||
 | 
			
		||||
        /* Generic implementation */
 | 
			
		||||
        #ifndef VB_LITTLE_ENDIAN
 | 
			
		||||
            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;
 | 
			
		||||
            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;
 | 
			
		||||
 | 
			
		||||
        /* Little-endian implementation */
 | 
			
		||||
        /* Little-endian host */
 | 
			
		||||
        #else
 | 
			
		||||
            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;
 | 
			
		||||
            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;
 | 
			
		||||
        #endif
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -109,54 +78,79 @@ static void busWriteBuffer(
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/***************************** Module Functions ******************************/
 | 
			
		||||
/***************************** Library Functions *****************************/
 | 
			
		||||
 | 
			
		||||
/* Read a typed value from the simulation bus */
 | 
			
		||||
static int32_t busRead(VB *sim, uint32_t address, int type) {
 | 
			
		||||
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;
 | 
			
		||||
 | 
			
		||||
    /* 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;
 | 
			
		||||
 | 
			
		||||
    /* Processing by address region */
 | 
			
		||||
    switch (address >> 24 & 7) {
 | 
			
		||||
    /* Working variables */
 | 
			
		||||
    address &= TYPE_MASKS[type];
 | 
			
		||||
 | 
			
		||||
    /* Process by address range */
 | 
			
		||||
    switch (address >> 24) {
 | 
			
		||||
        case 0: break; /* VIP */
 | 
			
		||||
        case 1: break; /* VSU */
 | 
			
		||||
        case 2: break; /* Misc. hardware */
 | 
			
		||||
        case 2: break; /* Misc. I/O */
 | 
			
		||||
        case 3: break; /* Unmapped */
 | 
			
		||||
        case 4: break; /* Game pak expansion */
 | 
			
		||||
        case 5:        /* WRAM */
 | 
			
		||||
            busWriteBuffer(sim->wram    ,0x10000          ,address,type,value);
 | 
			
		||||
        case 4: break; /* Game Pak expansion */
 | 
			
		||||
 | 
			
		||||
        case 5: /* WRAM */
 | 
			
		||||
            busWriteBuffer(&sim->wram[address & 0x0000FFFF], type, value);
 | 
			
		||||
            break;
 | 
			
		||||
        case 6:        /* Game pak RAM */
 | 
			
		||||
            busWriteBuffer(sim->cart.ram,sim->cart.ramSize,address,type,value);
 | 
			
		||||
 | 
			
		||||
        case 6: /* Game Pak RAM */
 | 
			
		||||
            if (sim->cart.ram != NULL) {
 | 
			
		||||
                busWriteBuffer(
 | 
			
		||||
                    &sim->cart.ram[address & sim->cart.ramMask], type, value);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
        case 7:        /* Game pak ROM */
 | 
			
		||||
            busWriteBuffer(sim->cart.rom,sim->cart.romSize,address,type,value);
 | 
			
		||||
 | 
			
		||||
        case 7: /* Game Pak ROM */
 | 
			
		||||
            if (debug && sim->cart.rom != NULL) {
 | 
			
		||||
                busWriteBuffer(
 | 
			
		||||
                    &sim->cart.rom[address & sim->cart.romMask], type, value);
 | 
			
		||||
            }
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
#endif /* VBAPI */
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2128
									
								
								core/cpu.c
								
								
								
								
							
							
						
						
							
								
								
									
										425
									
								
								core/vb.c
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -5,37 +5,117 @@
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/***************************** Utility Functions *****************************/
 | 
			
		||||
/*********************************** Types ***********************************/
 | 
			
		||||
 | 
			
		||||
/* Select the lesser of two unsigned numbers */
 | 
			
		||||
static uint32_t Min(uint32_t a, uint32_t b) {
 | 
			
		||||
/* 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 address;
 | 
			
		||||
                int32_t  value;
 | 
			
		||||
            } data;
 | 
			
		||||
        } aux;
 | 
			
		||||
 | 
			
		||||
        /* Other state */
 | 
			
		||||
        uint32_t clocks;    /* Master clocks to wait */
 | 
			
		||||
        uint16_t code[2];   /* Instruction code units */
 | 
			
		||||
        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 callbacks */
 | 
			
		||||
    vbOnExecute onExecute; /* CPU instruction execute */
 | 
			
		||||
    vbOnFetch   onFetch;   /* CPU instruction fetch */
 | 
			
		||||
    vbOnRead    onRead;    /* CPU instruction read */
 | 
			
		||||
    vbOnWrite   onWrite;   /* CPU instruction write */
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/***************************** Library Functions *****************************/
 | 
			
		||||
 | 
			
		||||
/* Determine the lesser of two clocks figures */
 | 
			
		||||
static uint32_t MinClocks(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
 | 
			
		||||
/* Sign-extend an integer of variable width */
 | 
			
		||||
static int32_t SignExtend(int32_t value, int32_t bits) {
 | 
			
		||||
    value &= ~((uint32_t) 0xFFFFFFFF << bits);
 | 
			
		||||
    bits   = (int32_t) 1 << (bits - (int32_t) 1);
 | 
			
		||||
    return (value ^ bits) - bits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/**************************** Sub-Module Imports *****************************/
 | 
			
		||||
/******************************** Sub-Modules ********************************/
 | 
			
		||||
 | 
			
		||||
#include "bus.c"
 | 
			
		||||
#include "cpu.c"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/***************************** Module Functions ******************************/
 | 
			
		||||
/***************************** Library Functions *****************************/
 | 
			
		||||
 | 
			
		||||
/* Process a simulation for a given number of clocks */
 | 
			
		||||
static int sysEmulate(VB *sim, uint32_t clocks) {
 | 
			
		||||
| 
						 | 
				
			
			@ -44,7 +124,7 @@ static int sysEmulate(VB *sim, uint32_t clocks) {
 | 
			
		|||
    ;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Determine how many clocks can be simulated without a breakpoint */
 | 
			
		||||
/* Determine how many clocks are guaranteed to process */
 | 
			
		||||
static uint32_t sysUntil(VB *sim, uint32_t clocks) {
 | 
			
		||||
    clocks = cpuUntil(sim, clocks);
 | 
			
		||||
    return clocks;
 | 
			
		||||
| 
						 | 
				
			
			@ -52,127 +132,121 @@ static uint32_t sysUntil(VB *sim, uint32_t clocks) {
 | 
			
		|||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/************************************ API ************************************/
 | 
			
		||||
/******************************* API Commands ********************************/
 | 
			
		||||
 | 
			
		||||
/* 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  );
 | 
			
		||||
/* 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) {
 | 
			
		||||
        until    = sysUntil(sim, *clocks);
 | 
			
		||||
        brk      = sysEmulate(sim, until);
 | 
			
		||||
        *clocks -= until;
 | 
			
		||||
        if (brk)
 | 
			
		||||
            return brk; /* TODO: return 1 */
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return brk;
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Process multiple simulations */
 | 
			
		||||
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 */
 | 
			
		||||
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      x;     /* Iterator */
 | 
			
		||||
 | 
			
		||||
    /* Process all clocks */
 | 
			
		||||
    for (brk = 0; *clocks != 0 && !brk; *clocks -= until) {
 | 
			
		||||
    while (*clocks != 0) {
 | 
			
		||||
        until = *clocks;
 | 
			
		||||
        for (x = 0; x < count; x++)
 | 
			
		||||
            until = sysUntil  (sims[x], until);
 | 
			
		||||
        for (x = 0; x < count; x++)
 | 
			
		||||
            brk  |= sysEmulate(sims[x], until);
 | 
			
		||||
    }
 | 
			
		||||
        for (x = count - 1; x >= 0; x--)
 | 
			
		||||
            until = sysUntil(sims[x], until);
 | 
			
		||||
 | 
			
		||||
    return brk;
 | 
			
		||||
        brk = 0;
 | 
			
		||||
        for (x = count - 1; x >= 0; x--)
 | 
			
		||||
            brk |= sysEmulate(sims[x], until);
 | 
			
		||||
 | 
			
		||||
        *clocks -= until;
 | 
			
		||||
        if (brk)
 | 
			
		||||
            return brk; /* TODO: return 1 */
 | 
			
		||||
    }
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Retrieve a current breakpoint handler */
 | 
			
		||||
void* vbGetCallback(VB *sim, int id) {
 | 
			
		||||
/* Retrieve a callback handler */
 | 
			
		||||
VBAPI 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;
 | 
			
		||||
        /*case VB_EXCEPTION: return *(void **) &sim->onException;*/
 | 
			
		||||
        case VB_EXECUTE  : return *(void **) &sim->onExecute;
 | 
			
		||||
        case VB_FETCH    : return *(void **) &sim->onFetch;
 | 
			
		||||
        /*case VB_FRAME    : return *(void **) &sim->onFrame;*/
 | 
			
		||||
        case VB_READ     : return *(void **) &sim->onRead;
 | 
			
		||||
        case VB_WRITE    : 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) {
 | 
			
		||||
/* Retrieve the game pack RAM buffer */
 | 
			
		||||
VBAPI void* vbGetCartRAM(VB *sim, uint32_t *size) {
 | 
			
		||||
    if (size != NULL)
 | 
			
		||||
        *size = sim->cart.romSize;
 | 
			
		||||
    return sim->cart.rom;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Retrieve a handle to the current cartridge RAM data */
 | 
			
		||||
uint8_t* vbGetSRAM(VB *sim, uint32_t *size) {
 | 
			
		||||
    if (size != NULL)
 | 
			
		||||
        *size = sim->cart.ramSize;
 | 
			
		||||
        *size = sim->cart.ram == NULL ? 0 : sim->cart.ramMask + 1;
 | 
			
		||||
    return sim->cart.ram;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Prepare a simulation instance for use */
 | 
			
		||||
void vbInit(VB *sim) {
 | 
			
		||||
/* 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;
 | 
			
		||||
    return sim->cart.rom;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    /* Breakpoint handlers */
 | 
			
		||||
    sim->onException = NULL;
 | 
			
		||||
    sim->onExecute   = NULL;
 | 
			
		||||
    sim->onFetch     = NULL;
 | 
			
		||||
    sim->onRead      = NULL;
 | 
			
		||||
    sim->onWrite     = NULL;
 | 
			
		||||
/* Retrieve the value of the program counter */
 | 
			
		||||
VBAPI uint32_t vbGetProgramCounter(VB *sim) {
 | 
			
		||||
    return sim->cpu.pc;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    /* Game pak */
 | 
			
		||||
    sim->cart.ram     = NULL;
 | 
			
		||||
    sim->cart.ramSize = 0;
 | 
			
		||||
    sim->cart.rom     = NULL;
 | 
			
		||||
    sim->cart.romSize = 0;
 | 
			
		||||
/* 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];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    /* Hardware reset */
 | 
			
		||||
/* Retrieve the value in a system register */
 | 
			
		||||
VBAPI uint32_t vbGetSystemRegister(VB *sim, int index) {
 | 
			
		||||
    return index < 0 || index > 31 ? 0 : cpuGetSystemRegister(sim, index);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Initialize a simulation instance */
 | 
			
		||||
VBAPI VB* vbInit(VB *sim) {
 | 
			
		||||
    sim->cart.ram      = NULL;
 | 
			
		||||
    sim->cart.rom      = NULL;
 | 
			
		||||
    sim->onExecute     = NULL;
 | 
			
		||||
    sim->onFetch       = NULL;
 | 
			
		||||
    sim->onRead        = NULL;
 | 
			
		||||
    sim->onWrite       = NULL;
 | 
			
		||||
    vbReset(sim);
 | 
			
		||||
    return sim;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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);
 | 
			
		||||
/* 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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Simulate a hardware reset */
 | 
			
		||||
void vbReset(VB *sim) {
 | 
			
		||||
    int x; /* Iterator */
 | 
			
		||||
VBAPI VB* vbReset(VB *sim) {
 | 
			
		||||
    uint32_t x; /* Iterator */
 | 
			
		||||
 | 
			
		||||
    /* Reset WRAM (the hardware does not do this) */
 | 
			
		||||
    /* WRAM (the hardware does not do this) */
 | 
			
		||||
    for (x = 0; x < 0x10000; x++)
 | 
			
		||||
        sim->wram[x] = 0x00;
 | 
			
		||||
 | 
			
		||||
    /* CPU (normal) */
 | 
			
		||||
    sim->cpu.pc = 0xFFFFFFF0;
 | 
			
		||||
    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 doesn't do this) */
 | 
			
		||||
    /* CPU (extra, hardware does not do this) */
 | 
			
		||||
    sim->cpu.adtre = 0x00000000;
 | 
			
		||||
    sim->cpu.eipc  = 0x00000000;
 | 
			
		||||
    sim->cpu.eipsw = 0x00000000;
 | 
			
		||||
| 
						 | 
				
			
			@ -184,99 +258,88 @@ void vbReset(VB *sim) {
 | 
			
		|||
    for (x = 0; x < 32; x++)
 | 
			
		||||
        sim->cpu.program[x] = 0x00000000;
 | 
			
		||||
 | 
			
		||||
    /* CPU (internal) */
 | 
			
		||||
    sim->cpu.bitstring = 0;
 | 
			
		||||
    /* CPU (other) */
 | 
			
		||||
    sim->cpu.clocks    = 0;
 | 
			
		||||
    sim->cpu.exception = 0;
 | 
			
		||||
    sim->cpu.stage     = CPU_FETCH;
 | 
			
		||||
    sim->cpu.nextPC    = 0xFFFFFFF0;
 | 
			
		||||
    sim->cpu.operation = CPU_FETCH;
 | 
			
		||||
    sim->cpu.step      = 0;
 | 
			
		||||
 | 
			
		||||
    return sim;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Specify a breakpoint handler */
 | 
			
		||||
void* vbSetCallback(VB *sim, int id, void *proc) {
 | 
			
		||||
    void *prev = vbGetCallback(sim, id);
 | 
			
		||||
/* Specify a new callback handler */
 | 
			
		||||
VBAPI void* vbSetCallback(VB *sim, int id, void *callback) {
 | 
			
		||||
    void  *prev   = NULL;
 | 
			
		||||
    void **target = NULL;
 | 
			
		||||
 | 
			
		||||
    /* Select callback by 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;
 | 
			
		||||
        /*case VB_EXCEPTION: target = (void **) &sim->onException; break;*/
 | 
			
		||||
        case VB_EXECUTE  : target = (void **) &sim->onExecute  ; break;
 | 
			
		||||
        case VB_FETCH    : target = (void **) &sim->onFetch    ; break;
 | 
			
		||||
        /*case VB_FRAME    : target = (void **) &sim->onFrame    ; break;*/
 | 
			
		||||
        case VB_READ     : target = (void **) &sim->onRead     ; break;
 | 
			
		||||
        case VB_WRITE    : target = (void **) &sim->onWrite    ; break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* Retrieve current state and update new state */
 | 
			
		||||
    if (target != NULL) {
 | 
			
		||||
        prev    = *target;
 | 
			
		||||
        *target = callback;
 | 
			
		||||
    }
 | 
			
		||||
    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;
 | 
			
		||||
            }
 | 
			
		||||
/* 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;
 | 
			
		||||
    }
 | 
			
		||||
    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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* 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;
 | 
			
		||||
    sim->cart.ram = sram;
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Specify a cartridge RAM buffer */
 | 
			
		||||
int vbSetSRAM(VB *sim, uint8_t *data, uint32_t size) {
 | 
			
		||||
 | 
			
		||||
    /* Specifying no SRAM */
 | 
			
		||||
    if (data == NULL) {
 | 
			
		||||
        sim->cart.ram     = NULL;
 | 
			
		||||
        sim->cart.ramSize = 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;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /* 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;
 | 
			
		||||
    sim->cart.rom = rom;
 | 
			
		||||
    return 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Write a value to memory */
 | 
			
		||||
void vbWrite(VB *sim, uint32_t address, int type, int32_t value) {
 | 
			
		||||
/* 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 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);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* Determine the size of a simulation instance */
 | 
			
		||||
VBAPI size_t vbSizeOf() {
 | 
			
		||||
    return sizeof (VB);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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)
 | 
			
		||||
        return 0;
 | 
			
		||||
    busWrite(sim, address, type, value, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/* 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);
 | 
			
		||||
    return vbRead(sim, address, type);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										213
									
								
								core/vb.h
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -15,38 +15,15 @@ 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 *********************************/
 | 
			
		||||
 | 
			
		||||
/* 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
 | 
			
		||||
/* 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
 | 
			
		||||
 | 
			
		||||
/* System registers */
 | 
			
		||||
#define VB_ADTRE 25
 | 
			
		||||
| 
						 | 
				
			
			@ -60,157 +37,55 @@ extern "C" {
 | 
			
		|||
#define VB_PSW    5
 | 
			
		||||
#define VB_TKCW   7
 | 
			
		||||
 | 
			
		||||
/* Other registers */
 | 
			
		||||
#define VB_PC 0
 | 
			
		||||
/* Memory access data types */
 | 
			
		||||
#define VB_S8  0
 | 
			
		||||
#define VB_U8  1
 | 
			
		||||
#define VB_S16 2
 | 
			
		||||
#define VB_U16 3
 | 
			
		||||
#define VB_S32 4
 | 
			
		||||
 | 
			
		||||
/* Callbacks */
 | 
			
		||||
#define VB_ONEXCEPTION 0
 | 
			
		||||
#define VB_ONEXECUTE   1
 | 
			
		||||
#define VB_ONFETCH     2
 | 
			
		||||
#define VB_ONREAD      3
 | 
			
		||||
#define VB_ONWRITE     4
 | 
			
		||||
/* CPU modes */
 | 
			
		||||
#define VB_ACCURACY 0
 | 
			
		||||
#define VB_DEBUG    1
 | 
			
		||||
#define VB_WHOLE    2
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/*********************************** Types ***********************************/
 | 
			
		||||
 | 
			
		||||
/* Forward references */
 | 
			
		||||
/* Simulation state */
 | 
			
		||||
typedef struct VB VB;
 | 
			
		||||
 | 
			
		||||
/* 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 */
 | 
			
		||||
};
 | 
			
		||||
/* Callbacks */
 | 
			
		||||
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);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/************************************ 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 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 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);
 | 
			
		||||
/******************************* API Commands ********************************/
 | 
			
		||||
 | 
			
		||||
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 uint32_t vbGetProgramCounter (VB *sim);
 | 
			
		||||
VBAPI int32_t  vbGetProgramRegister(VB *sim, int index);
 | 
			
		||||
VBAPI uint32_t vbGetSystemRegister (VB *sim, int index);
 | 
			
		||||
VBAPI VB*      vbInit              (VB *sim);
 | 
			
		||||
VBAPI int32_t  vbRead              (VB *sim, uint32_t address, int type);
 | 
			
		||||
VBAPI VB*      vbReset             (VB *sim);
 | 
			
		||||
VBAPI void*    vbSetCallback       (VB *sim, int index, void *callback);
 | 
			
		||||
VBAPI int      vbSetCartRAM        (VB *sim, void *sram, uint32_t size);
 | 
			
		||||
VBAPI int      vbSetCartROM        (VB *sim, void *rom, uint32_t size);
 | 
			
		||||
VBAPI uint32_t vbSetProgramCounter (VB *sim, uint32_t value);
 | 
			
		||||
VBAPI int32_t  vbSetProgramRegister(VB *sim, int index, int32_t value);
 | 
			
		||||
VBAPI uint32_t vbSetSystemRegister (VB *sim, int index, uint32_t value);
 | 
			
		||||
VBAPI size_t   vbSizeOf            ();
 | 
			
		||||
VBAPI int32_t  vbWrite             (VB *sim, uint32_t address, int type, int32_t value);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,4 +1,4 @@
 | 
			
		|||
Copyright (C) 2023 Guy Perfect
 | 
			
		||||
Copyright (C) 2024 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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										2
									
								
								makefile
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,7 +1,7 @@
 | 
			
		|||
.PHONY: help
 | 
			
		||||
help:
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Virtual Boy Emulator - March 12, 2023"
 | 
			
		||||
	@echo "Virtual Boy Emulator - October 10, 2024"
 | 
			
		||||
	@echo
 | 
			
		||||
	@echo "Target build environment is any Debian with the following packages:"
 | 
			
		||||
	@echo "  emscripten"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										691
									
								
								web/App.js
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,691 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
							
								
								
									
										203
									
								
								web/Bundle.java
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,203 +0,0 @@
 | 
			
		|||
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);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										310
									
								
								web/_boot.js
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,310 +0,0 @@
 | 
			
		|||
// 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);
 | 
			
		||||
| 
						 | 
				
			
			@ -1,88 +0,0 @@
 | 
			
		|||
"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);
 | 
			
		||||
							
								
								
									
										292
									
								
								web/core/Core.js
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,292 +0,0 @@
 | 
			
		|||
// 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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,338 +0,0 @@
 | 
			
		|||
"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();
 | 
			
		||||
| 
						 | 
				
			
			@ -1,546 +0,0 @@
 | 
			
		|||
// 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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,79 +0,0 @@
 | 
			
		|||
#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);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										1439
									
								
								web/debugger/CPU.js
								
								
								
								
							
							
						
						| 
						 | 
				
			
			@ -1,104 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,177 +0,0 @@
 | 
			
		|||
// 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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,574 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,77 +0,0 @@
 | 
			
		|||
{
 | 
			
		||||
    "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"
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1 +0,0 @@
 | 
			
		|||
<!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>
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 459 B  | 
| 
						 | 
				
			
			@ -1,4 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 484 B  | 
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 652 B  | 
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 367 B  | 
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
: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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,7 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 522 B  | 
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 710 B  | 
| 
						 | 
				
			
			@ -1,554 +0,0 @@
 | 
			
		|||
: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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,28 +0,0 @@
 | 
			
		|||
: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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 361 B  | 
| 
						 | 
				
			
			@ -1,6 +0,0 @@
 | 
			
		|||
<?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>
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 484 B  | 
| 
						 | 
				
			
			@ -1,194 +0,0 @@
 | 
			
		|||
/******************************** 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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,63 +0,0 @@
 | 
			
		|||
: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;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -1,249 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,119 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,157 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,473 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,86 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,154 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,46 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,92 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,106 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,455 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,113 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,109 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,380 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,317 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,363 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,67 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,51 +0,0 @@
 | 
			
		|||
// 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 };
 | 
			
		||||
| 
						 | 
				
			
			@ -1,479 +0,0 @@
 | 
			
		|||
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 };
 | 
			
		||||