diff --git a/package-lock.json b/package-lock.json index ab8374f..f7ef6a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -107,9 +107,9 @@ } }, "node_modules/@eslint/js": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.56.0.tgz", - "integrity": "sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", + "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -171,32 +171,32 @@ "dev": true }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" @@ -219,9 +219,9 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.22", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.22.tgz", - "integrity": "sha512-Wf963MzWtA2sjrNt+g18IAln9lKnlRp+K2eH4jjIoF1wYeq3aMREpG09xhlhdzS0EjwU7qmUJYangWa+151vZw==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -276,9 +276,9 @@ } }, "node_modules/@types/eslint": { - "version": "8.56.2", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.2.tgz", - "integrity": "sha512-uQDwm1wFHmbBbCZCqAlq6Do9LYwByNZHWzXppSnay9SuwJ+VRbjkbLABer54kcPnMSlG6Fdiy2yaFXm/z9Z5gw==", + "version": "8.56.5", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.5.tgz", + "integrity": "sha512-u5/YPJHo1tvkSF2CE0USEkxon82Z5DBy2xR+qfyYNszpX9qcs4sT6uq2kBbj4BXY1+DBGDPnrhMZV3pKWGNukw==", "dev": true, "dependencies": { "@types/estree": "*", @@ -308,18 +308,18 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.25", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz", + "integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, "node_modules/@types/semver": { - "version": "7.5.7", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.7.tgz", - "integrity": "sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==", + "version": "7.5.8", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", + "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { @@ -903,9 +903,9 @@ } }, "node_modules/browserslist": { - "version": "4.22.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz", - "integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==", + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", "dev": true, "funding": [ { @@ -922,8 +922,8 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001580", - "electron-to-chromium": "^1.4.648", + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", "node-releases": "^2.0.14", "update-browserslist-db": "^1.0.13" }, @@ -950,9 +950,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001587", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001587.tgz", - "integrity": "sha512-HMFNotUmLXn71BQxg8cijvqxnIAofforZOwGsxyXJ0qugTdspUF4sPSJ2vhgprHCB996tIDzEq1ubumPDV8ULA==", + "version": "1.0.30001597", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz", + "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==", "dev": true, "funding": [ { @@ -1104,34 +1104,6 @@ "webpack": "^5.1.0" } }, - "node_modules/copy-webpack-plugin/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/copy-webpack-plugin/node_modules/globby": { "version": "14.0.1", "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.1.tgz", @@ -1152,12 +1124,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, "node_modules/copy-webpack-plugin/node_modules/path-type": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", @@ -1170,25 +1136,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", - "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/copy-webpack-plugin/node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -1310,15 +1257,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.668", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.668.tgz", - "integrity": "sha512-ZOBocMYCehr9W31+GpMclR+KBaDZOoAEabLdhpZ8oU1JFDwIaFY0UDbpXVEUFc0BIP2O2Qn3rkfCjQmMR4T/bQ==", + "version": "1.4.700", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.700.tgz", + "integrity": "sha512-40dqKQ3F7C8fbBEmjSeJ+qEHCKzPyrP9SkeIBZ3wSCUH9nhWStrDz030XlDzlhNhlul1Z0fz7TpDFnsIzo4Jtg==", "dev": true }, "node_modules/enhanced-resolve": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", - "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz", + "integrity": "sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -1368,16 +1315,16 @@ } }, "node_modules/eslint": { - "version": "8.56.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.56.0.tgz", - "integrity": "sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", + "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.56.0", - "@humanwhocodes/config-array": "^0.11.13", + "@eslint/js": "8.57.0", + "@humanwhocodes/config-array": "^0.11.14", "@humanwhocodes/module-importer": "^1.0.1", "@nodelib/fs.walk": "^1.2.8", "@ungap/structured-clone": "^1.2.0", @@ -1668,9 +1615,9 @@ } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, "node_modules/fs.realpath": { @@ -1819,9 +1766,9 @@ } }, "node_modules/hasown": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz", - "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", "dev": true, "dependencies": { "function-bind": "^1.1.2" @@ -2772,9 +2719,9 @@ ] }, "node_modules/sass": { - "version": "1.71.0", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.0.tgz", - "integrity": "sha512-HKKIKf49Vkxlrav3F/w6qRuPcmImGVbIXJ2I3Kg0VMA+3Bav+8yE9G5XmP5lMj6nl4OlqbPftGAscNaNu28b8w==", + "version": "1.71.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz", + "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==", "dev": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", @@ -2789,9 +2736,9 @@ } }, "node_modules/sass-loader": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.0.tgz", - "integrity": "sha512-LS2mLeFWA+orYxHNu+O18Xe4jR0kyamNOOUsE3NyBP4DvIL+8stHpNX0arYTItdPe80kluIiJ7Wfe/9iHSRO0Q==", + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-14.1.1.tgz", + "integrity": "sha512-QX8AasDg75monlybel38BZ49JP5Z+uSKfKwF2rO7S74BywaRmGQMUBw9dtkS+ekyM/QnP+NOrRYq8ABMZ9G8jw==", "dev": true, "dependencies": { "neo-async": "^2.6.2" @@ -2829,23 +2776,58 @@ } }, "node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "node": ">= 10.13.0" + "node": ">= 12.13.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" } }, + "node_modules/schema-utils/node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/schema-utils/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/schema-utils/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, "node_modules/semver": { "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", @@ -3023,9 +3005,9 @@ } }, "node_modules/terser": { - "version": "5.27.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", - "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "version": "5.29.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.29.1.tgz", + "integrity": "sha512-lZQ/fyaIGxsbGxApKmoPTODIzELy3++mXhS5hOqaAWZjQtpq/hFHAc+rm29NND1rYRxRWKcjuARNwULNXa5RtQ==", "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", @@ -3074,6 +3056,24 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -3093,9 +3093,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.2.1.tgz", - "integrity": "sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", "dev": true, "engines": { "node": ">=16" @@ -3149,9 +3149,9 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz", + "integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -3238,9 +3238,9 @@ } }, "node_modules/webpack": { - "version": "5.90.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.2.tgz", - "integrity": "sha512-ziXu8ABGr0InCMEYFnHrYweinHK2PWrMqnwdHk2oK3rRhv/1B+2FnfwYv5oD+RrknK/Pp/Hmyvu+eAsaMYhzCw==", + "version": "5.90.3", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.90.3.tgz", + "integrity": "sha512-h6uDYlWCctQRuXBs1oYpVe6sFcWedl0dpcVaTf/YF67J9bKvwJajFulMVSYKHrksMB3I/pIagRzDxwxkebuzKA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -3383,6 +3383,24 @@ "node": ">=4.0" } }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/src/computer.ts b/src/computer.ts index 79b6621..18ac91c 100644 --- a/src/computer.ts +++ b/src/computer.ts @@ -2,6 +2,7 @@ import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, Ui import { byte_array_to_js_source, format_hex } from "./etc"; import { Instruction, ISA } from "./instructionSet"; import { m256, u2, u3, u8 } from "./num"; +import { DEFAULT_VRAM_BANK } from "./constants"; export type TempInstrState = { pos: u8; @@ -25,6 +26,7 @@ export class Computer { private carry_flag: boolean = false; private program_counter: u8 = 0; private bank: u2 = 0; + private vram_bank: u2 = DEFAULT_VRAM_BANK; private current_instr: TempInstrState | null = null; events: CpuEventHandler = new CpuEventHandler(); @@ -142,9 +144,9 @@ export class Computer { return this.call_stack.pop() ?? null; } - setBank(bank_no: u2): void { - this.events.dispatch(CpuEvent.SwitchBank, { bank: bank_no }); - this.bank = bank_no; + setBank(bank: u2): void { + this.events.dispatch(CpuEvent.SwitchBank, { bank: bank }); + this.bank = bank; } setCarry(state: boolean): void { @@ -156,6 +158,11 @@ export class Computer { return this.carry_flag; } + setVramBank(bank: u2): void { + this.vram_bank = bank; + this.events.dispatch(CpuEvent.SetVramBank, { bank }); + } + reset(): void { this.events.dispatch(CpuEvent.Reset); this.banks = init_banks(); @@ -164,6 +171,7 @@ export class Computer { this.current_instr = null; this.program_counter = 0; this.carry_flag = false; + this.vram_bank = 3; } init_events(ui: UiCpuSignalHandler): void { @@ -172,10 +180,11 @@ export class Computer { }); ui.listen(UiCpuSignal.RequestMemoryChange, ({ address, bank, value }) => this.setMemory(address, value, bank)); ui.listen(UiCpuSignal.RequestRegisterChange, ({ register_no, value }) => this.setRegister(register_no, value)); - ui.listen(UiCpuSignal.RequestMemoryDump, () => - this.events.dispatch(CpuEvent.MemoryDumped, { memory: this.dump_memory() }) - ); + ui.listen(UiCpuSignal.RequestMemoryDump, (callback) => callback(this.dump_memory())); ui.listen(UiCpuSignal.RequestCpuReset, () => this.reset()); + ui.listen(UiCpuSignal.RequestProgramCounterChange, ({ address }) => { + this.setProgramCounter(address); + }); } load_memory(program: Array): void { diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..ec7a3dd --- /dev/null +++ b/src/constants.ts @@ -0,0 +1,3 @@ +const DEFAULT_VRAM_BANK = 3; + +export { DEFAULT_VRAM_BANK }; diff --git a/src/etc.ts b/src/etc.ts index 1cb865e..3282c61 100644 --- a/src/etc.ts +++ b/src/etc.ts @@ -3,6 +3,9 @@ * @copyright Alexander Bass 2024 * @license GPL-3.0 */ +import el from "./util/elementMaker"; +// Re-export el +export { el }; import { u8 } from "./num"; /** @@ -26,28 +29,6 @@ export const byte_array_to_js_source = (bytes: Array): string => { return str; }; -/** - * Create an html element - * @param type - * @param id id attribute to set - */ -export function el( - type: E, - id?: string, - class_list?: string -): HTMLElementTagNameMap[E]; -export function el(type: string, id?: string, class_list?: string): HTMLElement | undefined { - const element = document.createElement(type); - if (id !== undefined) { - element.id = id; - } - if (class_list !== undefined) { - element.className = class_list; - } - - return element; -} - export type NonEmptyArray = T[] & { 0: T }; export const SVG_NS = "http://www.w3.org/2000/svg"; @@ -57,6 +38,10 @@ export function in_range(check: number, start: number, end: number): boolean { return false; } +/** + * Gets the `i`th element in a list. Negative indices return null. + * Out of range indices return null + */ export function at(l: Array, i: number): T | null { if (i < 0) { return null; diff --git a/src/events.ts b/src/events.ts index 2bd52eb..1fec75b 100644 --- a/src/events.ts +++ b/src/events.ts @@ -22,10 +22,10 @@ export enum CpuEvent { Print, Reset, Halt, - MemoryDumped, MemoryAccessed, SwitchBank, SetFlagCarry, + SetVramBank, } type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle; @@ -40,9 +40,9 @@ interface CpuEventMap { [CpuEvent.InvalidParsed]: { pos: u8; code: u8 }; [CpuEvent.InstructionExecuted]: { instr: Instruction }; [CpuEvent.SwitchBank]: { bank: u2 }; + [CpuEvent.SetVramBank]: { bank: u2 }; [CpuEvent.Print]: string; [CpuEvent.SetFlagCarry]: boolean; - [CpuEvent.MemoryDumped]: { memory: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] }; } export interface CpuEventHandler extends EventHandler { @@ -68,14 +68,17 @@ export enum UiCpuSignal { RequestRegisterChange, RequestCpuReset, RequestMemoryDump, + RequestProgramCounterChange, } -type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset | UiCpuSignal.RequestMemoryDump; +type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset; interface UiCpuSignalMap { [UiCpuSignal.RequestCpuCycle]: number; [UiCpuSignal.RequestMemoryChange]: { address: u8; bank: u2; value: u8 }; [UiCpuSignal.RequestRegisterChange]: { register_no: u3; value: u8 }; + [UiCpuSignal.RequestProgramCounterChange]: { address: u8 }; + [UiCpuSignal.RequestMemoryDump]: (memory: [Uint8Array, Uint8Array, Uint8Array, Uint8Array]) => void; } export interface UiCpuSignalHandler extends EventHandler { diff --git a/src/include/index.html b/src/include/index.html index cd39acf..5ce2782 100644 --- a/src/include/index.html +++ b/src/include/index.html @@ -26,31 +26,40 @@ -
-
- - - - -
- -
+
+ +
+
+
+ +
CPU
+ + + + + + +
+
BANK 0
+
BANK 1
+
BANK 2
+
BANK 3
+
+ + + + + + +
VRAM
+
+
diff --git a/src/instructionSet.ts b/src/instructionSet.ts index 3c6c5a2..784575f 100644 --- a/src/instructionSet.ts +++ b/src/instructionSet.ts @@ -50,6 +50,7 @@ interface GenericComputer { setBank: (bank_no: u2) => void; getCarry(): boolean; setCarry(state: boolean): void; + setVramBank(bank: u2): void; } interface AfterExecutionComputerAction { @@ -738,3 +739,15 @@ ISA.insertInstruction(0xf1, { a.dispatch(CpuEvent.Print, byte.toString(10)); }, }); + +ISA.insertInstruction(0xff, { + name: "Set VRAM Bank", + desc: "Set memory bank which screen gets pixels from memory bank (P1)", + params: [new ConstParam("memory bank to select")], + execute(c, p, a) { + const bank_no = p[0]; + if (!isU2(bank_no)) throw new Error("TO2O"); + c.setVramBank(bank_no); + a.dispatch(CpuEvent.SetVramBank, { bank: bank_no }); + }, +}); diff --git a/src/isaGenerator.ts b/src/isaGenerator.ts index 10c4387..f56d789 100644 --- a/src/isaGenerator.ts +++ b/src/isaGenerator.ts @@ -5,7 +5,7 @@ */ import { format_hex, in_range } from "./etc"; import { InstrCategory, Instruction, InstructionSet, ParameterType, ParamType } from "./instructionSet"; -import { u8 } from "./num.js"; +import { u8 } from "./num"; export function generate_isa(iset: InstructionSet): string { const instructions: Array<[u8, Instruction]> = []; diff --git a/src/style/memory_registers.scss b/src/style/memory_registers.scss index 8472844..bb9b3e1 100644 --- a/src/style/memory_registers.scss +++ b/src/style/memory_registers.scss @@ -1,6 +1,12 @@ #memory { - grid-template-columns: repeat(16, min-content); - + .celled_viewer.selected { + grid-template-columns: repeat(16, min-content); + // display: block; + display: grid; + } + .celled_viewer { + display: none; + } .program_counter { outline: 3px solid orange; } diff --git a/src/style/style.scss b/src/style/style.scss index 3ff17a1..9172226 100644 --- a/src/style/style.scss +++ b/src/style/style.scss @@ -25,6 +25,7 @@ body { main { display: flex; justify-content: center; + align-items: flex-start; } #grid { @@ -33,13 +34,12 @@ main { grid-template-columns: min-content min-content min-content; grid-template-rows: min-content min-content min-content; grid-template-areas: - ". regmemlabel . cycles " - ". registers . bank " + ". regmemlabel . cycles" + ". registers . bank " + "reset memory memory memory" "title memory memory memory" - ". buttons buttons buttons "; - #memory { - grid-area: memory; - } + ". buttons buttons buttons"; + #window_box { grid-area: windowbox; } @@ -66,6 +66,25 @@ main { user-select: none; transform: scale(-1, -1); } + #memory { + grid-area: memory; + } + #reset_buttons { + grid-area: reset; + } +} + +#reset_buttons { + flex-direction: column; + display: flex; + align-items: end; + button { + margin-bottom: 7px; + margin-right: 7px; + border: 5px solid yellow; + padding: 5px; + width: fit-content; + } } #labelcontainer { diff --git a/src/style/windows.scss b/src/style/windows.scss index a28c9fc..4ef990b 100644 --- a/src/style/windows.scss +++ b/src/style/windows.scss @@ -4,7 +4,7 @@ height: 18px; position: absolute; bottom: 0; - border-bottom: 5px solid yellow; + border-bottom: 5px solid var(--border); cursor: s-resize; } @@ -15,6 +15,12 @@ gap: 10px; margin-left: 10px; width: 500px; + .window:not(:has(#resize)) { + border-bottom: 5px solid var(--border); + &.collapsed { + border-bottom: unset; + } + } .window { overflow-y: hidden; position: relative; @@ -30,16 +36,18 @@ font-size: 0.6em; color: lightgray; border-bottom: 5px solid var(--border); - background: repeating-linear-gradient( - to top, - transparent, - transparent 2px, - transparent 2px, - yellow 2px, - yellow 4px - ), - repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px); + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAYAAACp8Z5+AAAAHUlEQVQI12P4/5/h////DP8ZoICJAQ0wIstiVQEA3CAI/c69TucAAAAASUVORK5CYII="); + image-rendering: pixelated; + // background: repeating-linear-gradient( + // to top, + // transparent, + // transparent 2px, + // transparent 2px, + // yellow 2px, + // yellow 4px + // ), + // repeating-linear-gradient(to right, transparent, transparent 2px, transparent 2px, yellow 2px, yellow 4px); #text { word-break: keep-all; white-space: nowrap; @@ -56,9 +64,6 @@ } } } - .window.collapsed > :not(:first-child) { - display: none; - } } .window#tv { @@ -69,6 +74,41 @@ } } +#bank_viz { + #visualization { + user-select: none; + display: flex; + flex-direction: row; + justify-content: center; + align-items: center; + font-size: 80%; + // max-width: 500px; + #cpu, + #target { + border: 3px solid yellow; + padding: 5px; + } + svg polyline { + stroke-linecap: butt; + stroke-width: 3; + fill: none; + } + + svg { + width: 50px; + } + #vram_bank { + transform: scale(-1, 1); + } + #targets { + white-space: nowrap; + display: flex; + flex-direction: column; + gap: 13px; + } + } +} + #instruction_explainer { display: flex; flex-direction: column; @@ -86,6 +126,7 @@ } #expl_icon { + user-select: none; margin-inline-end: 0.5em; font-size: 30px; padding: 5px; diff --git a/src/ui.ts b/src/ui.ts index 03659a9..df7c537 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -1,15 +1,20 @@ -import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEvent, UiEventHandler } from "./events"; +import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "./events"; import { $ } from "./etc"; -import { InstructionExplainer } from "./ui/windows/instructionExplainer"; -import { MemoryView } from "./ui/memoryView"; -import { frequencyIndicator } from "./ui/frequencyIndicator"; -import { RegisterView } from "./ui/registerView"; -import { Screen } from "./ui/windows/screen"; -import { EditButton } from "./ui/editButton"; -import { UiComponent, UiComponentConstructor } from "./ui/uiComponent"; -import { pausePlay } from "./ui/pausePlay"; -import { Printout } from "./ui/windows/printout"; -import { SaveLoad } from "./ui/saveLoad"; +import UiComponent, { UiComponentConstructor } from "./ui/uiComponent"; +// Components +import MemoryView from "./ui/components/memoryView"; +import frequencyIndicator from "./ui/components/frequencyIndicator"; +import RegisterView from "./ui/components/registerView"; +import BankSelector from "./ui/components/bank_view_selector"; +import EditButton from "./ui/components/editButton"; +import pausePlay from "./ui/components/pausePlay"; +import SaveLoad from "./ui/components/saveLoad"; +import ResetButtons from "./ui/components/reset_buttons"; +// Window Components +import InstructionExplainer from "./ui/windows/instructionExplainer"; +import Screen from "./ui/windows/screen"; +import Printout from "./ui/windows/printout"; +import BankVisualizer from "./ui/windows/bank_visualizer"; export class UI { ui_events: UiEventHandler = new UiEventHandler(); @@ -26,6 +31,9 @@ export class UI { this.register_component(EditButton, $("edit_button")); this.register_component(pausePlay, $("controls_buttons")); this.register_component(SaveLoad, $("save_load_buttons")); + this.register_component(BankSelector, $("memory_bank_view")); + this.register_component(BankVisualizer, $("bank_viz")); + this.register_component(ResetButtons, $("reset_buttons")); } private register_component(ctor: UiComponentConstructor, e: HTMLElement): void { diff --git a/src/ui/celledViewer.ts b/src/ui/celledViewer.ts index 0e80068..ca87631 100644 --- a/src/ui/celledViewer.ts +++ b/src/ui/celledViewer.ts @@ -5,28 +5,36 @@ */ import { NonEmptyArray, el, format_hex } from "../etc"; import { u8 } from "../num"; +import EditorContext from "./editableHex"; interface GenericCell { el: HTMLElement; } -export abstract class CelledViewer { +export default class CelledViewer { cells: Array = []; - width: number; - height: number; - element: HTMLElement; - constructor(width: number, height: number, element: HTMLElement) { - this.element = element; + readonly width: number; + readonly height: number; + container: HTMLElement; + editor: EditorContext; + constructor(width: number, height: number, element: HTMLElement, edit_callback: (address: u8, value: u8) => void) { + this.container = element; this.width = width; this.height = height; - this.element.classList.add("celled_viewer"); + + this.container.classList.add("celled_viewer"); for (let i = 0; i < this.width * this.height; i++) { - const mem_cell_el = el("div"); + const mem_cell_el = el("div").fin(); mem_cell_el.append("0", "0"); - this.element.appendChild(mem_cell_el); + this.container.appendChild(mem_cell_el); const mem_cell = { el: mem_cell_el }; this.cells.push(mem_cell); } + const list = this.cells.map((c) => c.el); + + this.editor = new EditorContext(list, this.width, this.height, (address, value) => { + edit_callback(address, value); + }); } reset(): void { @@ -53,6 +61,12 @@ export abstract class CelledViewer { } } + clear_all_classes(): void { + for (const cell of this.cells) { + cell.el.className = ""; + } + } + add_cell_class_exclusive(address: u8, css_class: string): void { this.remove_all_cell_class(css_class); this.add_cell_class(address, css_class); diff --git a/src/ui/components/bank_view_selector.ts b/src/ui/components/bank_view_selector.ts new file mode 100644 index 0000000..7587e27 --- /dev/null +++ b/src/ui/components/bank_view_selector.ts @@ -0,0 +1,35 @@ +import { el } from "../../etc"; +import { u2 } from "../../num"; +import { UiEventHandler, UiEvent } from "../../events"; +import UiComponent from "../uiComponent"; + +export default class BankSelector implements UiComponent { + container: HTMLElement; + events: UiEventHandler; + private bank_buttons: Array; + constructor(element: HTMLElement, events: UiEventHandler) { + this.container = element; + this.events = events; + + const bank_boxes = el("div").id("bank_boxes").fin(); + this.bank_buttons = []; + for (let i = 0; i < 4; i++) { + const button = el("button").cl("nostyle").fin(); + bank_boxes.appendChild(button); + button.addEventListener("click", () => { + for (const b of this.bank_buttons) b.classList.remove("selected"); + button.classList.add("selected"); + this.events.dispatch(UiEvent.ChangeViewBank, { bank: i as u2 }); + }); + button.textContent = i.toString(); + this.bank_buttons.push(button); + } + this.bank_buttons[0].classList.add("selected"); + + this.container.appendChild(bank_boxes); + } + reset(): void { + for (const b of this.bank_buttons) b.classList.remove("selected"); + this.bank_buttons[0].classList.add("selected"); + } +} diff --git a/src/ui/components/editButton.ts b/src/ui/components/editButton.ts new file mode 100644 index 0000000..91357c9 --- /dev/null +++ b/src/ui/components/editButton.ts @@ -0,0 +1,41 @@ +import { el, $ } from "../../etc"; +import { UiEventHandler, UiEvent, UiCpuSignalHandler, UiCpuSignal } from "../../events"; +import UiComponent from "../uiComponent"; + +export default class EditButton implements UiComponent { + container: HTMLElement; + events: UiEventHandler; + cpu_signals: UiCpuSignalHandler; + constructor(element: HTMLElement, event: UiEventHandler, cpu_signals: UiCpuSignalHandler) { + this.container = element; + this.events = event; + this.cpu_signals = cpu_signals; + const image = el("img").at("src", "pencil.png").st("width", "20px").st("height", "20px").fin(); + this.container.classList.add("editor_toggle"); + this.container.addEventListener("click", () => this.edit_toggle()); + this.container.appendChild(image); + } + + reset(): void { + const is_on = this.container.classList.contains("on"); + if (is_on) { + this.edit_toggle(); + } + } + + edit_toggle(): void { + const is_on = this.container.classList.contains("on"); + if (is_on) { + this.container.classList.remove("on"); + $("root").classList.remove("editor"); + this.container.classList.add("off"); + this.events.dispatch(UiEvent.EditOff); + } else { + this.events.dispatch(UiEvent.EditOn); + $("root").classList.add("editor"); + this.container.classList.add("on"); + this.container.classList.remove("off"); + this.cpu_signals.dispatch(UiCpuSignal.RequestProgramCounterChange, { address: 0 }); + } + } +} diff --git a/src/ui/frequencyIndicator.ts b/src/ui/components/frequencyIndicator.ts similarity index 77% rename from src/ui/frequencyIndicator.ts rename to src/ui/components/frequencyIndicator.ts index b1b4bf8..cd48836 100644 --- a/src/ui/frequencyIndicator.ts +++ b/src/ui/components/frequencyIndicator.ts @@ -1,15 +1,15 @@ -import { CpuEvent, CpuEventHandler, UiEventHandler } from "../events"; -import { UiComponent } from "./uiComponent"; +import { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events"; +import UiComponent from "../uiComponent"; -export class frequencyIndicator implements UiComponent { - element: HTMLElement; +export default class frequencyIndicator implements UiComponent { + container: HTMLElement; private running: number | null = null; private count: number = 0; private last_value: number = 0; private last_time: number = 0; events: UiEventHandler; constructor(element: HTMLElement, events: UiEventHandler) { - this.element = element; + this.container = element; this.events = events; this.start(); } @@ -34,11 +34,11 @@ export class frequencyIndicator implements UiComponent { const value = Math.round(this.count / dt); if (this.last_value !== value) { - this.element.textContent = `${value}hz`; + this.container.textContent = `${value}hz`; this.last_value = value; } if (value === 0) { - this.element.textContent = ""; + this.container.textContent = ""; } this.last_time = new_time; this.count = 0; @@ -51,6 +51,7 @@ export class frequencyIndicator implements UiComponent { this.stop(); this.count = 0; this.last_value = 0; + this.start(); } init_cpu_events(c: CpuEventHandler): void { c.listen(CpuEvent.Cycle, () => { diff --git a/src/ui/components/memoryView.ts b/src/ui/components/memoryView.ts new file mode 100644 index 0000000..8dba9a7 --- /dev/null +++ b/src/ui/components/memoryView.ts @@ -0,0 +1,126 @@ +import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../../events"; +import { ParamType } from "../../instructionSet"; +import { u2, u8 } from "../../num"; +import UiComponent from "../uiComponent"; +import { el } from "../../etc"; +import CelledViewer from "../celledViewer"; + +/** Only to be run once */ +function create_banks( + element: HTMLElement, + edit_callback: (address: u8, bank: u2, value: u8) => void +): [CelledViewer, CelledViewer, CelledViewer, CelledViewer] { + const list: Array = []; + + for (let i = 0; i < 4; i++) { + const child = el("div").fin(); + list.push( + new CelledViewer(16, 16, child, (address: u8, value: u8) => { + edit_callback(address, i as u2, value); + }) + ); + + element.appendChild(child); + } + list[0].container.classList.add("selected"); + + return list as [CelledViewer, CelledViewer, CelledViewer, CelledViewer]; +} + +export default class MemoryView implements UiComponent { + container: HTMLElement; + program_counter: u8 = 0; + last_accessed_cell: { address: u8; bank: u2 } | null = null; + events: UiEventHandler; + cpu_signals: UiCpuSignalHandler; + banks: [CelledViewer, CelledViewer, CelledViewer, CelledViewer]; + constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { + this.container = element; + this.events = events; + this.cpu_signals = cpu_signals; + + this.banks = create_banks(element, (address, bank, value) => { + cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value }); + }); + for (const bank of this.banks) { + this.events.listen(UiEvent.EditOn, () => { + bank.editor.enable(); + bank.clear_all_classes(); + }); + this.events.listen(UiEvent.EditOff, () => { + bank.editor.disable(); + bank.clear_all_classes(); + }); + } + this.events.listen(UiEvent.ChangeViewBank, ({ bank }) => this.set_bank(bank)); + } + + get program(): CelledViewer { + return this.banks[0]; + } + + set_bank(bank: u2): void { + for (const bank of this.banks) bank.container.classList.remove("selected"); + this.banks[bank].container.classList.add("selected"); + } + + set_program_counter(position: u8): void { + this.program.remove_cell_class(this.program_counter, "program_counter"); + this.program.add_cell_class(position, "program_counter"); + this.program_counter = position; + } + + reset(): void { + for (const viewer of this.banks) viewer.reset(); + this.last_accessed_cell = null; + this.set_program_counter(0); + } + + init_cpu_events(c: CpuEventHandler): void { + c.listen(CpuEvent.MemoryAccessed, ({ address, bank, value }) => { + if (this.last_accessed_cell?.address !== address || this.last_accessed_cell?.bank !== bank) { + if (this.last_accessed_cell !== null) { + this.banks[this.last_accessed_cell.bank].remove_cell_class(this.last_accessed_cell.address, "last_access"); + } + this.banks[bank].add_cell_class(address, "last_access"); + this.last_accessed_cell = { address, bank }; + } + }); + c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => { + if (bank !== 0) { + return; + } + this.banks[bank].set_cell_value(address, value); + }); + c.listen(CpuEvent.ProgramCounterChanged, ({ counter }) => { + this.set_program_counter(counter); + }); + c.listen(CpuEvent.ParameterParsed, ({ param, code, pos }) => { + this.program.add_cell_class(pos, "instruction_argument"); + const t = param.type; + this.program.remove_cell_class(pos, "constant", "register", "memory", "instruction", "invalid"); + let name: string = ""; + if (t === ParamType.Const) { + name = "constant"; + } else if (t === ParamType.Memory) { + name = "memory"; + } else if (t === ParamType.Register) { + name = "register"; + } else { + throw new Error("unreachable"); + } + this.program.add_cell_class(pos, name); + }); + c.listen(CpuEvent.InstructionParsed, ({ instr, code, pos }) => { + this.program.remove_all_cell_class("instruction_argument"); + this.program.remove_all_cell_class("current_instruction"); + this.program.remove_cell_class(pos, "constant", "register", "memory", "invalid"); + this.program.add_cell_class(pos, "current_instruction"); + this.program.add_cell_class(pos, "instruction"); + }); + c.listen(CpuEvent.InvalidParsed, ({ code, pos }) => { + this.program.remove_cell_class(pos, "constant", "register", "memory", "instruction"); + this.program.add_cell_class(pos, "invalid"); + }); + } +} diff --git a/src/ui/pausePlay.ts b/src/ui/components/pausePlay.ts similarity index 64% rename from src/ui/pausePlay.ts rename to src/ui/components/pausePlay.ts index e8ce91e..b4560fc 100644 --- a/src/ui/pausePlay.ts +++ b/src/ui/components/pausePlay.ts @@ -1,11 +1,11 @@ -import { el } from "../etc"; -import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../events"; -import { UiComponent } from "./uiComponent"; +import { el } from "../../etc"; +import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events"; +import UiComponent from "../uiComponent"; const MAX_SLIDER = 1000; -export class pausePlay implements UiComponent { - element: HTMLElement; +export default class pausePlay implements UiComponent { + container: HTMLElement; start_button: HTMLButtonElement; step_button: HTMLButtonElement; range: HTMLInputElement; @@ -15,35 +15,32 @@ export class pausePlay implements UiComponent { cpu_signals: UiCpuSignalHandler; constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { - this.element = element; + this.container = element; this.events = events; this.cpu_signals = cpu_signals; - this.start_button = el("button", "pause_play_button"); - this.step_button = el("button", "step_button"); - this.range = el("input", "speed_range"); - this.range.max = MAX_SLIDER.toString(); - this.range.min = "0"; - this.range.type = "range"; + this.start_button = el("button").id("pause_play_button").tx("Start").fin(); + this.step_button = el("button").id("step_button").tx("Step").fin(); + this.range = el("input") + .id("speed_range") + .at("type", "range") + .at("min", "0") + .at("max", MAX_SLIDER.toString()) + .at("value", "0") + .fin(); this.start_button.addEventListener("click", () => this.toggle()); this.step_button.addEventListener("click", () => this.step()); this.range.addEventListener("input", (e) => { const delay = MAX_SLIDER - parseInt((e.target as HTMLInputElement).value, 10) + 10; this.cycle_delay = delay; }); - this.start_button.textContent = "Start"; - this.step_button.textContent = "Step"; - this.element.appendChild(this.start_button); - this.element.appendChild(this.step_button); - this.element.appendChild(this.range); - this.cycle_delay = 1000; - this.range.value = "0"; - this.events.listen(UiEvent.EditOn, () => { - this.disable(); - }); - this.events.listen(UiEvent.EditOff, () => { - this.enable(); - }); + this.container.appendChild(this.start_button); + this.container.appendChild(this.step_button); + this.container.appendChild(this.range); + this.cycle_delay = 1000; + + this.events.listen(UiEvent.EditOn, () => this.disable()); + this.events.listen(UiEvent.EditOff, () => this.enable()); } disable(): void { @@ -72,9 +69,8 @@ export class pausePlay implements UiComponent { private cycle(): void { const loop = (): void => { - if (this.on === false) { - return; - } + if (this.on === false) return; + this.cpu_signals.dispatch(UiCpuSignal.RequestCpuCycle, 1); setTimeout(loop, this.cycle_delay); }; @@ -89,15 +85,13 @@ export class pausePlay implements UiComponent { } start(): void { - if (!this.on) { - this.toggle(); - } + if (this.on) return; + this.toggle(); } stop(): void { - if (this.on) { - this.toggle(); - } + if (!this.on) return; + this.toggle(); } reset(): void { diff --git a/src/ui/components/registerView.ts b/src/ui/components/registerView.ts new file mode 100644 index 0000000..e2ac0cd --- /dev/null +++ b/src/ui/components/registerView.ts @@ -0,0 +1,31 @@ +import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../../events"; +import { isU3, u3, u8 } from "../../num"; +import CelledViewer from "../celledViewer"; +import UiComponent from "../uiComponent"; + +export default class RegisterView extends CelledViewer implements UiComponent { + events: UiEventHandler; + cpu_signals: UiCpuSignalHandler; + constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { + super(8, 1, element, (address: u8, value: u8) => { + if (!isU3(address)) throw new Error("unreachable"); + this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: address as u3, value }); + }); + this.events = events; + this.cpu_signals = cpu_signals; + + this.events.listen(UiEvent.EditOn, () => { + this.editor.enable(); + for (const cell of this.cells) cell.el.className = ""; + }); + this.events.listen(UiEvent.EditOff, () => { + this.editor.disable(); + for (const cell of this.cells) cell.el.className = ""; + }); + } + + init_cpu_events(c: CpuEventHandler): void { + c.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => this.set_cell_value(register_no, value)); + c.listen(CpuEvent.Reset, () => this.reset()); + } +} diff --git a/src/ui/components/reset_buttons.ts b/src/ui/components/reset_buttons.ts new file mode 100644 index 0000000..2a6ed40 --- /dev/null +++ b/src/ui/components/reset_buttons.ts @@ -0,0 +1,26 @@ +import { el } from "../../etc"; +import { UiEventHandler, UiCpuSignalHandler, UiEvent, UiCpuSignal } from "../../events"; +import UiComponent from "../uiComponent"; + +export default class ResetButtons implements UiComponent { + container: HTMLElement; + events: UiEventHandler; + cpu_signals: UiCpuSignalHandler; + constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { + this.container = element; + this.events = events; + this.cpu_signals = cpu_signals; + const reset_button = el("button").cl("nostyle").tx("R").fin(); + const trash_button = el("button").cl("nostyle").tx("T").fin(); + + reset_button.addEventListener("click", () => this.reset_clicked()); + trash_button.addEventListener("click", () => this.trash_clicked()); + this.container.append(reset_button, trash_button); + } + + reset_clicked(): void {} + + trash_clicked(): void { + this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset); + } +} diff --git a/src/ui/components/saveLoad.ts b/src/ui/components/saveLoad.ts new file mode 100644 index 0000000..0080da3 --- /dev/null +++ b/src/ui/components/saveLoad.ts @@ -0,0 +1,90 @@ +import { el } from "../../etc"; +import { UiEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events"; +import { u2, u8, m256, isU2 } from "../../num"; +import UiComponent from "../uiComponent"; + +export default class SaveLoad implements UiComponent { + container: HTMLElement; + events: UiEventHandler; + save_button: HTMLButtonElement; + binary_upload: HTMLInputElement; + cpu_signals: UiCpuSignalHandler; + constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { + this.container = element; + this.events = events; + this.cpu_signals = cpu_signals; + + this.save_button = el("button").id("save_button").tx("Save").fin(); + this.binary_upload = el("input") + .id("binary_upload") + .at("type", "file") + .at("name", "binary_upload") + .st("display", "none") + .fin(); + const label = el("label").cl("button").at("for", "binary_upload").tx("Load Binary").fin(); + + this.container.append(this.binary_upload, label, this.save_button); + + this.save_button.addEventListener("click", () => { + this.download(); + }); + this.binary_upload.addEventListener("change", (e) => { + this.upload_changed(e); + }); + } + + private download(): void { + this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump, (memory) => { + const flattened = new Uint8Array(256 * memory.length); + for (let x = 0; x < 4; x++) { + for (let y = 0; y < 256; x++) { + flattened[256 * x + y] = memory[x][y]; + } + } + const blob = new Blob([flattened], { type: "application/octet-stream" }); + const url = URL.createObjectURL(blob); + + const link = document.createElement("a"); + link.href = url; + link.download = "bin.bin"; + link.click(); + link.remove(); + }); + } + + private upload_changed(e: Event): void { + const t = e.target; + if (t === null) return; + + const file = (t as HTMLInputElement).files?.[0]; + if (file === undefined) { + console.error("No files attribute on file input"); + return; + } + + const reader = new FileReader(); + reader.addEventListener("load", (e) => { + const target = e.target; + if (target === null) return; + const data = target.result; + if (!(data instanceof ArrayBuffer)) { + console.error("Data is not arraybuffer"); + return; + } + + const view = new Uint8Array(data); + const array = [...view] as Array; + this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset); + for (const [i, v] of array.entries()) { + const address = m256(i); + const bank = Math.floor(i / 256); + if (!isU2(bank)) { + // throw new Error("Too many banks in data file"); + return; + } + this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value: v }); + } + }); + reader.readAsArrayBuffer(file); + } +} diff --git a/src/ui/editButton.ts b/src/ui/editButton.ts deleted file mode 100644 index c9ee99b..0000000 --- a/src/ui/editButton.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { el, $ } from "../etc"; -import { UiEventHandler, UiEvent } from "../events"; -import { UiComponent } from "./uiComponent"; - -export class EditButton implements UiComponent { - element: HTMLElement; - events: UiEventHandler; - constructor(element: HTMLElement, event: UiEventHandler) { - this.element = element; - this.events = event; - - const image = el("img"); - image.src = "pencil.png"; - image.style.width = "20px"; - image.style.height = "20px"; - this.element.classList.add("editor_toggle"); - this.element.addEventListener("click", () => this.edit_toggle()); - this.element.appendChild(image); - } - reset(): void { - const is_on = this.element.classList.contains("on"); - if (is_on) { - this.edit_toggle(); - } - } - - edit_toggle(): void { - const is_on = this.element.classList.contains("on"); - if (is_on) { - this.element.classList.remove("on"); - $("root").classList.remove("editor"); - this.element.classList.add("off"); - this.events.dispatch(UiEvent.EditOff); - } else { - this.events.dispatch(UiEvent.EditOn); - $("root").classList.add("editor"); - this.element.classList.add("on"); - this.element.classList.remove("off"); - } - } -} diff --git a/src/ui/editableHex.ts b/src/ui/editableHex.ts index 8e8b894..1f95a37 100644 --- a/src/ui/editableHex.ts +++ b/src/ui/editableHex.ts @@ -1,18 +1,23 @@ // This file was cobbled together and is the messiest part of this project import { at } from "../etc"; -import { u8 } from "../num"; +import { isU8, u8 } from "../num"; const HEX_CHARACTERS = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"]; -export class EditorContext { +export default class EditorContext { private list: Array; private width: number; + private height: number; private enabled: boolean = false; private current_cell_info: { left?: string; right?: string; old?: string }; - private edit_callback: (n: number, value: u8) => void; - constructor(list: Array, width: number, callback: (n: number, value: u8) => void) { + private edit_callback: (n: u8, value: u8) => void; + constructor(list: Array, width: number, height: number, callback: (n: u8, value: u8) => void) { + if (!isU8(width * height - 1)) { + throw new RangeError("Grid is too big for editor. Maximum area is 256"); + } this.list = list; + this.height = height; this.width = width; this.edit_callback = callback; this.current_cell_info = {}; @@ -35,7 +40,7 @@ export class EditorContext { this.current_cell_info.right = undefined; cell.classList.add("caret_selected"); - // Reset cursor position (I know there's an API for this, but this is a simpler, more robust solution) + // Reset cursor position (there's an API for this, but this is a simpler, more robust solution) cell.textContent = cell.textContent ?? "00"; }); @@ -49,7 +54,7 @@ export class EditorContext { const text = `${left}${right}`; cell.textContent = text; const val = Number.parseInt(text, 16) as u8; - this.edit_callback(i, val); + this.edit_callback(i as u8, val); cell.classList.add("recent_edit"); } }); diff --git a/src/ui/memoryView.ts b/src/ui/memoryView.ts deleted file mode 100644 index 6609709..0000000 --- a/src/ui/memoryView.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../events"; -import { ParamType } from "../instructionSet"; -import { u8 } from "../num.js"; -import { UiComponent } from "./uiComponent"; -import { CelledViewer } from "./celledViewer"; -import { EditorContext } from "./editableHex"; - -export class MemoryView extends CelledViewer implements UiComponent { - program_counter: u8 = 0; - last_accessed_cell: u8 | null = null; - events: UiEventHandler; - cpu_signals: UiCpuSignalHandler; - constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { - super(16, 16, element); - this.program_counter = 0; - this.events = events; - this.cpu_signals = cpu_signals; - - const list = this.cells.map((c) => c.el); - const editor = new EditorContext(list, this.width, (i, value) => { - this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address: i as u8, bank: 0, value }); - }); - this.events.listen(UiEvent.EditOn, () => { - editor.enable(); - for (const cell of this.cells) { - cell.el.className = ""; - } - }); - this.events.listen(UiEvent.EditOff, () => { - editor.disable(); - for (const cell of this.cells) { - cell.el.className = ""; - } - }); - } - - set_program_counter(position: u8): void { - this.remove_cell_class(this.program_counter, "program_counter"); - this.add_cell_class(position, "program_counter"); - this.program_counter = position; - } - - reset(): void { - super.reset(); - this.last_accessed_cell = null; - this.set_program_counter(0); - } - - init_cpu_events(c: CpuEventHandler): void { - c.listen(CpuEvent.MemoryAccessed, ({ address, bank, value }) => { - if (bank !== 0) return; - if (this.last_accessed_cell !== address) { - if (this.last_accessed_cell !== null) { - this.remove_cell_class(this.last_accessed_cell, "last_access"); - } - this.add_cell_class(address, "last_access"); - this.last_accessed_cell = address; - } - }); - c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => { - if (bank !== 0) { - return; - } - this.set_cell_value(address, value); - }); - c.listen(CpuEvent.ProgramCounterChanged, ({ counter }) => { - this.set_program_counter(counter); - }); - c.listen(CpuEvent.ParameterParsed, ({ param, code, pos }) => { - this.add_cell_class(pos, "instruction_argument"); - const t = param.type; - this.remove_cell_class(pos, "constant", "register", "memory", "instruction", "invalid"); - let name: string = ""; - if (t === ParamType.Const) { - name = "constant"; - } else if (t === ParamType.Memory) { - name = "memory"; - } else if (t === ParamType.Register) { - name = "register"; - } else { - throw new Error("unreachable"); - } - this.add_cell_class(pos, name); - }); - c.listen(CpuEvent.InstructionParsed, ({ instr, code, pos }) => { - this.remove_all_cell_class("instruction_argument"); - this.remove_all_cell_class("current_instruction"); - this.remove_cell_class(pos, "constant", "register", "memory", "invalid"); - this.add_cell_class(pos, "current_instruction"); - this.add_cell_class(pos, "instruction"); - }); - c.listen(CpuEvent.InvalidParsed, ({ code, pos }) => { - this.remove_cell_class(pos, "constant", "register", "memory", "instruction"); - this.add_cell_class(pos, "invalid"); - }); - } -} diff --git a/src/ui/registerView.ts b/src/ui/registerView.ts deleted file mode 100644 index b80251e..0000000 --- a/src/ui/registerView.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { CpuEvent, CpuEventHandler, UiCpuSignal, UiCpuSignalHandler, UiEvent, UiEventHandler } from "../events"; -import { u3 } from "../num"; -import { CelledViewer } from "./celledViewer"; -import { EditorContext } from "./editableHex"; -import { UiComponent } from "./uiComponent"; - -export class RegisterView extends CelledViewer implements UiComponent { - events: UiEventHandler; - cpu_signals: UiCpuSignalHandler; - constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { - super(8, 1, element); - this.events = events; - this.cpu_signals = cpu_signals; - - const list = this.cells.map((c) => c.el); - const editor = new EditorContext(list, this.width, (i, value) => { - this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: i as u3, value }); - }); - this.events.listen(UiEvent.EditOn, () => { - editor.enable(); - for (const cell of this.cells) { - cell.el.className = ""; - } - }); - this.events.listen(UiEvent.EditOff, () => { - editor.disable(); - for (const cell of this.cells) { - cell.el.className = ""; - } - }); - } - - init_cpu_events(c: CpuEventHandler): void { - c.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => { - this.set_cell_value(register_no, value); - }); - } -} diff --git a/src/ui/saveLoad.ts b/src/ui/saveLoad.ts deleted file mode 100644 index d4baeca..0000000 --- a/src/ui/saveLoad.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { el } from "../etc"; -import { UiEventHandler, CpuEventHandler, CpuEvent, UiCpuSignalHandler, UiCpuSignal } from "../events"; -import { u2, u8, m256 } from "../num"; -import { UiComponent } from "./uiComponent"; - -export class SaveLoad implements UiComponent { - element: HTMLElement; - events: UiEventHandler; - save_button: HTMLButtonElement; - binary_upload: HTMLInputElement; - cpu_signals: UiCpuSignalHandler; - - constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { - this.element = element; - this.events = events; - this.cpu_signals = cpu_signals; - this.save_button = el("button", "save_button"); - this.binary_upload = el("input", "binary_upload"); - this.binary_upload.type = "file"; - this.binary_upload.name = "binary_upload"; - this.binary_upload.style.display = "none"; - const label = el("label"); - this.save_button.textContent = "Save"; - label.textContent = "Load Binary"; - label.classList.add("button"); - label.setAttribute("for", "binary_upload"); - - this.element.appendChild(this.binary_upload); - this.element.appendChild(label); - this.element.appendChild(this.save_button); - this.save_button.addEventListener("click", () => { - this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump); - }); - this.binary_upload.addEventListener("change", (e) => { - this.upload_changed(e); - }); - } - - private upload_changed(e: Event): void { - const t = e.target; - if (t === null) { - return; - } - - const file: File | undefined = (t as HTMLInputElement).files?.[0]; - if (file === undefined) { - console.log("No files attribute on file input"); - return; - } - const reader = new FileReader(); - console.log(file); - reader.addEventListener("load", (e) => { - if (e.target !== null) { - const data = e.target.result; - if (data instanceof ArrayBuffer) { - const view = new Uint8Array(data); - const array = [...view] as Array; - this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset); - for (const [i, v] of array.entries()) { - const address = m256(i); - const bank = Math.floor(i / 256) as u2; - this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryChange, { address, bank, value: v }); - } - } else { - console.log("not array"); - } - } - }); - reader.readAsArrayBuffer(file); - } - - // eslint-disable-next-line class-methods-use-this - init_cpu_events(e: CpuEventHandler): void { - e.listen(CpuEvent.MemoryDumped, ({ memory }) => { - const flattened = new Uint8Array(256 * memory.length); - for (let x = 0; x < 4; x++) { - for (let y = 0; y < 256; x++) { - flattened[256 * x + y] = memory[x][y]; - } - } - const blob = new Blob([flattened], { type: "application/octet-stream" }); - const url = URL.createObjectURL(blob); - - const link = document.createElement("a"); - link.href = url; - link.download = "bin.bin"; - link.click(); - link.remove(); - }); - } -} diff --git a/src/ui/uiComponent.ts b/src/ui/uiComponent.ts index add22e7..78d87ef 100644 --- a/src/ui/uiComponent.ts +++ b/src/ui/uiComponent.ts @@ -12,8 +12,8 @@ import { CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../events"; // - CpuEventHandler: listen to events created as a result of CPU actions // - UiCpuEventSignaler: dispatch signals to request actions from the CPU -export interface UiComponent { - element: HTMLElement; +export default interface UiComponent { + container: HTMLElement; /** Allows listening and emitting UiEvents*/ events: UiEventHandler; /** Creating signals for the cpu to process */ diff --git a/src/ui/windowBox.ts b/src/ui/windowBox.ts index 5e0ce6e..5b2002c 100644 --- a/src/ui/windowBox.ts +++ b/src/ui/windowBox.ts @@ -1,52 +1,78 @@ import { el } from "../etc"; -export abstract class WindowBox { - element: HTMLElement; + +interface WindowBoxOptions { + collapsed?: boolean; + fit_content?: boolean; +} + +const BORDER_STROKE = 5; // px + +export default abstract class WindowBox { + container: HTMLElement; title_bar: HTMLElement; readonly title: string; - private resize: HTMLElement; private collapse_button: HTMLButtonElement; - private collapsed: boolean = false; - private resize_func: (e: MouseEvent) => void; + private collapsed = false; + private fit_content = false; + private resize?: HTMLElement; + private resize_func?: (e: MouseEvent) => void; - constructor(element: HTMLElement, title: string, options?: { collapsed?: boolean }) { - this.element = element; + constructor(element: HTMLElement, title: string, options?: WindowBoxOptions) { + this.container = element; this.title = title; - this.element.classList.add("window"); - this.title_bar = el("div", undefined, "window_title"); - this.element.appendChild(this.title_bar); - const title_bar_text_box = el("div", "text"); + + this.container.classList.add("window"); + this.title_bar = el("div").cl("window_title").fin(); + if (this.container.firstChild !== null) { + this.container.firstChild.before(this.title_bar); + } else { + this.container.appendChild(this.title_bar); + } + const title_bar_text_box = el("div").id("text").fin(); title_bar_text_box.textContent = title; + + this.collapse_button = el("button").id("collapse_button").cl("nostyle").fin(); + this.collapse_button.addEventListener("click", () => this.toggle_collapse()); + this.title_bar.appendChild(title_bar_text_box); - this.resize = el("div", "resize"); - this.element.appendChild(this.resize); - this.resize_func = this.resize_move.bind(this); - this.collapse_button = el("button", "collapse_button", "nostyle"); - this.collapse_button.addEventListener("click", () => { - this.toggle_collapse(); - }); this.title_bar.appendChild(this.collapse_button); - this.resize.addEventListener("mousedown", (e) => { - window.addEventListener("mousemove", this.resize_func); - }); - window.addEventListener("mouseup", () => { - this.remove_resize_listeners(); - }); - window.addEventListener("mouseleave", () => { - this.remove_resize_listeners(); - }); - if (options?.collapsed) { - this.collapse(); + + if (options?.collapsed) this.collapse(); + + if (options?.fit_content) { + this.fit_content = true; + } else { + this.resize = el("div").id("resize").fin(); + this.container.appendChild(this.resize); + this.resize_func = this.resize_move.bind(this); + this.resize.addEventListener("mousedown", (e) => { + if (this.resize_func) window.addEventListener("mousemove", this.resize_func); + }); + window.addEventListener("mouseup", () => this.remove_resize_listeners()); + window.addEventListener("mouseleave", () => this.remove_resize_listeners()); } } collapse(): void { - this.element.classList.add("collapsed"); + this.container.classList.add("collapsed"); this.remove_resize_listeners(); - this.resize.style.visibility = "hidden"; - this.element.style.height = `${this.title_bar.offsetHeight + 4}px`; + if (this.resize) this.resize.style.visibility = "hidden"; + this.set_height(this.title_bar.offsetHeight - BORDER_STROKE); this.collapsed = true; } + correct_height_value(height: number): number { + if (this.fit_content) { + let height_sum = 0; + for (const c of this.container.children) { + height_sum += (c).offsetHeight; + } + return height_sum; + } + + return height; + } + toggle_collapse(): void { if (this.collapsed) { this.uncollapse(); @@ -55,15 +81,21 @@ export abstract class WindowBox { } } + set_height(height: number): void { + this.container.style.height = `${height + 2 * BORDER_STROKE}px`; + } + uncollapse(): void { - this.element.classList.remove("collapsed"); - this.resize.style.visibility = "unset"; - this.element.style.height = `${this.title_bar.offsetHeight + 10 + 200}px`; + this.container.classList.remove("collapsed"); + if (this.resize) this.resize.style.visibility = "unset"; + const new_height = this.correct_height_value(this.title_bar.offsetHeight + 200); + this.set_height(new_height); + this.collapsed = false; } remove_resize_listeners(): void { - window.removeEventListener("mousemove", this.resize_func); + if (this.resize_func) window.removeEventListener("mousemove", this.resize_func); } resize_move(e: MouseEvent): void { @@ -72,11 +104,11 @@ export abstract class WindowBox { this.remove_resize_listeners(); return; } - const distance_to_title = e.clientY - this.element.offsetTop - this.title_bar.offsetHeight + window.scrollY + 5; + const distance_to_title = e.clientY - this.container.offsetTop - this.title_bar.offsetHeight + window.scrollY + 5; if (distance_to_title <= 5) { this.collapse(); return; } - this.element.style.height = `${e.clientY - this.element.offsetTop + window.scrollY + 8}px`; + this.set_height(e.clientY - this.container.offsetTop + window.scrollY); } } diff --git a/src/ui/windows/bank_visualizer.ts b/src/ui/windows/bank_visualizer.ts new file mode 100644 index 0000000..54aeb03 --- /dev/null +++ b/src/ui/windows/bank_visualizer.ts @@ -0,0 +1,35 @@ +import { CpuEvent, CpuEventHandler, UiEventHandler } from "../../events"; +import UiComponent from "../uiComponent"; +import WindowBox from "../windowBox"; +import { DEFAULT_VRAM_BANK } from "../../constants"; + +export default class BankVisualizer extends WindowBox implements UiComponent { + events: UiEventHandler; + cpu_banks: Array; + vram_banks: Array; + constructor(element: HTMLElement, events: UiEventHandler) { + super(element, "Bank Status", { fit_content: true }); + this.events = events; + this.cpu_banks = [...element.querySelectorAll("#cpu_bank>polyline")] as Array; + this.vram_banks = [...element.querySelectorAll("#vram_bank>polyline")] as Array; + } + + init_cpu_events(c: CpuEventHandler): void { + c.listen(CpuEvent.SetVramBank, ({ bank }) => { + for (const bank_path of this.vram_banks) bank_path.setAttribute("stroke", "gray"); + this.vram_banks[bank].setAttribute("stroke", "yellow"); + }); + c.listen(CpuEvent.SwitchBank, ({ bank }) => { + for (const bank_path of this.cpu_banks) bank_path.setAttribute("stroke", "gray"); + this.cpu_banks[bank].setAttribute("stroke", "yellow"); + }); + } + + reset(): void { + for (const bank_path of this.vram_banks) bank_path.setAttribute("stroke", "gray"); + for (const bank_path of this.cpu_banks) bank_path.setAttribute("stroke", "gray"); + this.vram_banks[DEFAULT_VRAM_BANK].setAttribute("stroke", "yellow"); + + this.cpu_banks[0].setAttribute("stroke", "yellow"); + } +} diff --git a/src/ui/windows/instructionExplainer.ts b/src/ui/windows/instructionExplainer.ts index bd342aa..7fdc2f4 100644 --- a/src/ui/windows/instructionExplainer.ts +++ b/src/ui/windows/instructionExplainer.ts @@ -2,10 +2,10 @@ import { el, format_hex } from "../../etc"; import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events"; import { Instruction, ParamType, ParameterType } from "../../instructionSet"; import { u8 } from "../../num"; -import { WindowBox } from "../windowBox"; -import { UiComponent } from "../uiComponent"; +import WindowBox from "../windowBox"; +import UiComponent from "../uiComponent"; -export class InstructionExplainer extends WindowBox implements UiComponent { +export default class InstructionExplainer extends WindowBox implements UiComponent { events: UiEventHandler; cpu_signals: UiCpuSignalHandler; constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) { @@ -19,16 +19,17 @@ export class InstructionExplainer extends WindowBox implements UiComponent { } private add_box(box_icon_text: string, name: string, css_class: string): void { - const instr_box = el("div", "expl_box"); - const instr_icon = el("span", "expl_icon"); - instr_icon.classList.add(css_class); - instr_icon.setAttribute("title", css_class.toUpperCase()); - instr_icon.textContent = box_icon_text; - const instr_box_text = el("span", "expl_text"); - instr_box_text.textContent = name; + const instr_box = el("div").id("expl_box").fin(); + const instr_icon = el("span") + .id("expl_icon") + .cl(css_class) + .at("title", css_class.toUpperCase()) + .tx(box_icon_text) + .fin(); + const instr_box_text = el("span").id("expl_text").tx(name).fin(); instr_box.appendChild(instr_icon); instr_box.appendChild(instr_box_text); - this.element.appendChild(instr_box); + this.container.appendChild(instr_box); } add_parameter(param: ParameterType, pos: u8, byte: u8): void { @@ -64,6 +65,6 @@ export class InstructionExplainer extends WindowBox implements UiComponent { } reset(): void { - this.element.querySelectorAll("#expl_box").forEach((e) => e.remove()); + this.container.querySelectorAll("#expl_box").forEach((e) => e.remove()); } } diff --git a/src/ui/windows/printout.ts b/src/ui/windows/printout.ts index 2975cd8..4fafc94 100644 --- a/src/ui/windows/printout.ts +++ b/src/ui/windows/printout.ts @@ -1,9 +1,9 @@ import { el } from "../../etc"; import { CpuEvent, CpuEventHandler, UiCpuSignalHandler, UiEventHandler } from "../../events"; -import { WindowBox } from "../windowBox"; -import { UiComponent } from "../uiComponent"; +import WindowBox from "../windowBox"; +import UiComponent from "../uiComponent"; -export class Printout extends WindowBox implements UiComponent { +export default class Printout extends WindowBox implements UiComponent { events: UiEventHandler; text_box: HTMLElement; cpu_signals: UiCpuSignalHandler; @@ -11,8 +11,8 @@ export class Printout extends WindowBox implements UiComponent { super(element, "Printout"); this.cpu_signals = cpu_signals; this.events = events; - this.text_box = el("div", "printout_text"); - this.element.appendChild(this.text_box); + this.text_box = el("div").id("printout_text").fin(); + this.container.appendChild(this.text_box); } init_cpu_events(c: CpuEventHandler): void { diff --git a/src/ui/windows/screen.ts b/src/ui/windows/screen.ts index f63da2e..8f29311 100644 --- a/src/ui/windows/screen.ts +++ b/src/ui/windows/screen.ts @@ -1,35 +1,41 @@ +import { DEFAULT_VRAM_BANK } from "../../constants"; import { el } from "../../etc"; -import { UiEventHandler, CpuEventHandler, CpuEvent } from "../../events"; -import { u4, u8 } from "../../num"; -import { UiComponent } from "../uiComponent"; -import { WindowBox } from "../windowBox"; -export class Screen extends WindowBox implements UiComponent { +import { UiEventHandler, CpuEventHandler, CpuEvent, UiCpuSignalHandler, UiCpuSignal } from "../../events"; +import { u2, u4, u8 } from "../../num"; +import UiComponent from "../uiComponent"; +import WindowBox from "../windowBox"; + +const CANVAS_SIZE = 512; +const WIDTH = 16; + +export default class Screen extends WindowBox implements UiComponent { events: UiEventHandler; screen: HTMLCanvasElement; + cpu_signals: UiCpuSignalHandler; ctx: CanvasRenderingContext2D; - scale: [number, number]; - constructor(element: HTMLElement, event: UiEventHandler) { - super(element, "TV", { collapsed: true }); - this.screen = el("canvas", "screen"); + scale: number; + current_vram_bank: u2 = DEFAULT_VRAM_BANK; + constructor(element: HTMLElement, event: UiEventHandler, cpu_signals: UiCpuSignalHandler) { + super(element, "TV", { collapsed: true, fit_content: true }); + this.cpu_signals = cpu_signals; this.events = event; - const canvas_size = [512, 512]; - const data_size = [16, 16]; - this.scale = [canvas_size[0] / data_size[0], canvas_size[1] / data_size[1]]; - [this.screen.width, this.screen.height] = canvas_size; + + this.scale = CANVAS_SIZE / WIDTH; + this.screen = el("canvas").id("screen").fin(); + this.screen.width = CANVAS_SIZE; + this.screen.height = CANVAS_SIZE; const ctx = this.screen.getContext("2d"); if (ctx === null) { throw new Error("could not load screen"); } this.ctx = ctx; - this.element.appendChild(this.screen); + this.container.appendChild(this.screen); this.test_pattern(); } private test_pattern(): void { - for (let x = 0; x < 16; x++) { - for (let y = 0; y < 16; y++) { - this.setPixel(x as u4, y as u4, (x + 16 * y) as u8); - } + for (let x = 0; x < 256; x++) { + this.setPixel(x as u8, x as u8); } } @@ -43,14 +49,24 @@ export class Screen extends WindowBox implements UiComponent { init_cpu_events(c: CpuEventHandler): void { c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => { if (bank !== 1) return; - const x = (address % 16) as u4; - const y = Math.floor(address / 16) as u4; - this.setPixel(x, y, value); + + this.setPixel(address, value); + }); + c.listen(CpuEvent.SetVramBank, ({ bank }) => { + this.current_vram_bank = bank; + this.cpu_signals.dispatch(UiCpuSignal.RequestMemoryDump, (memory) => { + const vram = memory[this.current_vram_bank]; + for (const [i, pixel] of vram.entries()) { + this.setPixel(i as u8, pixel as u8); + } + }); }); } - setPixel(x: u4, y: u4, value: u8): void { - const point: [number, number] = [x * this.scale[0], y * this.scale[1]]; + setPixel(address: u8, value: u8): void { + const x = (address % 16) as u4; + const y = Math.floor(address / 16) as u4; + const point: [number, number] = [x * this.scale, y * this.scale]; const RED_SCALE = 255 / 2 ** 3; const GREEN_SCALE = 255 / 2 ** 3; @@ -61,6 +77,6 @@ export class Screen extends WindowBox implements UiComponent { const color = `rgb(${red},${green},${blue})`; this.ctx.fillStyle = color; - this.ctx.fillRect(...point, ...this.scale); + this.ctx.fillRect(...point, this.scale, this.scale); } } diff --git a/src/util/elementMaker.ts b/src/util/elementMaker.ts new file mode 100644 index 0000000..d43b458 --- /dev/null +++ b/src/util/elementMaker.ts @@ -0,0 +1,47 @@ +class ElementInProgress { + private element: E; + constructor(el: E) { + this.element = el; + } + + /** Set attribute */ + at(name: string, value: string): ElementInProgress { + this.element.setAttribute(name, value); + return this; + } + + /** Set id */ + id(id: string): ElementInProgress { + this.element.id = id; + return this; + } + + /** Add class */ + cl(class_name: string): ElementInProgress { + this.element.classList.add(class_name); + return this; + } + + /** Set textContent */ + tx(text_contents: string): ElementInProgress { + this.element.textContent = text_contents; + return this; + } + + /** Set style */ + st(name: string, value: string): ElementInProgress { + this.element.style.setProperty(name, value); + return this; + } + + fin(): E { + return this.element; + } +} + +export default function el( + name: E +): ElementInProgress { + const element = document.createElement(name); + return new ElementInProgress(element); +} diff --git a/svg.html b/svg.html deleted file mode 100644 index e826c0c..0000000 --- a/svg.html +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - Document - - - -
-
CPU
- - - - - - -
-
Main
-
Bank 1
-
Bank 2
-
VRAM
-
-
- - diff --git a/test.bin b/test.bin deleted file mode 100644 index 42736ce..0000000 Binary files a/test.bin and /dev/null differ diff --git a/todo.md b/todo.md index d859ee2..46f90c0 100644 --- a/todo.md +++ b/todo.md @@ -1,3 +1,5 @@ +Verify mod256 behavior on negatives + Edit Mode - Select where program counter is @@ -8,18 +10,26 @@ Speed control slider behavior Speed control slider styling Overclock Box +Error log: error in instruction when number out of range (fix new Error("todo")) -UI for showing which Memory bank is selected -VRAM select instruction +- Allow ignoring errors +- Highlight errored instruction parameters and give description + +Hard reset button (resets everything) + +Soft reset button. Resets program counter and registers. + +Fixed size windows + +- TV +- Bank Status + +Limit size to printout text buffer Improve instruction explainer. Clearly show what is an instruction and what is a parameter -Verify mod256 behavior on negatives - -UI showing CPU flag(s) (Carry) - -Error log +Ui showing CPU flag(s) (Carry) Responsive layout @@ -28,5 +38,3 @@ standardize names of all things Documentation with standard names Example Programs - -Ui for togging your mother.