Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19c760a204 | ||
|
|
3a81ef54ca | ||
|
|
6cfdcb1b0d | ||
|
|
30ef1fcaa5 | ||
|
|
84b8121105 | ||
|
|
218cfd256c | ||
|
|
1757f2733c | ||
|
|
9411acbc73 | ||
|
|
cb4102cdcb | ||
|
|
1926c9bbbc | ||
|
|
06145c02b1 | ||
|
|
d65898ffed | ||
|
|
b5e6066f8c | ||
|
|
0981ca8796 | ||
|
|
ffeaa2b976 | ||
|
|
167765f932 | ||
|
|
bcdbb61ed4 | ||
|
|
ea7571c2b9 | ||
|
|
880b726bcb | ||
|
|
902da2800b | ||
|
|
2bd9c4e839 | ||
|
|
b3d36478ca | ||
|
|
b07c016fea | ||
|
|
0b5625581e | ||
|
|
e4b473785d | ||
|
|
cb8d93a1ef | ||
|
|
8636d0f820 | ||
|
|
f79644bd4c | ||
|
|
917e31ee17 | ||
|
|
aba59e9386 | ||
|
|
1c7c4eb62f | ||
|
|
fc41968131 | ||
|
|
04177de95b | ||
|
|
c9c07aeabd | ||
|
|
0cc759e0b1 | ||
|
|
852255ea03 | ||
|
|
88f459583e | ||
|
|
fd8a0690c9 | ||
|
|
6e2744a299 | ||
|
|
15e3b1ac12 | ||
|
|
5a5ed6d767 | ||
|
|
74ea0c23ca | ||
|
|
6d0a287ed9 | ||
|
|
83ff4e97c3 | ||
|
|
7b28dc4d59 | ||
|
|
02d3ef599a | ||
|
|
c118f04187 | ||
|
|
2bd3869547 | ||
|
|
69379ba59b | ||
|
|
5ed05295ba | ||
|
|
4ee4a0f988 | ||
|
|
d6b8a21219 | ||
|
|
2e2d275d6c | ||
|
|
33bba35be7 | ||
|
|
c7660fb49c | ||
|
|
6d527c42b0 | ||
|
|
440da4a5ff | ||
|
|
e66fd50383 | ||
|
|
f69f9decca | ||
|
|
e70138b63c | ||
|
|
becec91e98 | ||
|
|
03dc1908a5 | ||
|
|
d2298d84a0 | ||
|
|
0b56b56c06 | ||
|
|
6d46b7b86f | ||
|
|
b829ad8386 | ||
|
|
9bc65c36a6 | ||
|
|
a562f33d53 | ||
|
|
fe30cd6ed8 | ||
|
|
9e84895141 | ||
|
|
497df06f3f | ||
|
|
650cda31fd | ||
|
|
dec3791f4c | ||
|
|
e46a9e12d0 | ||
|
|
7f64fa9ddd | ||
|
|
73d20c1d3b | ||
|
|
fd44d76262 | ||
|
|
309ba47483 | ||
|
|
4084dc3949 | ||
|
|
42bacc3283 | ||
|
|
46bd97247d | ||
|
|
3bf72cdefc | ||
|
|
de23616589 | ||
|
|
39c2e57ad3 | ||
|
|
d19361ac85 | ||
|
|
1f5159febe | ||
|
|
3a2ae96960 | ||
|
|
ad1f2ac2a8 | ||
|
|
b972e7f5ad | ||
|
|
503fe99b0a | ||
|
|
953cf005c0 | ||
|
|
d177acccee | ||
|
|
4451369c66 | ||
|
|
6b09c1b576 | ||
|
|
71eb514bd0 | ||
|
|
cd552500fc | ||
|
|
ed1773c103 | ||
|
|
b7c119a071 | ||
|
|
452241ca92 | ||
|
|
1880399849 | ||
|
|
d4ee0309d9 | ||
|
|
a351f5758a | ||
|
|
3597d43788 | ||
|
|
f381783bd5 | ||
|
|
6969a94fe4 | ||
|
|
2377472eca | ||
|
|
0edabed595 | ||
|
|
7d98375ce4 | ||
|
|
862e83d7be | ||
|
|
7cf9b68e2c | ||
|
|
97a0df260b | ||
|
|
c66dc82286 | ||
|
|
6c29864847 | ||
|
|
d82da195f3 | ||
|
|
abfe391d91 | ||
|
|
c22e684904 | ||
|
|
6406fd79c1 | ||
|
|
0271fdf49d | ||
|
|
805b41b80f | ||
|
|
c9bacc80d7 | ||
|
|
54ee698ba6 | ||
|
|
295e3c7f0c | ||
|
|
d219da480c | ||
|
|
02fa38c334 | ||
|
|
4d05905736 | ||
|
|
81a3c80bc4 | ||
|
|
f6fabebf8d | ||
|
|
b735e7f1c4 | ||
|
|
839271096e | ||
|
|
6a087e9896 | ||
|
|
dda3f5f545 | ||
|
|
04041d030f | ||
|
|
095ceda639 | ||
|
|
0aa9fc5bb8 | ||
|
|
e092ca70e1 | ||
|
|
0592837620 | ||
|
|
fc03a1698b | ||
|
|
c9ef4a8eb9 | ||
|
|
846573bb01 | ||
|
|
0f70055d76 | ||
|
|
3f2ed42e73 | ||
|
|
2a3c2d9520 | ||
|
|
5e95475d6b | ||
|
|
57a65e0858 | ||
|
|
bfa9dd5060 | ||
|
|
04f79aa7b4 | ||
|
|
86ccd709bf | ||
|
|
c257a7247e | ||
|
|
0a49a5dde1 | ||
|
|
c6e3cc007c | ||
|
|
713baeb64d | ||
|
|
efba2c9040 | ||
|
|
67ef455515 | ||
|
|
975d52fab9 | ||
|
|
6b37187b4d | ||
|
|
84ab6a49c4 | ||
|
|
4ebc75aae0 | ||
|
|
54335ef54a | ||
|
|
43d8db77f9 | ||
|
|
5a4a46f130 | ||
|
|
b0672aac09 | ||
|
|
fcf378b172 | ||
|
|
25551a4b98 | ||
|
|
67b3faef01 | ||
|
|
8b545a3fe4 | ||
|
|
4f4ce0146d | ||
|
|
e7b347a2ef | ||
|
|
7908b7cf62 | ||
|
|
392f8e9c98 | ||
|
|
b5cd4f1492 | ||
|
|
4edfc12cbf | ||
|
|
19c1ee56d3 | ||
|
|
a4f8b07a7f | ||
|
|
8d4d5bd54b | ||
|
|
d5710afcb3 | ||
|
|
8bd8195e30 | ||
|
|
ffe066dfef | ||
|
|
11e90591f9 | ||
|
|
0b84d17b18 | ||
|
|
376f0d9972 | ||
|
|
478535d236 | ||
|
|
3f12d2925a | ||
|
|
8e65ee8832 | ||
|
|
a7f0c3868c | ||
|
|
96486c414b | ||
|
|
b560d85553 | ||
|
|
7796b4101a | ||
|
|
a982484392 | ||
|
|
e15c6b9f3e | ||
|
|
807183bba6 | ||
|
|
dc19628014 | ||
|
|
ca68a5c59a | ||
|
|
22e8a2f8e7 | ||
|
|
171ba446bd | ||
|
|
89e0b87eac | ||
|
|
2c9e711cae | ||
|
|
b0f1c38fd2 | ||
|
|
b393008b54 | ||
|
|
65f41a151e | ||
|
|
ddc4e75583 | ||
|
|
869ce3aa35 | ||
|
|
16ea621a5f | ||
|
|
5204374494 | ||
|
|
9f1a71485c | ||
|
|
49e0480f78 | ||
|
|
114924f3fa | ||
|
|
aeba140c75 | ||
|
|
aea7680f76 | ||
|
|
fb4a4e6127 | ||
|
|
cc82f605fe | ||
|
|
a055948937 | ||
|
|
8246440795 | ||
|
|
073aa6d439 | ||
|
|
6bbe5a82a8 | ||
|
|
1e6dfec4ae | ||
|
|
5285b9ac45 | ||
|
|
50d1834871 | ||
|
|
b1f785879b | ||
|
|
28fcffe46c | ||
|
|
14b99dca76 | ||
|
|
1b13127f0b | ||
|
|
a37ea14d10 | ||
|
|
f8a5d3a91e | ||
|
|
8d7137417b | ||
|
|
e47296c1b9 | ||
|
|
96b26b2e4a | ||
|
|
8ac88bc6bf | ||
|
|
7eead44048 | ||
|
|
49a0904857 | ||
|
|
a4c62792df | ||
|
|
1c345bef48 | ||
|
|
415100e8b8 | ||
|
|
627410a537 | ||
|
|
cad9c4164c | ||
|
|
a87b6c3f2e | ||
|
|
a2a855d747 | ||
|
|
fb91b01217 | ||
|
|
bd661fdb07 | ||
|
|
f983b3cac1 | ||
|
|
0ba12301a6 | ||
|
|
97e084aa50 | ||
|
|
e0fb3b4d19 | ||
|
|
a1050d1a11 | ||
|
|
7f9e3b1225 | ||
|
|
48f15cb543 | ||
|
|
6e35bcd767 | ||
|
|
80e474c6ad | ||
|
|
4dee0c5d4b | ||
|
|
a22ad76bd8 | ||
|
|
4f5154fd21 | ||
|
|
cc6def57e4 | ||
|
|
c4750eaf77 | ||
|
|
8db2244f5d | ||
|
|
5d8e4a0af4 | ||
|
|
eecf52e949 | ||
|
|
d8c31d7ad0 | ||
|
|
d8cf3af555 | ||
|
|
d3976c05f1 | ||
|
|
94c3d11239 | ||
|
|
00f6d3a88c | ||
|
|
f61d8c91a3 | ||
|
|
ac659cb412 | ||
|
|
34475f24c3 | ||
|
|
8116e035b4 | ||
|
|
56b37a73ac | ||
|
|
fc8043a25a | ||
|
|
f88c0e7480 | ||
|
|
d948b31304 | ||
|
|
9a4d5dbe26 | ||
|
|
8e7093f03b | ||
|
|
c4eef2f470 | ||
|
|
5fcb084089 | ||
|
|
e89c507d06 | ||
|
|
eb3de3ebd5 | ||
|
|
ffa4d921bb | ||
|
|
a060c64a08 | ||
|
|
0d5a526e86 | ||
|
|
3ec08948ec | ||
|
|
4d97729010 | ||
|
|
5f1651ffbc | ||
|
|
ab0082b73a | ||
|
|
aa55d1bde6 | ||
|
|
e0324e32a8 | ||
|
|
3b260a2193 | ||
|
|
622acaaad7 | ||
|
|
0d7e18d236 | ||
|
|
83c09c34db | ||
|
|
03e3105c55 | ||
|
|
c5d015c908 | ||
|
|
e1e8f28d97 | ||
|
|
f6a61acc86 | ||
|
|
6316eee672 | ||
|
|
9259ec258f | ||
|
|
c741c13409 | ||
|
|
f2ff3a6a7a | ||
|
|
dbef850039 | ||
|
|
590a8219e3 | ||
|
|
d22f7c9edd | ||
|
|
c2bf87ee34 | ||
|
|
0bc5ca8f80 | ||
|
|
5b785d29a1 | ||
|
|
f38a49b4d5 | ||
|
|
26c974f9b6 | ||
|
|
333b0e28ac | ||
|
|
0bc71156bf | ||
|
|
dece3cc41c | ||
|
|
8752c4fa8e | ||
|
|
53d0297898 | ||
|
|
af6f03600f | ||
|
|
1f8e6f0952 | ||
|
|
f087bd121e | ||
|
|
d669c6e9d6 | ||
|
|
c5f9eacbb1 | ||
|
|
6fcc496547 | ||
|
|
5dee66353a | ||
|
|
2a602ef231 | ||
|
|
e3e3c347ea | ||
|
|
2e8b793ba0 | ||
|
|
03fcf60aaf | ||
|
|
ed15a8e170 | ||
|
|
969db2bcf9 | ||
|
|
8e378c07e9 | ||
|
|
1a22c44f2b | ||
|
|
cf9949840b | ||
|
|
3a90f7e7c0 | ||
|
|
035025dba3 | ||
|
|
52ed65d7bd | ||
|
|
150499b7e5 | ||
|
|
193d305dcd | ||
|
|
2236e230fc | ||
|
|
cd6f370c28 | ||
|
|
2659de2291 | ||
|
|
34df17e36d | ||
|
|
44388e5e6c | ||
|
|
785b932d75 | ||
|
|
e58f73dc82 | ||
|
|
7bba07e24e | ||
|
|
39eea8ed88 | ||
|
|
20c1f1a5f7 | ||
|
|
e87f70d6d6 | ||
|
|
51c580e7a4 | ||
|
|
23328a394b | ||
|
|
399621eeb5 | ||
|
|
66675ef649 | ||
|
|
c6df208a3e | ||
|
|
8201925fe8 | ||
|
|
d74e43a42a | ||
|
|
25b69e5ed0 | ||
|
|
37c722d556 | ||
|
|
2b597a82a1 | ||
|
|
318abe641b | ||
|
|
eca9c721d4 | ||
|
|
0b8120808c | ||
|
|
c6e4788344 | ||
|
|
c548977a3e | ||
|
|
04d196a32d | ||
|
|
494fe3a95d | ||
|
|
8f8813a19a | ||
|
|
df8a57d14b | ||
|
|
868fc7ec29 | ||
|
|
2febe19222 | ||
|
|
693d09cbad | ||
|
|
ab90dcd0dc | ||
|
|
1f45d5344c | ||
|
|
a9eea3fd46 | ||
|
|
aec1f2b2be | ||
|
|
66efb0388a | ||
|
|
4ac5a91c95 | ||
|
|
4e8e6b9788 | ||
|
|
275a8a91ee | ||
|
|
270ac1c7d9 | ||
|
|
c42f290eb8 | ||
|
|
8b323bbf69 | ||
|
|
e68bf3ca5b | ||
|
|
b742da4bfa | ||
|
|
6368cb4cff | ||
|
|
81d68b115c | ||
|
|
d62b4531c3 | ||
|
|
1431210ecd | ||
|
|
50b1e1045c | ||
|
|
09a5da5d33 | ||
|
|
349acbba34 | ||
|
|
78128762f9 | ||
|
|
497cfcb74c | ||
|
|
11c806a4d0 | ||
|
|
9fb84cb89d | ||
|
|
1ceec0b6fa | ||
|
|
bbff3e8396 | ||
|
|
73cb96ddec | ||
|
|
25eb7d6472 | ||
|
|
6225cc2b9c |
+19
-2
@@ -7,6 +7,7 @@
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"project": [
|
||||
"demo/tsconfig.json",
|
||||
"src/browser/tsconfig.json",
|
||||
"src/common/tsconfig.json",
|
||||
"src/headless/tsconfig.json",
|
||||
@@ -38,7 +39,8 @@
|
||||
"**/*.js"
|
||||
],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
"@typescript-eslint",
|
||||
"jsdoc"
|
||||
],
|
||||
"rules": {
|
||||
"no-extra-semi": "error",
|
||||
@@ -95,7 +97,19 @@
|
||||
{ "selector": "enumMember", "format": ["UPPER_CASE"] },
|
||||
// memberLike - Allow enum-like objects to use UPPER_CASE
|
||||
{ "selector": "property", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
||||
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"] },
|
||||
// restrict on* naming for events only
|
||||
{ "selector": "method", "modifiers": ["public"], "format": ["camelCase", "UPPER_CASE"], "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
{ "selector": "method", "modifiers": ["private"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
{ "selector": "method", "modifiers": ["protected"], "format": ["camelCase"], "leadingUnderscore": "require", "custom": {
|
||||
"regex": "^on[A-Z].+",
|
||||
"match": false
|
||||
} },
|
||||
// typeLike
|
||||
{ "selector": "typeLike", "format": ["PascalCase"] },
|
||||
{ "selector": "interface", "format": ["PascalCase"], "prefix": ["I"] }
|
||||
@@ -128,6 +142,9 @@
|
||||
"warn",
|
||||
"always"
|
||||
],
|
||||
"jsdoc/check-alignment": 1,
|
||||
"jsdoc/check-param-names": 1,
|
||||
"jsdoc/no-multi-asterisks": 1,
|
||||
"keyword-spacing": "warn",
|
||||
"new-parens": "warn",
|
||||
"no-duplicate-imports": "warn",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://stackoverflow.com/questions/tagged/xtermjs
|
||||
about: Please ask and answer questions here.
|
||||
- name: Support / Q&A
|
||||
url: https://github.com/xtermjs/xterm.js/discussions/categories/q-a
|
||||
about: Use GitHub Discussions for community support and general Q&A
|
||||
|
||||
@@ -99,6 +99,26 @@ The full API for xterm.js is contained within the [TypeScript declaration file](
|
||||
|
||||
Note that some APIs are marked *experimental*, these are added to enable experimentation with new ideas without committing to support it like a normal [semver](https://semver.org/) API. Note that these APIs can change radically between versions, so be sure to read release notes if you plan on using experimental APIs.
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
|
||||
|
||||
```bash
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
|
||||
|
||||
## Real-world uses
|
||||
Xterm.js is used in several world-class applications to provide great terminal experiences.
|
||||
|
||||
@@ -166,7 +186,7 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**DockerStacks**](https://docker-stacks.com/): Local LAMP/LEMP development studio
|
||||
- [**Codecademy**](https://codecademy.com/): Uses xterm.js in its courses on Bash.
|
||||
- [**Laravel Ssh Web Client**](https://github.com/roke22/Laravel-ssh-client): Laravel server inventory with ssh web client to connect at server using xterm.js
|
||||
- [**Repl.it**](https://repl.it): Collaborative browser based IDE with support for 50+ different languages.
|
||||
- [**Replit**](https://replit.com): Collaborative browser based IDE with support for 50+ different languages.
|
||||
- [**TeleType**](https://github.com/akshaykmr/TeleType): cli tool that allows you to share your terminal online conveniently. Show off mad cli-fu, help a colleague, teach, or troubleshoot.
|
||||
- [**Intervue**](https://www.intervue.io): Pair programming for interviews. Multiple programming languages are supported, with results displayed by xterm.js.
|
||||
- [**TRASA**](https://trasa.io): Zero trust access to Web, SSH, RDP, and Database services.
|
||||
@@ -193,34 +213,15 @@ Xterm.js is used in several world-class applications to provide great terminal e
|
||||
- [**Go SSH Web Client**](https://github.com/wuchihsu/go-ssh-web-client): A simple SSH web client using Go, WebSocket and Xterm.js.
|
||||
- [**web3os**](https://web3os.sh): A decentralized operating system for the next web
|
||||
- [**Cratecode**](https://cratecode.com): Learn to program for free through interactive online lessons. Cratecode uses xterm.js to give users access to their own Linux environment.
|
||||
- [**Super Terminal**](https://github.com/bugwheels94/super-terminal): It is a http based terminal for developers who dont like repetition and save time.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only.
|
||||
|
||||
## Releases
|
||||
|
||||
Xterm.js follows a monthly release cycle roughly.
|
||||
|
||||
All current and past releases are available on this repo's [Releases page](https://github.com/sourcelair/xterm.js/releases), you can view the [high-level roadmap on the wiki](https://github.com/xtermjs/xterm.js/wiki/Roadmap) and see what we're working on now by looking through [Milestones](https://github.com/sourcelair/xterm.js/milestones).
|
||||
|
||||
### Beta builds
|
||||
|
||||
Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
|
||||
|
||||
```bash
|
||||
npm install -S xterm@beta
|
||||
```
|
||||
|
||||
These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes.
|
||||
|
||||
## Contributing
|
||||
|
||||
You can read the [guide on the wiki](https://github.com/xtermjs/xterm.js/wiki/Contributing) to learn how to contribute and set up xterm.js for development.
|
||||
|
||||
## License Agreement
|
||||
|
||||
If you contribute code to this project, you implicitly allow your code to be distributed under the MIT license. You are also implicitly verifying that all code is your original work.
|
||||
|
||||
Copyright (c) 2017-2019, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2017-2022, [The xterm.js authors](https://github.com/xtermjs/xterm.js/graphs/contributors) (MIT License)<br>
|
||||
Copyright (c) 2014-2017, SourceLair, Private Company ([www.sourcelair.com](https://www.sourcelair.com/home)) (MIT License)<br>
|
||||
Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-attach",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -47,16 +47,14 @@ export class AttachAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
private _sendData(data: string): void {
|
||||
// TODO: do something better than just swallowing
|
||||
// the data if the socket is not in a working condition
|
||||
if (this._socket.readyState !== 1) {
|
||||
if (!this._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
this._socket.send(data);
|
||||
}
|
||||
|
||||
private _sendBinary(data: string): void {
|
||||
if (this._socket.readyState !== 1) {
|
||||
if (!this._checkOpenSocket()) {
|
||||
return;
|
||||
}
|
||||
const buffer = new Uint8Array(data.length);
|
||||
@@ -65,6 +63,22 @@ export class AttachAddon implements ITerminalAddon {
|
||||
}
|
||||
this._socket.send(buffer);
|
||||
}
|
||||
|
||||
private _checkOpenSocket(): boolean {
|
||||
switch (this._socket.readyState) {
|
||||
case WebSocket.OPEN:
|
||||
return true;
|
||||
case WebSocket.CONNECTING:
|
||||
throw new Error('Attach addon was loaded before socket was open');
|
||||
case WebSocket.CLOSING:
|
||||
console.warn('Attach addon socket is closing');
|
||||
return false;
|
||||
case WebSocket.CLOSED:
|
||||
throw new Error('Attach addon socket is closed');
|
||||
default:
|
||||
throw new Error('Unexpected socket state');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSocketListener<K extends keyof WebSocketEventMap>(socket: WebSocket, type: K, handler: (this: WebSocket, ev: WebSocketEventMap[K]) => any): IDisposable {
|
||||
|
||||
@@ -3,77 +3,78 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderLayer } from './Types';
|
||||
import { ICellData, IColor } from 'common/Types';
|
||||
import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants';
|
||||
import { IGlyphIdentifier } from './atlas/Types';
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
|
||||
import { BaseCharAtlas } from './atlas/BaseCharAtlas';
|
||||
import { acquireCharAtlas } from './atlas/CharAtlasCache';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { excludeFromContrastRatioDemands, throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { channels, color, rgba } from 'common/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ReadonlyColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WHITESPACE_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICellData, IDisposable } from 'common/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IRenderLayer } from './Types';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number = 0;
|
||||
private _scaledCharHeight: number = 0;
|
||||
private _scaledCellWidth: number = 0;
|
||||
private _scaledCellHeight: number = 0;
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
private _deviceCharWidth: number = 0;
|
||||
private _deviceCharHeight: number = 0;
|
||||
private _deviceCellWidth: number = 0;
|
||||
private _deviceCellHeight: number = 0;
|
||||
private _deviceCharLeft: number = 0;
|
||||
private _deviceCharTop: number = 0;
|
||||
|
||||
protected _selectionStart: [number, number] | undefined;
|
||||
protected _selectionEnd: [number, number] | undefined;
|
||||
protected _columnSelectMode: boolean = false;
|
||||
protected _selectionModel: ISelectionRenderModel = createSelectionRenderModel();
|
||||
private _cellColorResolver: CellColorResolver;
|
||||
private _bitmapGenerator: (BitmapGenerator | undefined)[] = [];
|
||||
|
||||
protected _charAtlas: BaseCharAtlas | undefined;
|
||||
|
||||
/**
|
||||
* An object that's reused when drawing glyphs in order to reduce GC.
|
||||
*/
|
||||
private _currentGlyphIdentifier: IGlyphIdentifier = {
|
||||
chars: '',
|
||||
code: 0,
|
||||
bg: 0,
|
||||
fg: 0,
|
||||
bold: false,
|
||||
dim: false,
|
||||
italic: false
|
||||
};
|
||||
protected _charAtlas!: ITextureAtlas;
|
||||
private _charAtlasDisposable?: IDisposable;
|
||||
|
||||
public get canvas(): HTMLCanvasElement { return this._canvas; }
|
||||
public get cacheCanvas(): HTMLCanvasElement { return this._charAtlas?.pages[0].canvas!; }
|
||||
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet,
|
||||
private _rendererId: number,
|
||||
protected readonly _themeService: IThemeService,
|
||||
protected readonly _bufferService: IBufferService,
|
||||
protected readonly _optionsService: IOptionsService,
|
||||
protected readonly _decorationService: IDecorationService,
|
||||
protected readonly _coreBrowserService: ICoreBrowserService
|
||||
) {
|
||||
super();
|
||||
this._cellColorResolver = new CellColorResolver(this._terminal, this._selectionModel, this._decorationService, this._coreBrowserService, this._themeService);
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
this._refreshCharAtlas(this._themeService.colors);
|
||||
this.register(this._themeService.onChangeColors(e => {
|
||||
this._refreshCharAtlas(e);
|
||||
this.reset();
|
||||
}));
|
||||
|
||||
public dispose(): void {
|
||||
removeElementFromParent(this._canvas);
|
||||
this._charAtlas?.dispose();
|
||||
this.register(toDisposable(() => {
|
||||
removeElementFromParent(this._canvas);
|
||||
this._charAtlas?.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
@@ -84,20 +85,13 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {}
|
||||
public onBlur(): void {}
|
||||
public onFocus(): void {}
|
||||
public onCursorMove(): void {}
|
||||
public onGridChanged(startRow: number, endRow: number): void {}
|
||||
public handleBlur(): void {}
|
||||
public handleFocus(): void {}
|
||||
public handleCursorMove(): void {}
|
||||
public handleGridChanged(startRow: number, endRow: number): void {}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._selectionStart = start;
|
||||
this._selectionEnd = end;
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public setColors(colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(colorSet);
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._selectionModel.update(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
protected _setTransparency(alpha: boolean): void {
|
||||
@@ -115,46 +109,51 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(this._colors);
|
||||
this.onGridChanged(0, this._bufferService.rows - 1);
|
||||
this._refreshCharAtlas(this._themeService.colors);
|
||||
this.handleGridChanged(0, this._bufferService.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(colorSet: IColorSet): void {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
private _refreshCharAtlas(colorSet: ReadonlyColorSet): void {
|
||||
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(this._optionsService.rawOptions, this._rendererId, colorSet, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlasDisposable?.dispose();
|
||||
this._charAtlas = acquireTextureAtlas(this._terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlasDisposable = forwardEvent(this._charAtlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
this._charAtlas.warmUp();
|
||||
for (let i = 0; i < this._charAtlas.pages.length; i++) {
|
||||
this._bitmapGenerator[i] = new BitmapGenerator(this._charAtlas.pages[i].canvas);
|
||||
}
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
this._scaledCharHeight = dim.scaledCharHeight;
|
||||
this._scaledCharLeft = dim.scaledCharLeft;
|
||||
this._scaledCharTop = dim.scaledCharTop;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||
this._deviceCellWidth = dim.device.cell.width;
|
||||
this._deviceCellHeight = dim.device.cell.height;
|
||||
this._deviceCharWidth = dim.device.char.width;
|
||||
this._deviceCharHeight = dim.device.char.height;
|
||||
this._deviceCharLeft = dim.device.char.left;
|
||||
this._deviceCharTop = dim.device.char.top;
|
||||
this._canvas.width = dim.device.canvas.width;
|
||||
this._canvas.height = dim.device.canvas.height;
|
||||
this._canvas.style.width = `${dim.css.canvas.width}px`;
|
||||
this._canvas.style.height = `${dim.css.canvas.height}px`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(this._colors);
|
||||
this._refreshCharAtlas(this._themeService.colors);
|
||||
}
|
||||
|
||||
public abstract reset(): void;
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._charAtlas?.clear();
|
||||
this._charAtlas?.clearTexture();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,24 +165,24 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
* Fills a 1px line (2px on HDPI) at the middle of the cell. This uses the
|
||||
* existing fillStyle on the context.
|
||||
* @param x The column to fill.
|
||||
* @param y The row to fill.
|
||||
*/
|
||||
protected _fillMiddleLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
const cellOffset = Math.ceil(this._scaledCellHeight * 0.5);
|
||||
const cellOffset = Math.ceil(this._deviceCellHeight * 0.5);
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - cellOffset - this._coreBrowserService.dpr,
|
||||
width * this._scaledCellWidth,
|
||||
x * this._deviceCellWidth,
|
||||
(y + 1) * this._deviceCellHeight - cellOffset - this._coreBrowserService.dpr,
|
||||
width * this._deviceCellWidth,
|
||||
this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
@@ -195,9 +194,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1, pixelOffset: number = 0): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
x * this._deviceCellWidth,
|
||||
(y + 1) * this._deviceCellHeight + pixelOffset - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._deviceCellWidth,
|
||||
this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
@@ -208,10 +207,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
for (let xOffset = 0; xOffset < width; xOffset++) {
|
||||
const xLeft = (x + xOffset) * this._scaledCellWidth;
|
||||
const xMid = (x + xOffset + 0.5) * this._scaledCellWidth;
|
||||
const xRight = (x + xOffset + 1) * this._scaledCellWidth;
|
||||
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
|
||||
const xLeft = (x + xOffset) * this._deviceCellWidth;
|
||||
const xMid = (x + xOffset + 0.5) * this._deviceCellWidth;
|
||||
const xRight = (x + xOffset + 1) * this._deviceCellWidth;
|
||||
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
|
||||
const yMidBot = yMid - lineWidth;
|
||||
const yMidTop = yMid + lineWidth;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
@@ -237,12 +236,12 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
this._ctx.setLineDash([lineWidth * 2, lineWidth]);
|
||||
const xLeft = x * this._scaledCellWidth;
|
||||
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
|
||||
const xLeft = x * this._deviceCellWidth;
|
||||
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
for (let xOffset = 0; xOffset < width; xOffset++) {
|
||||
// const xLeft = x * this._scaledCellWidth;
|
||||
const xRight = (x + width + xOffset) * this._scaledCellWidth;
|
||||
// const xLeft = x * this._deviceCellWidth;
|
||||
const xRight = (x + width + xOffset) * this._deviceCellWidth;
|
||||
this._ctx.lineTo(xRight, yMid);
|
||||
}
|
||||
this._ctx.stroke();
|
||||
@@ -257,9 +256,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
this._ctx.setLineDash([lineWidth * 4, lineWidth * 3]);
|
||||
const xLeft = x * this._scaledCellWidth;
|
||||
const xRight = (x + width) * this._scaledCellWidth;
|
||||
const yMid = (y + 1) * this._scaledCellHeight - lineWidth - 1;
|
||||
const xLeft = x * this._deviceCellWidth;
|
||||
const xRight = (x + width) * this._deviceCellWidth;
|
||||
const yMid = (y + 1) * this._deviceCellHeight - lineWidth - 1;
|
||||
this._ctx.moveTo(xLeft, yMid);
|
||||
this._ctx.lineTo(xRight, yMid);
|
||||
this._ctx.stroke();
|
||||
@@ -275,10 +274,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
this._coreBrowserService.dpr * width,
|
||||
this._scaledCellHeight);
|
||||
this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -291,10 +290,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
const lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.lineWidth = lineWidth;
|
||||
this._ctx.strokeRect(
|
||||
x * this._scaledCellWidth + lineWidth / 2,
|
||||
y * this._scaledCellHeight + (lineWidth / 2),
|
||||
width * this._scaledCellWidth - lineWidth,
|
||||
(height * this._scaledCellHeight) - lineWidth);
|
||||
x * this._deviceCellWidth + lineWidth / 2,
|
||||
y * this._deviceCellHeight + (lineWidth / 2),
|
||||
width * this._deviceCellWidth - lineWidth,
|
||||
(height * this._deviceCellHeight) - lineWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -304,7 +303,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
@@ -319,17 +318,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +339,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected _fillCharTrueColor(cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(false, false);
|
||||
@@ -350,149 +348,58 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.rawOptions.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._deviceCellWidth, y * this._deviceCellHeight, this._deviceCellWidth, this._deviceCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!drawSuccess) {
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
x * this._deviceCellWidth + this._deviceCharLeft,
|
||||
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one or more characters at a cell. If possible this will draw using
|
||||
* the character atlas to reduce draw time.
|
||||
* @param chars The character or characters.
|
||||
* @param code The character code.
|
||||
* @param width The width of the characters.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param bg The background color, in the format stored within the attributes.
|
||||
* This is used to validate whether a cached image can be used.
|
||||
* @param bold Whether the text is bold.
|
||||
*/
|
||||
protected _drawChars(cell: ICellData, x: number, y: number): void {
|
||||
const contrastColor = this._getContrastColor(cell, x, y);
|
||||
|
||||
// skip cache right away if we draw in RGB
|
||||
// Note: to avoid bad runtime JoinedCellData will be skipped
|
||||
// in the cache handler itself (atlasDidDraw == false) and
|
||||
// fall through to uncached later down below
|
||||
if (contrastColor || cell.isFgRGB() || cell.isBgRGB()) {
|
||||
this._drawUncachedChars(cell, x, y, contrastColor);
|
||||
const chars = cell.getChars();
|
||||
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y);
|
||||
let glyph: IRasterizedGlyph;
|
||||
if (chars && chars.length > 1) {
|
||||
glyph = this._charAtlas.getRasterizedGlyphCombinedChar(chars, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
|
||||
} else {
|
||||
glyph = this._charAtlas.getRasterizedGlyph(cell.getCode() || WHITESPACE_CELL_CODE, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
|
||||
}
|
||||
if (!glyph.size.x || !glyph.size.y) {
|
||||
return;
|
||||
}
|
||||
|
||||
let fg;
|
||||
let bg;
|
||||
if (cell.isInverse()) {
|
||||
fg = (cell.isBgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getBgColor();
|
||||
bg = (cell.isFgDefault()) ? INVERTED_DEFAULT_COLOR : cell.getFgColor();
|
||||
} else {
|
||||
bg = (cell.isBgDefault()) ? DEFAULT_COLOR : cell.getBgColor();
|
||||
fg = (cell.isFgDefault()) ? DEFAULT_COLOR : cell.getFgColor();
|
||||
}
|
||||
|
||||
const drawInBrightColor = this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8;
|
||||
|
||||
fg += drawInBrightColor ? 8 : 0;
|
||||
this._currentGlyphIdentifier.chars = cell.getChars() || WHITESPACE_CELL_CHAR;
|
||||
this._currentGlyphIdentifier.code = cell.getCode() || WHITESPACE_CELL_CODE;
|
||||
this._currentGlyphIdentifier.bg = bg;
|
||||
this._currentGlyphIdentifier.fg = fg;
|
||||
this._currentGlyphIdentifier.bold = !!cell.isBold();
|
||||
this._currentGlyphIdentifier.dim = !!cell.isDim();
|
||||
this._currentGlyphIdentifier.italic = !!cell.isItalic();
|
||||
|
||||
// Don't try cache the glyph if it uses any decoration foreground/background override.
|
||||
let hasOverrides = false;
|
||||
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
|
||||
if (d.backgroundColorRGB || d.foregroundColorRGB) {
|
||||
hasOverrides = true;
|
||||
}
|
||||
});
|
||||
|
||||
const atlasDidDraw = hasOverrides ? false : this._charAtlas?.draw(this._ctx, this._currentGlyphIdentifier, x * this._scaledCellWidth + this._scaledCharLeft, y * this._scaledCellHeight + this._scaledCharTop);
|
||||
|
||||
if (!atlasDidDraw) {
|
||||
this._drawUncachedChars(cell, x, y);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one or more characters at one or more cells. The character(s) will be
|
||||
* clipped to ensure that they fit with the cell(s), including the cell to the
|
||||
* right if the last character is a wide character.
|
||||
* @param chars The character.
|
||||
* @param width The width of the character.
|
||||
* @param fg The foreground color, in the format stored within the attributes.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
*/
|
||||
private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void {
|
||||
this._ctx.save();
|
||||
this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic());
|
||||
this._ctx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (fgOverride) {
|
||||
this._ctx.fillStyle = fgOverride.css;
|
||||
} else if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = color.opaque(this._colors.background).css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
} else {
|
||||
if (fgOverride) {
|
||||
this._ctx.fillStyle = fgOverride.css;
|
||||
} else if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
|
||||
this._clipRow(y);
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (cell.isDim()) {
|
||||
this._ctx.globalAlpha = DIM_OPACITY;
|
||||
// Draw the image, use the bitmap if it's available
|
||||
if (this._charAtlas.pages[glyph.texturePage].version !== this._bitmapGenerator[glyph.texturePage]?.version) {
|
||||
if (!this._bitmapGenerator[glyph.texturePage]) {
|
||||
this._bitmapGenerator[glyph.texturePage] = new BitmapGenerator(this._charAtlas.pages[glyph.texturePage].canvas);
|
||||
}
|
||||
this._bitmapGenerator[glyph.texturePage]!.refresh();
|
||||
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version;
|
||||
}
|
||||
|
||||
// Draw custom characters if applicable
|
||||
let drawSuccess = false;
|
||||
if (this._optionsService.rawOptions.customGlyphs !== false) {
|
||||
drawSuccess = tryDrawCustomChar(this._ctx, cell.getChars(), x * this._scaledCellWidth, y * this._scaledCellHeight, this._scaledCellWidth, this._scaledCellHeight, this._optionsService.rawOptions.fontSize, this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
if (!drawSuccess) {
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
}
|
||||
|
||||
this._ctx.drawImage(
|
||||
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
|
||||
glyph.texturePosition.x,
|
||||
glyph.texturePosition.y,
|
||||
glyph.size.x,
|
||||
glyph.size.y,
|
||||
x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x,
|
||||
y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y,
|
||||
glyph.size.x,
|
||||
glyph.size.y
|
||||
);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clips a row to ensure no pixels will be drawn outside the cells in the row.
|
||||
* @param y The row to clip.
|
||||
@@ -501,9 +408,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
0,
|
||||
y * this._scaledCellHeight,
|
||||
this._bufferService.cols * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
y * this._deviceCellHeight,
|
||||
this._bufferService.cols * this._deviceCellWidth,
|
||||
this._deviceCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
@@ -517,137 +424,60 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
|
||||
return `${fontStyle} ${fontWeight} ${this._optionsService.rawOptions.fontSize * this._coreBrowserService.dpr}px ${this._optionsService.rawOptions.fontFamily}`;
|
||||
}
|
||||
|
||||
private _getContrastColor(cell: CellData, x: number, y: number): IColor | undefined {
|
||||
// Get any decoration foreground/background overrides, this must be fetched before the early
|
||||
// exist but applied after inverse
|
||||
let bgOverride: number | undefined;
|
||||
let fgOverride: number | undefined;
|
||||
let isTop = false;
|
||||
this._decorationService.forEachDecorationAtCell(x, y, undefined, d => {
|
||||
if (d.options.layer !== 'top' && isTop) {
|
||||
return;
|
||||
}
|
||||
if (d.backgroundColorRGB) {
|
||||
bgOverride = d.backgroundColorRGB.rgba;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
fgOverride = d.foregroundColorRGB.rgba;
|
||||
}
|
||||
isTop = d.options.layer === 'top';
|
||||
});
|
||||
|
||||
// Apply selection foreground if applicable
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && this._isCellInSelection(x, y)) {
|
||||
fgOverride = this._colors.selectionForeground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
if (!bgOverride && !fgOverride && (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode()))) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!bgOverride && !fgOverride) {
|
||||
// Try get from cache
|
||||
const adjustedColor = this._colors.contrastCache.getColor(cell.bg, cell.fg);
|
||||
if (adjustedColor !== undefined) {
|
||||
return adjustedColor || undefined;
|
||||
}
|
||||
}
|
||||
|
||||
let fgColor = cell.getFgColor();
|
||||
let fgColorMode = cell.getFgColorMode();
|
||||
let bgColor = cell.getBgColor();
|
||||
let bgColorMode = cell.getBgColorMode();
|
||||
const isInverse = !!cell.isInverse();
|
||||
const isBold = !!cell.isInverse();
|
||||
if (isInverse) {
|
||||
const temp = fgColor;
|
||||
fgColor = bgColor;
|
||||
bgColor = temp;
|
||||
const temp2 = fgColorMode;
|
||||
fgColorMode = bgColorMode;
|
||||
bgColorMode = temp2;
|
||||
}
|
||||
|
||||
const bgRgba = this._resolveBackgroundRgba(bgOverride !== undefined ? Attributes.CM_RGB : bgColorMode, bgOverride ?? bgColor, isInverse);
|
||||
const fgRgba = this._resolveForegroundRgba(fgColorMode, fgColor, isInverse, isBold);
|
||||
let result = rgba.ensureContrastRatio(bgOverride ?? bgRgba, fgOverride ?? fgRgba, this._optionsService.rawOptions.minimumContrastRatio);
|
||||
|
||||
if (!result) {
|
||||
if (!fgOverride) {
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, null);
|
||||
return undefined;
|
||||
}
|
||||
// If it was an override and there was no contrast change, set as the result
|
||||
result = fgOverride;
|
||||
}
|
||||
|
||||
const color: IColor = {
|
||||
css: channels.toCss(
|
||||
(result >> 24) & 0xFF,
|
||||
(result >> 16) & 0xFF,
|
||||
(result >> 8) & 0xFF
|
||||
),
|
||||
rgba: result
|
||||
};
|
||||
if (!bgOverride && !fgOverride) {
|
||||
this._colors.contrastCache.setColor(cell.bg, cell.fg, color);
|
||||
}
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
private _resolveBackgroundRgba(bgColorMode: number, bgColor: number, inverse: boolean): number {
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
return this._colors.ansi[bgColor].rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return bgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._colors.foreground.rgba;
|
||||
}
|
||||
return this._colors.background.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _resolveForegroundRgba(fgColorMode: number, fgColor: number, inverse: boolean, bold: boolean): number {
|
||||
switch (fgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && bold && fgColor < 8) {
|
||||
fgColor += 8;
|
||||
}
|
||||
return this._colors.ansi[fgColor].rgba;
|
||||
case Attributes.CM_RGB:
|
||||
return fgColor << 8;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (inverse) {
|
||||
return this._colors.background.rgba;
|
||||
}
|
||||
return this._colors.foreground.rgba;
|
||||
}
|
||||
}
|
||||
|
||||
private _isCellInSelection(x: number, y: number): boolean {
|
||||
const start = this._selectionStart;
|
||||
const end = this._selectionEnd;
|
||||
if (!start || !end) {
|
||||
return false;
|
||||
}
|
||||
if (this._columnSelectMode) {
|
||||
return x >= start[0] && y >= start[1] &&
|
||||
x < end[0] && y < end[1];
|
||||
}
|
||||
return (y > start[1] && y < end[1]) ||
|
||||
(start[1] === end[1] && y === start[1] && x >= start[0] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === end[1] && x < end[0]) ||
|
||||
(start[1] < end[1] && y === start[1] && x >= start[0]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
|
||||
* the operation as window.createImageBitmap is asynchronous.
|
||||
*/
|
||||
const GLYPH_BITMAP_COMMIT_DELAY = 100;
|
||||
|
||||
const enum BitmapGeneratorState {
|
||||
IDLE = 0,
|
||||
GENERATING = 1,
|
||||
GENERATING_INVALID = 2
|
||||
}
|
||||
|
||||
class BitmapGenerator {
|
||||
private _state: BitmapGeneratorState = BitmapGeneratorState.IDLE;
|
||||
private _commitTimeout: number | undefined = undefined;
|
||||
private _bitmap: ImageBitmap | undefined = undefined;
|
||||
public get bitmap(): ImageBitmap | undefined { return this._bitmap; }
|
||||
public version: number = -1;
|
||||
|
||||
constructor(private readonly _canvas: HTMLCanvasElement) {
|
||||
}
|
||||
|
||||
public refresh(): void {
|
||||
// Clear the bitmap immediately as it's stale
|
||||
this._bitmap = undefined;
|
||||
// Disable ImageBitmaps on Safari because of https://bugs.webkit.org/show_bug.cgi?id=149990
|
||||
if (isSafari) {
|
||||
return;
|
||||
}
|
||||
if (this._commitTimeout === undefined) {
|
||||
this._commitTimeout = window.setTimeout(() => this._generate(), GLYPH_BITMAP_COMMIT_DELAY);
|
||||
}
|
||||
if (this._state === BitmapGeneratorState.GENERATING) {
|
||||
this._state = BitmapGeneratorState.GENERATING_INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
private _generate(): void {
|
||||
if (this._state === BitmapGeneratorState.IDLE) {
|
||||
this._bitmap = undefined;
|
||||
this._state = BitmapGeneratorState.GENERATING;
|
||||
window.createImageBitmap(this._canvas).then(bitmap => {
|
||||
if (this._state === BitmapGeneratorState.GENERATING_INVALID) {
|
||||
this.refresh();
|
||||
} else {
|
||||
this._bitmap = bitmap;
|
||||
}
|
||||
this._state = BitmapGeneratorState.IDLE;
|
||||
});
|
||||
if (this._commitTimeout) {
|
||||
this._commitTimeout = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,45 +3,60 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
import { CanvasRenderer } from './CanvasRenderer';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
export class CanvasAddon implements ITerminalAddon {
|
||||
export class CanvasAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: CanvasRenderer;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (!terminal.element) {
|
||||
throw new Error('Cannot activate CanvasAddon before Terminal.open');
|
||||
}
|
||||
this._terminal = terminal;
|
||||
const bufferService: IBufferService = (terminal as any)._core._bufferService;
|
||||
const renderService: IRenderService = (terminal as any)._core._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
|
||||
const charSizeService: ICharSizeService = (terminal as any)._core._charSizeService;
|
||||
const coreService: ICoreService = (terminal as any)._core.coreService;
|
||||
const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService;
|
||||
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
|
||||
const optionsService: IOptionsService = (terminal as any)._core.optionsService;
|
||||
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
|
||||
const screenElement: HTMLElement = (terminal as any)._core.screenElement;
|
||||
const linkifier = (terminal as any)._core.linkifier2;
|
||||
this._renderer = new CanvasRenderer(colors, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService);
|
||||
renderService.setRenderer(this._renderer);
|
||||
renderService.onResize(bufferService.cols, bufferService.rows);
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._renderer?.textureAtlas;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot dispose CanvasAddon because it is activated');
|
||||
public activate(terminal: Terminal): void {
|
||||
const core = (terminal as any)._core as ITerminal;
|
||||
if (!terminal.element) {
|
||||
this.register(core.onWillOpen(() => this.activate(terminal)));
|
||||
return;
|
||||
}
|
||||
const renderService: IRenderService = (this._terminal as any)._core._renderService;
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.onResize(this._terminal.cols, this._terminal.rows);
|
||||
this._renderer?.dispose();
|
||||
this._renderer = undefined;
|
||||
|
||||
this._terminal = terminal;
|
||||
const coreService = core.coreService;
|
||||
const optionsService = core.optionsService;
|
||||
const screenElement = core.screenElement!;
|
||||
const linkifier = core.linkifier2;
|
||||
|
||||
const unsafeCore = core as any;
|
||||
const bufferService: IBufferService = unsafeCore._bufferService;
|
||||
const renderService: IRenderService = unsafeCore._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
|
||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
this._renderer = new CanvasRenderer(terminal, screenElement, linkifier, bufferService, charSizeService, optionsService, characterJoinerService, coreService, coreBrowserService, decorationService, themeService);
|
||||
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
renderService.handleResize(bufferService.cols, bufferService.rows);
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.handleResize(terminal.cols, terminal.rows);
|
||||
this._renderer?.dispose();
|
||||
this._renderer = undefined;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,35 +3,37 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2 } from 'browser/Types';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { CursorRenderLayer } from './CursorRenderLayer';
|
||||
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IRenderLayer } from './Types';
|
||||
import { LinkRenderLayer } from './LinkRenderLayer';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService, IDecorationService, ICoreService } from 'common/services/Services';
|
||||
import { removeTerminalFromCache } from './atlas/CharAtlasCache';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
|
||||
|
||||
let nextRendererId = 1;
|
||||
import { SelectionRenderLayer } from './SelectionRenderLayer';
|
||||
import { TextRenderLayer } from './TextRenderLayer';
|
||||
import { IRenderLayer } from './Types';
|
||||
|
||||
export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
private _id = nextRendererId++;
|
||||
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
||||
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
|
||||
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
private _colors: IColorSet,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
linkifier2: ILinkifier2,
|
||||
private readonly _bufferService: IBufferService,
|
||||
@@ -40,65 +42,47 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
characterJoinerService: ICharacterJoinerService,
|
||||
coreService: ICoreService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService
|
||||
decorationService: IDecorationService,
|
||||
private readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
const allowTransparency = this._optionsService.rawOptions.allowTransparency;
|
||||
this._renderLayers = [
|
||||
new TextRenderLayer(this._screenElement, 0, this._colors, allowTransparency, this._id, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService),
|
||||
new SelectionRenderLayer(this._screenElement, 1, this._colors, this._id, this._bufferService, this._coreBrowserService, decorationService, this._optionsService),
|
||||
new LinkRenderLayer(this._screenElement, 2, this._colors, this._id, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService),
|
||||
new CursorRenderLayer(this._screenElement, 3, this._colors, this._id, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService)
|
||||
new TextRenderLayer(this._terminal, this._screenElement, 0, allowTransparency, this._bufferService, this._optionsService, characterJoinerService, decorationService, this._coreBrowserService, _themeService),
|
||||
new SelectionRenderLayer(this._terminal, this._screenElement, 1, this._bufferService, this._coreBrowserService, decorationService, this._optionsService, _themeService),
|
||||
new LinkRenderLayer(this._terminal, this._screenElement, 2, linkifier2, this._bufferService, this._optionsService, decorationService, this._coreBrowserService, _themeService),
|
||||
new CursorRenderLayer(this._terminal, this._screenElement, 3, this._onRequestRedraw, this._bufferService, this._optionsService, coreService, this._coreBrowserService, decorationService, _themeService)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
for (const layer of this._renderLayers) {
|
||||
forwardEvent(layer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas);
|
||||
}
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this._updateDimensions();
|
||||
|
||||
this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)));
|
||||
|
||||
this.onOptionsChanged();
|
||||
this.register(toDisposable(() => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
removeTerminalFromCache(this._terminal);
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
removeTerminalFromCache(this._id);
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._renderLayers[0].cacheCanvas;
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this.onResize(this._bufferService.cols, this._bufferService.rows);
|
||||
this.handleResize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
// Clear layers and force a full render
|
||||
for (const l of this._renderLayers) {
|
||||
l.setColors(this._colors);
|
||||
l.reset();
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
@@ -108,36 +92,32 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// Resize the screen
|
||||
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._bufferService.cols, this._bufferService.rows);
|
||||
public handleCharSizeChanged(): void {
|
||||
this.handleResize(this._bufferService.cols, this._bufferService.rows);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._runOperation(l => l.onBlur());
|
||||
public handleBlur(): void {
|
||||
this._runOperation(l => l.handleBlur());
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._runOperation(l => l.onFocus());
|
||||
public handleFocus(): void {
|
||||
this._runOperation(l => l.handleFocus());
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.onSelectionChanged(start, end, columnSelectMode));
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this._runOperation(l => l.handleSelectionChanged(start, end, columnSelectMode));
|
||||
// Selection foreground requires a full re-render
|
||||
if (this._colors.selectionForeground) {
|
||||
if (this._themeService.colors.selectionForeground) {
|
||||
this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 });
|
||||
}
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._runOperation(l => l.onCursorMove());
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._runOperation(l => l.onOptionsChanged());
|
||||
public handleCursorMove(): void {
|
||||
this._runOperation(l => l.handleCursorMove());
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
@@ -156,7 +136,7 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
*/
|
||||
public renderRows(start: number, end: number): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onGridChanged(start, end);
|
||||
l.handleGridChanged(start, end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,23 +156,23 @@ export class CanvasRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// See the WebGL renderer for an explanation of this section.
|
||||
const dpr = this._coreBrowserService.dpr;
|
||||
this.dimensions.scaledCharWidth = Math.floor(this._charSizeService.width * dpr);
|
||||
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr);
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
|
||||
this.dimensions.scaledCharTop = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
this.dimensions.scaledCanvasHeight = this._bufferService.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._bufferService.cols * this.dimensions.scaledCellWidth;
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr);
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
|
||||
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * dpr);
|
||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
|
||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
||||
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
this.dimensions.device.canvas.height = this._bufferService.rows * this.dimensions.device.cell.height;
|
||||
this.dimensions.device.canvas.width = this._bufferService.cols * this.dimensions.device.cell.width;
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
|
||||
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
|
||||
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
|
||||
}
|
||||
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
this.dimensions.scaledCanvasHeight = height;
|
||||
this.dimensions.scaledCanvasWidth = width;
|
||||
this.dimensions.device.canvas.height = height;
|
||||
this.dimensions.device.canvas.width = width;
|
||||
// Resize all render layers
|
||||
for (const l of this._renderLayers) {
|
||||
l.resize(this.dimensions);
|
||||
|
||||
@@ -3,14 +3,16 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService, IDecorationService } from 'common/services/Services';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -32,18 +34,18 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
private _cell: ICellData = new CellData();
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
private readonly _onRequestRedraw: IEventEmitter<IRequestRedrawEvent>,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
private readonly _coreService: ICoreService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService
|
||||
decorationService: IDecorationService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
super(terminal, container, 'cursor', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -56,14 +58,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager.dispose();
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this.register(toDisposable(() => {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
}
|
||||
super.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -81,20 +80,20 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
public reset(): void {
|
||||
this._clearCursor();
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation();
|
||||
this.onOptionsChanged();
|
||||
this._handleOptionsChanged();
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
public handleBlur(): void {
|
||||
this._cursorBlinkStateManager?.pause();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
public handleFocus(): void {
|
||||
this._cursorBlinkStateManager?.resume();
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
private _handleOptionsChanged(): void {
|
||||
if (this._optionsService.rawOptions.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(this._coreBrowserService.isFocused, () => {
|
||||
@@ -110,11 +109,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._onRequestRedraw.fire({ start: this._bufferService.buffer.y, end: this._bufferService.buffer.y });
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
public handleCursorMove(): void {
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation();
|
||||
}
|
||||
|
||||
public onGridChanged(startRow: number, endRow: number): void {
|
||||
public handleGridChanged(startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(false);
|
||||
} else {
|
||||
@@ -148,7 +147,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (!this._coreBrowserService.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](cursorX, viewportRelativeCursorY, this._cell);
|
||||
@@ -213,30 +212,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _renderBarCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
|
||||
this._fillCharTrueColor(cell, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
@@ -3,32 +3,33 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { is256Color } from './atlas/CharAtlasUtils';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier2, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
linkifier2: ILinkifier2,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
decorationService: IDecorationService,
|
||||
coreBrowserService: ICoreBrowserService
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
super(terminal, container, 'link', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
|
||||
linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
|
||||
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -53,14 +54,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
} else if (e.fg && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg].css;
|
||||
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
@@ -77,7 +78,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
interface ISelectionState {
|
||||
start?: [number, number];
|
||||
@@ -20,16 +21,16 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
private _state!: ISelectionState;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
rendererId: number,
|
||||
bufferService: IBufferService,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
decorationService: IDecorationService,
|
||||
optionsService: IOptionsService
|
||||
optionsService: IOptionsService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'selection', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
super(terminal, container, 'selection', zIndex, true, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._clearState();
|
||||
}
|
||||
|
||||
@@ -46,8 +47,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
super.resize(dim);
|
||||
// On resize use the base render layer's cached selection values since resize clears _state
|
||||
// inside reset.
|
||||
if (this._selectionStart && this._selectionEnd) {
|
||||
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
|
||||
if (this._selectionModel.selectionStart && this._selectionModel.selectionEnd) {
|
||||
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,18 +59,18 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
public handleBlur(): void {
|
||||
this.reset();
|
||||
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
|
||||
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
public handleFocus(): void {
|
||||
this.reset();
|
||||
this._redrawSelection(this._selectionStart, this._selectionEnd, this._columnSelectMode);
|
||||
this._redrawSelection(this._selectionModel.selectionStart, this._selectionModel.selectionEnd, this._selectionModel.columnSelectMode);
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
super.onSelectionChanged(start, end, columnSelectMode);
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
super.handleSelectionChanged(start, end, columnSelectMode);
|
||||
this._redrawSelection(start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
@@ -101,8 +102,8 @@ export class SelectionRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
this._ctx.fillStyle = (this._coreBrowserService.isFocused
|
||||
? this._colors.selectionBackgroundTransparent
|
||||
: this._colors.selectionInactiveBackgroundTransparent).css;
|
||||
? this._themeService.colors.selectionBackgroundTransparent
|
||||
: this._themeService.colors.selectionInactiveBackgroundTransparent).css;
|
||||
|
||||
if (columnSelectMode) {
|
||||
const startCol = start[0];
|
||||
|
||||
@@ -3,18 +3,19 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { CharData, ICellData } from 'common/Types';
|
||||
import { GridCache } from './GridCache';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { NULL_CELL_CODE, Content, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { color, css } from 'common/Color';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
/**
|
||||
* This CharData looks like a null character, which will forc a clear and render
|
||||
@@ -31,19 +32,20 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
private _workCell = new CellData();
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
alpha: boolean,
|
||||
rendererId: number,
|
||||
bufferService: IBufferService,
|
||||
optionsService: IOptionsService,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
decorationService: IDecorationService,
|
||||
coreBrowserService: ICoreBrowserService
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'text', zIndex, alpha, colors, rendererId, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
super(terminal, container, 'text', zIndex, alpha, themeService, bufferService, optionsService, decorationService, coreBrowserService);
|
||||
this._state = new GridCache<CharData>();
|
||||
this.register(optionsService.onSpecificOptionChange('allowTransparency', value => this._setTransparency(value)));
|
||||
}
|
||||
|
||||
public resize(dim: IRenderDimensions): void {
|
||||
@@ -51,8 +53,8 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
// Clear the character width cache if the font or width has changed
|
||||
const terminalFont = this._getFont(false, false);
|
||||
if (this._characterWidth !== dim.scaledCharWidth || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.scaledCharWidth;
|
||||
if (this._characterWidth !== dim.device.char.width || this._characterFont !== terminalFont) {
|
||||
this._characterWidth = dim.device.char.width;
|
||||
this._characterFont = terminalFont;
|
||||
this._characterOverlapCache = {};
|
||||
}
|
||||
@@ -93,6 +95,12 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
continue;
|
||||
}
|
||||
|
||||
// exit early for NULL and SP
|
||||
const code = cell.getCode();
|
||||
if (code === 0 || code === 32) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Process any joined character ranges as needed. Because of how the
|
||||
// ranges are produced, we know that they are valid for the characters
|
||||
// and attributes of our input.
|
||||
@@ -167,16 +175,16 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isFgDefault()) {
|
||||
nextFillStyle = this._colors.foreground.css;
|
||||
nextFillStyle = this._themeService.colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
nextFillStyle = this._colors.ansi[cell.getFgColor()].css;
|
||||
nextFillStyle = this._themeService.colors.ansi[cell.getFgColor()].css;
|
||||
}
|
||||
} else if (cell.isBgRGB()) {
|
||||
nextFillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else if (cell.isBgPalette()) {
|
||||
nextFillStyle = this._colors.ansi[cell.getBgColor()].css;
|
||||
nextFillStyle = this._themeService.colors.ansi[cell.getBgColor()].css;
|
||||
}
|
||||
|
||||
// Apply dim to the background, this is relatively slow as the CSS is re-parsed but dim is
|
||||
@@ -232,81 +240,10 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
|
||||
private _drawForeground(firstRow: number, lastRow: number): void {
|
||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => {
|
||||
if (cell.isInvisible()) {
|
||||
return;
|
||||
}
|
||||
this._drawChars(cell, x, y);
|
||||
if (cell.isUnderline() || cell.isStrikethrough()) {
|
||||
this._ctx.save();
|
||||
|
||||
if (cell.isInverse()) {
|
||||
if (cell.isBgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
} else if (cell.isBgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getBgColor()).join(',')})`;
|
||||
} else {
|
||||
let bg = cell.getBgColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && bg < 8) {
|
||||
bg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[bg].css;
|
||||
}
|
||||
} else {
|
||||
if (cell.isFgDefault()) {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
} else if (cell.isFgRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getFgColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getFgColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
|
||||
if (cell.isStrikethrough()) {
|
||||
this._fillMiddleLineAtCells(x, y, cell.getWidth());
|
||||
}
|
||||
if (cell.isUnderline()) {
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
this._ctx.fillStyle = `rgb(${AttributeData.toColorRGB(cell.getUnderlineColor()).join(',')})`;
|
||||
} else {
|
||||
let fg = cell.getUnderlineColor();
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
this._ctx.fillStyle = this._colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
switch (cell.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth(), -this._coreBrowserService.dpr);
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth(), this._coreBrowserService.dpr);
|
||||
break;
|
||||
case UnderlineStyle.CURLY:
|
||||
this._curlyUnderlineAtCell(x, y, cell.getWidth());
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
this._dottedUnderlineAtCell(x, y, cell.getWidth());
|
||||
break;
|
||||
case UnderlineStyle.DASHED:
|
||||
this._dashedUnderlineAtCell(x, y, cell.getWidth());
|
||||
break;
|
||||
case UnderlineStyle.SINGLE:
|
||||
default:
|
||||
this._fillBottomLineAtCells(x, y, cell.getWidth());
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._ctx.restore();
|
||||
}
|
||||
});
|
||||
this._forEachCell(firstRow, lastRow, (cell, x, y) => this._drawChars(cell, x, y));
|
||||
}
|
||||
|
||||
public onGridChanged(firstRow: number, lastRow: number): void {
|
||||
public handleGridChanged(firstRow: number, lastRow: number): void {
|
||||
// Resize has not been called yet
|
||||
if (this._state.cache.length === 0) {
|
||||
return;
|
||||
@@ -321,10 +258,6 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._drawForeground(firstRow, lastRow);
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
this._setTransparency(this._optionsService.rawOptions.allowTransparency);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a character is overlapping to the next cell.
|
||||
*/
|
||||
@@ -363,19 +296,4 @@ export class TextRenderLayer extends BaseRenderLayer {
|
||||
this._characterOverlapCache[chars] = overlaps;
|
||||
return overlaps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the charcater at the cell specified.
|
||||
* @param x The column of the char.
|
||||
* @param y The row of the char.
|
||||
*/
|
||||
// private _clearChar(x: number, y: number): void {
|
||||
// let colsToClear = 1;
|
||||
// // Clear the adjacent character if it was wide
|
||||
// const state = this._state.cache[x][y];
|
||||
// if (state && state[CHAR_DATA_WIDTH_INDEX] === 2) {
|
||||
// colsToClear = 2;
|
||||
// }
|
||||
// this.clearCells(x, y, colsToClear, 1);
|
||||
// }
|
||||
}
|
||||
|
||||
+17
-43
@@ -4,24 +4,8 @@
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
// TODO: Use core interfaces
|
||||
export interface IRenderDimensions {
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharLeft: number;
|
||||
scaledCharTop: number;
|
||||
scaledCanvasWidth: number;
|
||||
scaledCanvasHeight: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
actualCellWidth: number;
|
||||
actualCellHeight: number;
|
||||
}
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
export interface IRequestRedrawEvent {
|
||||
start: number;
|
||||
@@ -41,16 +25,14 @@ export interface IRenderer extends IDisposable {
|
||||
*/
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
handleDevicePixelRatioChange(): void;
|
||||
handleResize(cols: number, rows: number): void;
|
||||
handleCharSizeChanged(): void;
|
||||
handleBlur(): void;
|
||||
handleFocus(): void;
|
||||
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleCursorMove(): void;
|
||||
handleOptionsChanged(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
clearTextureAtlas?(): void;
|
||||
@@ -58,42 +40,34 @@ export interface IRenderer extends IDisposable {
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
readonly canvas: HTMLCanvasElement;
|
||||
readonly cacheCanvas: HTMLCanvasElement;
|
||||
|
||||
readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(): void;
|
||||
handleBlur(): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
* Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(): void;
|
||||
handleFocus(): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(colorSet: IColorSet): void;
|
||||
handleCursorMove(): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(startRow: number, endRow: number): void;
|
||||
handleGridChanged(startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Resize the render layer.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IGlyphIdentifier } from './Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
|
||||
export abstract class BaseCharAtlas implements IDisposable {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
public dispose(): void { }
|
||||
|
||||
/**
|
||||
* Perform any work needed to warm the cache before it can be used. May be called multiple times.
|
||||
* Implement _doWarmUp instead if you only want to get called once.
|
||||
*/
|
||||
public warmUp(): void {
|
||||
if (!this._didWarmUp) {
|
||||
this._doWarmUp();
|
||||
this._didWarmUp = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform any work needed to warm the cache before it can be used. Used by the default
|
||||
* implementation of warmUp(), and will only be called once.
|
||||
*/
|
||||
private _doWarmUp(): void { }
|
||||
|
||||
public clear(): void { }
|
||||
|
||||
/**
|
||||
* Called when we start drawing a new frame.
|
||||
*
|
||||
* TODO: We rely on this getting called by TextRenderLayer. This should really be called by
|
||||
* Renderer instead, but we need to make Renderer the source-of-truth for the char atlas, instead
|
||||
* of BaseRenderLayer.
|
||||
*/
|
||||
public beginFrame(): void { }
|
||||
|
||||
/**
|
||||
* May be called before warmUp finishes, however it is okay for the implementation to
|
||||
* do nothing and return false in that case.
|
||||
*
|
||||
* @param ctx Where to draw the character onto.
|
||||
* @param glyph Information about what to draw
|
||||
* @param x The position on the context to start drawing at
|
||||
* @param y The position on the context to start drawing at
|
||||
* @returns The success state. True if we drew the character.
|
||||
*/
|
||||
public abstract draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean;
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
import { BaseCharAtlas } from './BaseCharAtlas';
|
||||
import { DynamicCharAtlas } from './DynamicCharAtlas';
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
atlas: BaseCharAtlas;
|
||||
config: ICharAtlasConfig;
|
||||
// N.B. This implementation potentially holds onto copies of the terminal forever, so
|
||||
// this may cause memory leaks.
|
||||
ownedBy: number[];
|
||||
}
|
||||
|
||||
const charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
*/
|
||||
export function acquireCharAtlas(
|
||||
options: Required<ITerminalOptions>,
|
||||
rendererId: number,
|
||||
colors: IColorSet,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number,
|
||||
devicePixelRatio: number
|
||||
): BaseCharAtlas {
|
||||
const newConfig = generateConfig(scaledCharWidth, scaledCharHeight, options, colors, devicePixelRatio);
|
||||
|
||||
// Check to see if the renderer already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
const ownedByIndex = entry.ownedBy.indexOf(rendererId);
|
||||
if (ownedByIndex >= 0) {
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
return entry.atlas;
|
||||
}
|
||||
// The configs differ, release the renderer from the entry
|
||||
if (entry.ownedBy.length === 1) {
|
||||
entry.atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
entry.ownedBy.splice(ownedByIndex, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Try match a char atlas from the cache
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const entry = charAtlasCache[i];
|
||||
if (configEquals(entry.config, newConfig)) {
|
||||
// Add the renderer to the cache entry and return
|
||||
entry.ownedBy.push(rendererId);
|
||||
return entry.atlas;
|
||||
}
|
||||
}
|
||||
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
atlas: new DynamicCharAtlas(
|
||||
document,
|
||||
newConfig
|
||||
),
|
||||
config: newConfig,
|
||||
ownedBy: [rendererId]
|
||||
};
|
||||
charAtlasCache.push(newEntry);
|
||||
return newEntry.atlas;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a terminal reference from the cache, allowing its memory to be freed.
|
||||
*/
|
||||
export function removeTerminalFromCache(rendererId: number): void {
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
const index = charAtlasCache[i].ownedBy.indexOf(rendererId);
|
||||
if (index !== -1) {
|
||||
if (charAtlasCache[i].ownedBy.length === 1) {
|
||||
// Remove the cache entry if it's the only renderer
|
||||
charAtlasCache[i].atlas.dispose();
|
||||
charAtlasCache.splice(i, 1);
|
||||
} else {
|
||||
// Remove the reference from the cache entry
|
||||
charAtlasCache[i].ownedBy.splice(index, 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DEFAULT_COLOR } from 'common/buffer/Constants';
|
||||
import { IColorSet, IPartialColorSet } from 'browser/Types';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
|
||||
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, options: Required<ITerminalOptions>, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IPartialColorSet = {
|
||||
foreground: colors.foreground,
|
||||
background: colors.background,
|
||||
cursor: undefined,
|
||||
cursorAccent: undefined,
|
||||
selectionBackground: undefined,
|
||||
ansi: colors.ansi.slice()
|
||||
};
|
||||
return {
|
||||
devicePixelRatio,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: options.fontFamily,
|
||||
fontSize: options.fontSize,
|
||||
fontWeight: options.fontWeight,
|
||||
fontWeightBold: options.fontWeightBold,
|
||||
allowTransparency: options.allowTransparency,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
|
||||
export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean {
|
||||
for (let i = 0; i < a.colors.ansi.length; i++) {
|
||||
if (a.colors.ansi[i].rgba !== b.colors.ansi[i].rgba) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return a.devicePixelRatio === b.devicePixelRatio &&
|
||||
a.fontFamily === b.fontFamily &&
|
||||
a.fontSize === b.fontSize &&
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
a.colors.background === b.colors.background;
|
||||
}
|
||||
|
||||
export function is256Color(colorCode: number): boolean {
|
||||
return colorCode < DEFAULT_COLOR;
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants';
|
||||
import { IGlyphIdentifier, ICharAtlasConfig } from './Types';
|
||||
import { BaseCharAtlas } from './BaseCharAtlas';
|
||||
import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
|
||||
import { LRUMap } from './LRUMap';
|
||||
import { isFirefox, isSafari } from 'common/Platform';
|
||||
import { IColor } from 'common/Types';
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { color } from 'common/Color';
|
||||
|
||||
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
|
||||
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
const TRANSPARENT_COLOR = {
|
||||
css: 'rgba(0, 0, 0, 0)',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
// Drawing to the cache is expensive: If we have to draw more than this number of glyphs to the
|
||||
// cache in a single frame, give up on trying to cache anything else, and try to finish the current
|
||||
// frame ASAP.
|
||||
//
|
||||
// This helps to limit the amount of damage a program can do when it would otherwise thrash the
|
||||
// cache.
|
||||
const FRAME_CACHE_DRAW_LIMIT = 100;
|
||||
|
||||
/**
|
||||
* The number of milliseconds to wait before generating the ImageBitmap, this is to debounce/batch
|
||||
* the operation as window.createImageBitmap is asynchronous.
|
||||
*/
|
||||
const GLYPH_BITMAP_COMMIT_DELAY = 100;
|
||||
|
||||
interface IGlyphCacheValue {
|
||||
index: number;
|
||||
isEmpty: boolean;
|
||||
inBitmap: boolean;
|
||||
}
|
||||
|
||||
export function getGlyphCacheKey(glyph: IGlyphIdentifier): number {
|
||||
// Note that this only returns a valid key when code < 256
|
||||
// Layout:
|
||||
// 0b00000000000000000000000000000001: italic (1)
|
||||
// 0b00000000000000000000000000000010: dim (1)
|
||||
// 0b00000000000000000000000000000100: bold (1)
|
||||
// 0b00000000000000000000111111111000: fg (9)
|
||||
// 0b00000000000111111111000000000000: bg (9)
|
||||
// 0b00011111111000000000000000000000: code (8)
|
||||
// 0b11100000000000000000000000000000: unused (3)
|
||||
return glyph.code << 21 | glyph.bg << 12 | glyph.fg << 3 | (glyph.bold ? 0 : 4) + (glyph.dim ? 0 : 2) + (glyph.italic ? 0 : 1);
|
||||
}
|
||||
|
||||
export class DynamicCharAtlas extends BaseCharAtlas {
|
||||
// An ordered map that we're using to keep track of where each glyph is in the atlas texture.
|
||||
// It's ordered so that we can determine when to remove the old entries.
|
||||
private _cacheMap: LRUMap<IGlyphCacheValue>;
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
private _cacheCanvas: HTMLCanvasElement;
|
||||
private _cacheCtx: CanvasRenderingContext2D;
|
||||
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// The number of characters stored in the atlas by width/height
|
||||
private _width: number;
|
||||
private _height: number;
|
||||
|
||||
private _drawToCacheCount: number = 0;
|
||||
|
||||
// An array of glyph keys that are waiting on the bitmap to be generated.
|
||||
private _glyphsWaitingOnBitmap: IGlyphCacheValue[] = [];
|
||||
|
||||
// The timeout that is used to batch bitmap generation so it's not requested for every new glyph.
|
||||
private _bitmapCommitTimeout: number | null = null;
|
||||
|
||||
// The bitmap to draw from, this is much faster on other browsers than others.
|
||||
private _bitmap: ImageBitmap | null = null;
|
||||
|
||||
constructor(document: Document, private _config: ICharAtlasConfig) {
|
||||
super();
|
||||
this._cacheCanvas = document.createElement('canvas');
|
||||
this._cacheCanvas.width = TEXTURE_WIDTH;
|
||||
this._cacheCanvas.height = TEXTURE_HEIGHT;
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this._cacheCanvas.getContext('2d', { alpha: true }));
|
||||
|
||||
const tmpCanvas = document.createElement('canvas');
|
||||
tmpCanvas.width = this._config.scaledCharWidth;
|
||||
tmpCanvas.height = this._config.scaledCharHeight;
|
||||
this._tmpCtx = throwIfFalsy(tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
|
||||
|
||||
this._width = Math.floor(TEXTURE_WIDTH / this._config.scaledCharWidth);
|
||||
this._height = Math.floor(TEXTURE_HEIGHT / this._config.scaledCharHeight);
|
||||
const capacity = this._width * this._height;
|
||||
this._cacheMap = new LRUMap(capacity);
|
||||
this._cacheMap.prealloc(capacity);
|
||||
|
||||
// This is useful for debugging
|
||||
// document.body.appendChild(this._cacheCanvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._bitmapCommitTimeout !== null) {
|
||||
window.clearTimeout(this._bitmapCommitTimeout);
|
||||
this._bitmapCommitTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
public beginFrame(): void {
|
||||
this._drawToCacheCount = 0;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
if (this._cacheMap.size > 0) {
|
||||
const capacity = this._width * this._height;
|
||||
this._cacheMap = new LRUMap(capacity);
|
||||
this._cacheMap.prealloc(capacity);
|
||||
}
|
||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
this._tmpCtx.clearRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
|
||||
}
|
||||
|
||||
public draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean {
|
||||
// Space is always an empty cell, special case this as it's so common
|
||||
if (glyph.code === 32) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Exit early for uncachable glyphs
|
||||
if (!this._canCache(glyph)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const glyphKey = getGlyphCacheKey(glyph);
|
||||
const cacheValue = this._cacheMap.get(glyphKey);
|
||||
if (cacheValue !== null && cacheValue !== undefined) {
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
}
|
||||
if (this._drawToCacheCount < FRAME_CACHE_DRAW_LIMIT) {
|
||||
let index;
|
||||
if (this._cacheMap.size < this._cacheMap.capacity) {
|
||||
index = this._cacheMap.size;
|
||||
} else {
|
||||
// we're out of space, so our call to set will delete this item
|
||||
index = this._cacheMap.peek()!.index;
|
||||
}
|
||||
const cacheValue = this._drawToCache(glyph, index);
|
||||
this._cacheMap.set(glyphKey, cacheValue);
|
||||
this._drawFromCache(ctx, cacheValue, x, y);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private _canCache(glyph: IGlyphIdentifier): boolean {
|
||||
// Only cache ascii and extended characters for now, to be safe. In the future, we could do
|
||||
// something more complicated to determine the expected width of a character.
|
||||
//
|
||||
// If we switch the renderer over to webgl at some point, we may be able to use blending modes
|
||||
// to draw overlapping glyphs from the atlas:
|
||||
// https://github.com/servo/webrender/issues/464#issuecomment-255632875
|
||||
// https://webglfundamentals.org/webgl/lessons/webgl-text-texture.html
|
||||
return glyph.code < 256;
|
||||
}
|
||||
|
||||
private _toCoordinateX(index: number): number {
|
||||
return (index % this._width) * this._config.scaledCharWidth;
|
||||
}
|
||||
|
||||
private _toCoordinateY(index: number): number {
|
||||
return Math.floor(index / this._width) * this._config.scaledCharHeight;
|
||||
}
|
||||
|
||||
private _drawFromCache(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
cacheValue: IGlyphCacheValue,
|
||||
x: number,
|
||||
y: number
|
||||
): void {
|
||||
// We don't actually need to do anything if this is whitespace.
|
||||
if (cacheValue.isEmpty) {
|
||||
return;
|
||||
}
|
||||
const cacheX = this._toCoordinateX(cacheValue.index);
|
||||
const cacheY = this._toCoordinateY(cacheValue.index);
|
||||
ctx.drawImage(
|
||||
cacheValue.inBitmap ? this._bitmap! : this._cacheCanvas,
|
||||
cacheX,
|
||||
cacheY,
|
||||
this._config.scaledCharWidth,
|
||||
this._config.scaledCharHeight,
|
||||
x,
|
||||
y,
|
||||
this._config.scaledCharWidth,
|
||||
this._config.scaledCharHeight
|
||||
);
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
if (idx < this._config.colors.ansi.length) {
|
||||
return this._config.colors.ansi[idx];
|
||||
}
|
||||
return DEFAULT_ANSI_COLORS[idx];
|
||||
}
|
||||
|
||||
private _getBackgroundColor(glyph: IGlyphIdentifier): IColor {
|
||||
if (this._config.allowTransparency) {
|
||||
// The background color might have some transparency, so we need to render it as fully
|
||||
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
|
||||
// around the anti-aliased edges of the glyph, and it would look too dark.
|
||||
return TRANSPARENT_COLOR;
|
||||
}
|
||||
let result: IColor;
|
||||
if (glyph.bg === INVERTED_DEFAULT_COLOR) {
|
||||
result = this._config.colors.foreground;
|
||||
} else if (glyph.bg < 256) {
|
||||
result = this._getColorFromAnsiIndex(glyph.bg);
|
||||
} else {
|
||||
result = this._config.colors.background;
|
||||
}
|
||||
if (glyph.dim) {
|
||||
result = color.blend(this._config.colors.background, color.multiplyOpacity(result, 0.5));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private _getForegroundColor(glyph: IGlyphIdentifier): IColor {
|
||||
if (glyph.fg === INVERTED_DEFAULT_COLOR) {
|
||||
return color.opaque(this._config.colors.background);
|
||||
}
|
||||
if (glyph.fg < 256) {
|
||||
// 256 color support
|
||||
return this._getColorFromAnsiIndex(glyph.fg);
|
||||
}
|
||||
return this._config.colors.foreground;
|
||||
}
|
||||
|
||||
// TODO: We do this (or something similar) in multiple places. We should split this off
|
||||
// into a shared function.
|
||||
private _drawToCache(glyph: IGlyphIdentifier, index: number): IGlyphCacheValue {
|
||||
this._drawToCacheCount++;
|
||||
|
||||
this._tmpCtx.save();
|
||||
|
||||
// draw the background
|
||||
const backgroundColor = this._getBackgroundColor(glyph);
|
||||
// Use a 'copy' composite operation to clear any existing glyph out of _tmpCtxWithAlpha, regardless of
|
||||
// transparency in backgroundColor
|
||||
this._tmpCtx.globalCompositeOperation = 'copy';
|
||||
this._tmpCtx.fillStyle = backgroundColor.css;
|
||||
this._tmpCtx.fillRect(0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight);
|
||||
this._tmpCtx.globalCompositeOperation = 'source-over';
|
||||
|
||||
// draw the foreground/glyph
|
||||
const fontWeight = glyph.bold ? this._config.fontWeightBold : this._config.fontWeight;
|
||||
const fontStyle = glyph.italic ? 'italic' : '';
|
||||
this._tmpCtx.font =
|
||||
`${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`;
|
||||
this._tmpCtx.textBaseline = TEXT_BASELINE;
|
||||
|
||||
this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css;
|
||||
|
||||
// Apply alpha to dim the character
|
||||
if (glyph.dim) {
|
||||
this._tmpCtx.globalAlpha = DIM_OPACITY;
|
||||
}
|
||||
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight);
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
let imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
|
||||
);
|
||||
let isEmpty = false;
|
||||
if (!this._config.allowTransparency) {
|
||||
isEmpty = clearColor(imageData, backgroundColor);
|
||||
}
|
||||
|
||||
// If this charcater is underscore and empty, shift it up until it is visible, try for a maximum
|
||||
// of 5 pixels.
|
||||
if (isEmpty && glyph.chars === '_' && !this._config.allowTransparency) {
|
||||
for (let offset = 1; offset <= 5; offset++) {
|
||||
// Draw the character
|
||||
this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight - offset);
|
||||
|
||||
// clear the background from the character to avoid issues with drawing over the previous
|
||||
// character if it extends past it's bounds
|
||||
imageData = this._tmpCtx.getImageData(
|
||||
0, 0, this._config.scaledCharWidth, this._config.scaledCharHeight
|
||||
);
|
||||
isEmpty = clearColor(imageData, backgroundColor);
|
||||
if (!isEmpty) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._tmpCtx.restore();
|
||||
|
||||
// copy the data from imageData to _cacheCanvas
|
||||
const x = this._toCoordinateX(index);
|
||||
const y = this._toCoordinateY(index);
|
||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||
this._cacheCtx.putImageData(imageData, x, y);
|
||||
|
||||
// Add the glyph and queue it to the bitmap (if the browser supports it)
|
||||
const cacheValue = {
|
||||
index,
|
||||
isEmpty,
|
||||
inBitmap: false
|
||||
};
|
||||
this._addGlyphToBitmap(cacheValue);
|
||||
|
||||
return cacheValue;
|
||||
}
|
||||
|
||||
private _addGlyphToBitmap(cacheValue: IGlyphCacheValue): void {
|
||||
// Support is patchy for createImageBitmap at the moment, pass a canvas back
|
||||
// if support is lacking as drawImage works there too. Firefox is also
|
||||
// included here as ImageBitmap appears both buggy and has horrible
|
||||
// performance (tested on v55).
|
||||
if (!('createImageBitmap' in window) || isFirefox || isSafari) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Add the glyph to the queue
|
||||
this._glyphsWaitingOnBitmap.push(cacheValue);
|
||||
|
||||
// Check if bitmap generation timeout already exists
|
||||
if (this._bitmapCommitTimeout !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._bitmapCommitTimeout = window.setTimeout(() => this._generateBitmap(), GLYPH_BITMAP_COMMIT_DELAY);
|
||||
}
|
||||
|
||||
private _generateBitmap(): void {
|
||||
const glyphsMovingToBitmap = this._glyphsWaitingOnBitmap;
|
||||
this._glyphsWaitingOnBitmap = [];
|
||||
window.createImageBitmap(this._cacheCanvas).then(bitmap => {
|
||||
// Set bitmap
|
||||
this._bitmap = bitmap;
|
||||
|
||||
// Mark all new glyphs as in bitmap, excluding glyphs that came in after
|
||||
// the bitmap was requested
|
||||
for (let i = 0; i < glyphsMovingToBitmap.length; i++) {
|
||||
const value = glyphsMovingToBitmap[i];
|
||||
// It doesn't matter if the value was already evicted, it will be
|
||||
// released from memory after this block if so.
|
||||
value.inBitmap = true;
|
||||
}
|
||||
});
|
||||
this._bitmapCommitTimeout = null;
|
||||
}
|
||||
}
|
||||
|
||||
// This is used for debugging the renderer, just swap out `new DynamicCharAtlas` with
|
||||
// `new NoneCharAtlas`.
|
||||
export class NoneCharAtlas extends BaseCharAtlas {
|
||||
constructor(document: Document, config: ICharAtlasConfig) {
|
||||
super();
|
||||
}
|
||||
|
||||
public draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
glyph: IGlyphIdentifier,
|
||||
x: number,
|
||||
y: number
|
||||
): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes a particular rgb color and colors that are nearly the same in an ImageData completely
|
||||
* transparent.
|
||||
* @returns True if the result is "empty", meaning all pixels are fully transparent.
|
||||
*/
|
||||
function clearColor(imageData: ImageData, color: IColor): boolean {
|
||||
let isEmpty = true;
|
||||
const r = color.rgba >>> 24;
|
||||
const g = color.rgba >>> 16 & 0xFF;
|
||||
const b = color.rgba >>> 8 & 0xFF;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
if (Math.abs(imageData.data[offset] - r) +
|
||||
Math.abs(imageData.data[offset + 1] - g) +
|
||||
Math.abs(imageData.data[offset + 2] - b) < 35) {
|
||||
imageData.data[offset + 3] = 0;
|
||||
} else {
|
||||
isEmpty = false;
|
||||
}
|
||||
}
|
||||
return isEmpty;
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { LRUMap } from './LRUMap';
|
||||
|
||||
describe('LRUMap', () => {
|
||||
it('can be used to store and retrieve values', () => {
|
||||
const map = new LRUMap(10);
|
||||
map.set(1, 'valuea');
|
||||
map.set(2, 'valueb');
|
||||
map.set(3, 'valuec');
|
||||
assert.strictEqual(map.get(1), 'valuea');
|
||||
assert.strictEqual(map.get(2), 'valueb');
|
||||
assert.strictEqual(map.get(3), 'valuec');
|
||||
});
|
||||
|
||||
it('maintains a size from insertions', () => {
|
||||
const map = new LRUMap(10);
|
||||
assert.strictEqual(map.size, 0);
|
||||
map.set(1, 'value');
|
||||
assert.strictEqual(map.size, 1);
|
||||
map.set(2, 'value');
|
||||
assert.strictEqual(map.size, 2);
|
||||
});
|
||||
|
||||
it('deletes the oldest entry when the capacity is exceeded', () => {
|
||||
const map = new LRUMap(4);
|
||||
map.set(1, 'value');
|
||||
map.set(2, 'value');
|
||||
map.set(3, 'value');
|
||||
map.set(4, 'value');
|
||||
map.set(5, 'value');
|
||||
assert.isNull(map.get(1));
|
||||
assert.isNotNull(map.get(2));
|
||||
assert.isNotNull(map.get(3));
|
||||
assert.isNotNull(map.get(4));
|
||||
assert.isNotNull(map.get(5));
|
||||
assert.strictEqual(map.size, 4);
|
||||
});
|
||||
|
||||
it('prevents a recently accessed entry from getting deleted', () => {
|
||||
const map = new LRUMap(2);
|
||||
map.set(1, 'value');
|
||||
map.set(2, 'value');
|
||||
map.get(1);
|
||||
// a would normally get deleted here, except that we called get()
|
||||
map.set(3, 'value');
|
||||
assert.isNotNull(map.get(1));
|
||||
// b got deleted instead of a
|
||||
assert.isNull(map.get(2));
|
||||
assert.isNotNull(map.get(3));
|
||||
});
|
||||
|
||||
it('supports mutation', () => {
|
||||
const map = new LRUMap(10);
|
||||
map.set(1, 'oldvalue');
|
||||
map.set(1, 'newvalue');
|
||||
// mutation doesn't change the size
|
||||
assert.strictEqual(map.size, 1);
|
||||
assert.strictEqual(map.get(1), 'newvalue');
|
||||
});
|
||||
});
|
||||
@@ -1,136 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
interface ILinkedListNode<T> {
|
||||
prev: ILinkedListNode<T> | null;
|
||||
next: ILinkedListNode<T> | null;
|
||||
key: number | null;
|
||||
value: T | null;
|
||||
}
|
||||
|
||||
export class LRUMap<T> {
|
||||
private _map: { [key: number]: ILinkedListNode<T> } = {};
|
||||
private _head: ILinkedListNode<T> | null = null;
|
||||
private _tail: ILinkedListNode<T> | null = null;
|
||||
private _nodePool: ILinkedListNode<T>[] = [];
|
||||
public size: number = 0;
|
||||
|
||||
constructor(public capacity: number) { }
|
||||
|
||||
private _unlinkNode(node: ILinkedListNode<T>): void {
|
||||
const prev = node.prev;
|
||||
const next = node.next;
|
||||
if (node === this._head) {
|
||||
this._head = next;
|
||||
}
|
||||
if (node === this._tail) {
|
||||
this._tail = prev;
|
||||
}
|
||||
if (prev !== null) {
|
||||
prev.next = next;
|
||||
}
|
||||
if (next !== null) {
|
||||
next.prev = prev;
|
||||
}
|
||||
}
|
||||
|
||||
private _appendNode(node: ILinkedListNode<T>): void {
|
||||
const tail = this._tail;
|
||||
if (tail !== null) {
|
||||
tail.next = node;
|
||||
}
|
||||
node.prev = tail;
|
||||
node.next = null;
|
||||
this._tail = node;
|
||||
if (this._head === null) {
|
||||
this._head = node;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preallocate a bunch of linked-list nodes. Allocating these nodes ahead of time means that
|
||||
* they're more likely to live next to each other in memory, which seems to improve performance.
|
||||
*
|
||||
* Each empty object only consumes about 60 bytes of memory, so this is pretty cheap, even for
|
||||
* large maps.
|
||||
*/
|
||||
public prealloc(count: number): void {
|
||||
const nodePool = this._nodePool;
|
||||
for (let i = 0; i < count; i++) {
|
||||
nodePool.push({
|
||||
prev: null,
|
||||
next: null,
|
||||
key: null,
|
||||
value: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public get(key: number): T | null {
|
||||
// This is unsafe: We're assuming our keyspace doesn't overlap with Object.prototype. However,
|
||||
// it's faster than calling hasOwnProperty, and in our case, it would never overlap.
|
||||
const node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
this._unlinkNode(node);
|
||||
this._appendNode(node);
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a value from a key without marking it as the most recently used item.
|
||||
*/
|
||||
public peekValue(key: number): T | null {
|
||||
const node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
return node.value;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public peek(): T | null {
|
||||
const head = this._head;
|
||||
return head === null ? null : head.value;
|
||||
}
|
||||
|
||||
public set(key: number, value: T): void {
|
||||
// This is unsafe: See note above.
|
||||
let node = this._map[key];
|
||||
if (node !== undefined) {
|
||||
// already exists, we just need to mutate it and move it to the end of the list
|
||||
node = this._map[key];
|
||||
this._unlinkNode(node);
|
||||
node.value = value;
|
||||
} else if (this.size >= this.capacity) {
|
||||
// we're out of space: recycle the head node, move it to the tail
|
||||
node = this._head!;
|
||||
this._unlinkNode(node);
|
||||
delete this._map[node.key!];
|
||||
node.key = key;
|
||||
node.value = value;
|
||||
this._map[key] = node;
|
||||
} else {
|
||||
// make a new element
|
||||
const nodePool = this._nodePool;
|
||||
if (nodePool.length > 0) {
|
||||
// use a preallocated node if we can
|
||||
node = nodePool.pop()!;
|
||||
node.key = key;
|
||||
node.value = value;
|
||||
} else {
|
||||
node = {
|
||||
prev: null,
|
||||
next: null,
|
||||
key,
|
||||
value
|
||||
};
|
||||
}
|
||||
this._map[key] = node;
|
||||
this.size++;
|
||||
}
|
||||
this._appendNode(node);
|
||||
}
|
||||
}
|
||||
-29
@@ -1,29 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'common/services/Services';
|
||||
import { IPartialColorSet } from 'browser/Types';
|
||||
|
||||
export interface IGlyphIdentifier {
|
||||
chars: string;
|
||||
code: number;
|
||||
bg: number;
|
||||
fg: number;
|
||||
bold: boolean;
|
||||
dim: boolean;
|
||||
italic: boolean;
|
||||
}
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
devicePixelRatio: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
colors: IPartialColorSet;
|
||||
}
|
||||
@@ -12,6 +12,16 @@ declare module 'xterm-addon-canvas' {
|
||||
export class CanvasAddon implements ITerminalAddon {
|
||||
public textureAtlas?: HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* An event that is fired when the texture atlas of the renderer changes.
|
||||
*/
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
constructor();
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon } from 'xterm';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
interface ITerminalDimensions {
|
||||
/**
|
||||
@@ -58,8 +59,9 @@ export class FitAddon implements ITerminalAddon {
|
||||
|
||||
// TODO: Remove reliance on private API
|
||||
const core = (this._terminal as any)._core;
|
||||
const dims: IRenderDimensions = core._renderService.dimensions;
|
||||
|
||||
if (core._renderService.dimensions.actualCellWidth === 0 || core._renderService.dimensions.actualCellHeight === 0) {
|
||||
if (dims.css.cell.width === 0 || dims.css.cell.height === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -81,8 +83,8 @@ export class FitAddon implements ITerminalAddon {
|
||||
const availableHeight = parentElementHeight - elementPaddingVer;
|
||||
const availableWidth = parentElementWidth - elementPaddingHor - scrollbarWidth;
|
||||
const geometry = {
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / core._renderService.dimensions.actualCellWidth)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / core._renderService.dimensions.actualCellHeight))
|
||||
cols: Math.max(MINIMUM_COLS, Math.floor(availableWidth / dims.css.cell.width)),
|
||||
rows: Math.max(MINIMUM_ROWS, Math.floor(availableHeight / dims.css.cell.height))
|
||||
};
|
||||
return geometry;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,20 @@
|
||||
"strict": true,
|
||||
"types": [
|
||||
"../../../node_modules/@types/mocha"
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"browser/*": [
|
||||
"../../../src/browser/*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
"../../../typings/xterm.d.ts"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../../src/browser"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
export interface ISearchOptions {
|
||||
regex?: boolean;
|
||||
@@ -50,7 +51,7 @@ type LineCacheEntry = [
|
||||
const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\\;:"\',./<>?';
|
||||
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
|
||||
|
||||
export class SearchAddon implements ITerminalAddon {
|
||||
export class SearchAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal: Terminal | undefined;
|
||||
private _cachedSearchTerm: string | undefined;
|
||||
private _selectedDecoration: IDecoration | undefined;
|
||||
@@ -72,13 +73,18 @@ export class SearchAddon implements ITerminalAddon {
|
||||
|
||||
private _resultIndex: number | undefined;
|
||||
|
||||
private readonly _onDidChangeResults = new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>();
|
||||
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number } | undefined>());
|
||||
public readonly onDidChangeResults = this._onDidChangeResults.event;
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
this._terminal = terminal;
|
||||
this._onDataDisposable = this._terminal.onWriteParsed(() => this._updateMatches());
|
||||
this._onResizeDisposable = this._terminal.onResize(() => this._updateMatches());
|
||||
this._onDataDisposable = this.register(this._terminal.onWriteParsed(() => this._updateMatches()));
|
||||
this._onResizeDisposable = this.register(this._terminal.onResize(() => this._updateMatches()));
|
||||
this.register(toDisposable(() => {
|
||||
this.clearDecorations();
|
||||
this._onDataDisposable?.dispose();
|
||||
this._onResizeDisposable?.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateMatches(): void {
|
||||
@@ -94,12 +100,6 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this.clearDecorations();
|
||||
this._onDataDisposable?.dispose();
|
||||
this._onResizeDisposable?.dispose();
|
||||
}
|
||||
|
||||
public clearDecorations(retainCachedSearchTerm?: boolean): void {
|
||||
this._selectedDecoration?.dispose();
|
||||
this._searchResults?.clear();
|
||||
@@ -126,7 +126,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* doesn't exist, do nothing.
|
||||
* @param term The search term.
|
||||
* @param searchOptions Search options.
|
||||
* @return Whether a result was found.
|
||||
* @returns Whether a result was found.
|
||||
*/
|
||||
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
@@ -307,7 +307,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* doesn't exist, do nothing.
|
||||
* @param term The search term.
|
||||
* @param searchOptions Search options.
|
||||
* @return Whether a result was found.
|
||||
* @returns Whether a result was found.
|
||||
*/
|
||||
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
|
||||
if (!this._terminal) {
|
||||
@@ -477,10 +477,10 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
|
||||
* text starts on is searched.
|
||||
* @param term The search term.
|
||||
* @param position The position to start the search.
|
||||
* @param searchPosition The position to start the search.
|
||||
* @param searchOptions Search options.
|
||||
* @param isReverseSearch Whether the search should start from the right side of the terminal and search to the left.
|
||||
* @return The search result if it was found.
|
||||
* @returns The search result if it was found.
|
||||
*/
|
||||
protected _findInLine(term: string, searchPosition: ISearchPosition, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult | undefined {
|
||||
const terminal = this._terminal!;
|
||||
@@ -627,7 +627,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
* Wide characters will count as two columns in the resulting string. This
|
||||
* function is useful for getting the actual text underneath the raw selection
|
||||
* position.
|
||||
* @param line The line being translated.
|
||||
* @param lineIndex The index of the line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
*/
|
||||
private _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): LineCacheEntry {
|
||||
@@ -662,7 +662,7 @@ export class SearchAddon implements ITerminalAddon {
|
||||
/**
|
||||
* Selects and scrolls to a result.
|
||||
* @param result The result to select.
|
||||
* @return Whether a result was selected.
|
||||
* @returns Whether a result was selected.
|
||||
*/
|
||||
private _selectResult(result: ISearchResult | undefined, options?: ISearchDecorationOptions, noScroll?: boolean): boolean {
|
||||
const terminal = this._terminal!;
|
||||
@@ -702,10 +702,10 @@ export class SearchAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies styles to the decoration when it is rendered
|
||||
* @param element the decoration's element
|
||||
* @param backgroundColor the background color to apply
|
||||
* @param borderColor the border color to apply
|
||||
* Applies styles to the decoration when it is rendered.
|
||||
* @param element The decoration's element.
|
||||
* @param borderColor The border color to apply.
|
||||
* @param isActiveResult Whether the element is part of the active search result.
|
||||
* @returns
|
||||
*/
|
||||
private _applyStyles(element: HTMLElement, borderColor: string | undefined, isActiveResult: boolean): void {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-serialize",
|
||||
"version": "0.8.0",
|
||||
"version": "0.9.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -7,9 +7,10 @@ import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { SerializeAddon } from './SerializeAddon';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { SelectionModel } from 'browser/selection/SelectionModel';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
function sgr(...seq: string[]): string {
|
||||
return `\x1b[${seq.join(';')}m`;
|
||||
@@ -44,14 +45,11 @@ class TestSelectionService {
|
||||
}
|
||||
|
||||
describe('xterm-addon-serialize', () => {
|
||||
let cm: ColorManager;
|
||||
let dom: jsdom.JSDOM;
|
||||
let document: Document;
|
||||
let window: jsdom.DOMWindow;
|
||||
|
||||
let serializeAddon: SerializeAddon;
|
||||
let terminal: Terminal;
|
||||
let selectionService: any;
|
||||
|
||||
before(() => {
|
||||
serializeAddon = new SerializeAddon();
|
||||
@@ -60,7 +58,6 @@ describe('xterm-addon-serialize', () => {
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
@@ -77,10 +74,8 @@ describe('xterm-addon-serialize', () => {
|
||||
terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true });
|
||||
terminal.loadAddon(serializeAddon);
|
||||
|
||||
selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
cm = new ColorManager(document, false);
|
||||
(terminal as any)._core._colorManager = cm;
|
||||
(terminal as any)._core._selectionService = selectionService;
|
||||
(terminal as any)._core._themeService = new ThemeService(new OptionsService({}));
|
||||
(terminal as any)._core._selectionService = new TestSelectionService((terminal as any)._core._bufferService);
|
||||
});
|
||||
|
||||
describe('text', () => {
|
||||
|
||||
@@ -544,7 +544,7 @@ export class HTMLSerializeHandler extends BaseSerializeHandler {
|
||||
super(buffer);
|
||||
|
||||
// https://github.com/xtermjs/xterm.js/issues/3601
|
||||
this._colors = (_terminal as any)._core._colorManager.colors;
|
||||
this._colors = (_terminal as any)._core._themeService.colors;
|
||||
}
|
||||
|
||||
private _padStart(target: string, targetLength: number, padString: string): string {
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
*/
|
||||
|
||||
import { IUnicodeVersionProvider } from 'xterm';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
|
||||
type CharWidth = 0 | 1 | 2;
|
||||
|
||||
@@ -198,15 +197,15 @@ export class UnicodeV11 implements IUnicodeVersionProvider {
|
||||
constructor() {
|
||||
if (!table) {
|
||||
table = new Uint8Array(65536);
|
||||
fill(table, 1);
|
||||
table.fill(1);
|
||||
table[0] = 0;
|
||||
fill(table, 0, 1, 32);
|
||||
fill(table, 0, 0x7f, 0xa0);
|
||||
table.fill(0, 1, 32);
|
||||
table.fill(0, 0x7f, 0xa0);
|
||||
for (let r = 0; r < BMP_COMBINING.length; ++r) {
|
||||
fill(table, 0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
|
||||
table.fill(0, BMP_COMBINING[r][0], BMP_COMBINING[r][1] + 1);
|
||||
}
|
||||
for (let r = 0; r < BMP_WIDE.length; ++r) {
|
||||
fill(table, 2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
|
||||
table.fill(2, BMP_WIDE[r][0], BMP_WIDE[r][1] + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-web-links",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange } from 'xterm';
|
||||
import { ILinkProvider, ILink, Terminal, IViewportRange, IBufferLine } from 'xterm';
|
||||
|
||||
export interface ILinkProviderOptions {
|
||||
hover?(event: MouseEvent, text: string, location: IViewportRange): void;
|
||||
@@ -45,55 +45,51 @@ export class LinkComputer {
|
||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
|
||||
|
||||
const [line, startLineIndex] = LinkComputer._translateBufferLineToStringWithWrap(y - 1, false, terminal);
|
||||
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
|
||||
const line = lines.join('');
|
||||
|
||||
let match;
|
||||
let stringIndex = -1;
|
||||
const result: ILink[] = [];
|
||||
|
||||
while ((match = rex.exec(line)) !== null) {
|
||||
const text = match[1];
|
||||
if (!text) {
|
||||
// something matched but does not comply with the given matchIndex
|
||||
// since this is most likely a bug the regex itself we simply do nothing here
|
||||
console.log('match found without corresponding matchIndex');
|
||||
break;
|
||||
while (match = rex.exec(line)) {
|
||||
const text = match[0];
|
||||
|
||||
// check via URL if the matched text would form a proper url
|
||||
// NOTE: This outsources the ugly url parsing to the browser.
|
||||
// To avoid surprising auto expansion from URL we additionally
|
||||
// check afterwards if the provided string resembles the parsed
|
||||
// one close enough:
|
||||
// - decodeURI decode path segement back to byte repr
|
||||
// to detect unicode auto conversion correctly
|
||||
// - append / also match domain urls w'o any path notion
|
||||
try {
|
||||
const url = new URL(text);
|
||||
const urlText = decodeURI(url.toString());
|
||||
if (text !== urlText && text + '/' !== urlText) {
|
||||
continue;
|
||||
}
|
||||
} catch (e) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get index, match.index is for the outer match which includes negated chars
|
||||
// therefore we cannot use match.index directly, instead we search the position
|
||||
// of the match group in text again
|
||||
// also correct regex and string search offsets for the next loop run
|
||||
stringIndex = line.indexOf(text, stringIndex + 1);
|
||||
rex.lastIndex = stringIndex + text.length;
|
||||
if (stringIndex < 0) {
|
||||
// invalid stringIndex (should not have happened)
|
||||
break;
|
||||
}
|
||||
|
||||
let endX = stringIndex + text.length;
|
||||
let endY = startLineIndex + 1;
|
||||
|
||||
while (endX > terminal.cols) {
|
||||
endX -= terminal.cols;
|
||||
endY++;
|
||||
}
|
||||
|
||||
let startX = stringIndex + 1;
|
||||
let startY = startLineIndex + 1;
|
||||
while (startX > terminal.cols) {
|
||||
startX -= terminal.cols;
|
||||
startY++;
|
||||
// map string positions back to buffer positions
|
||||
// values are 0-based right side excluding
|
||||
const [startY, startX] = LinkComputer._mapStrIdx(terminal, startLineIndex, 0, match.index);
|
||||
const [endY, endX] = LinkComputer._mapStrIdx(terminal, startY, startX, text.length);
|
||||
|
||||
if (startY === -1 || startX === -1 || endY === -1 || endX === -1) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// range expects values 1-based right side including, thus +1 except for endX
|
||||
const range = {
|
||||
start: {
|
||||
x: startX,
|
||||
y: startY
|
||||
x: startX + 1,
|
||||
y: startY + 1
|
||||
},
|
||||
end: {
|
||||
x: endX,
|
||||
y: endY
|
||||
y: endY + 1
|
||||
}
|
||||
};
|
||||
|
||||
@@ -104,42 +100,99 @@ export class LinkComputer {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the entire line for the buffer line
|
||||
* @param line The line being translated.
|
||||
* @param trimRight Whether to trim whitespace to the right.
|
||||
* @param terminal The terminal
|
||||
* Get wrapped content lines for the current line index.
|
||||
* The top/bottom line expansion stops at whitespaces or length > 2048.
|
||||
* Returns an array with line strings and the top line index.
|
||||
*
|
||||
* NOTE: We pull line strings with trimRight=true on purpose to make sure
|
||||
* to correctly match urls with early wrapped wide chars. This corrupts the string index
|
||||
* for 1:1 backmapping to buffer positions, thus needs an additional correction in _mapStrIdx.
|
||||
*/
|
||||
private static _translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean, terminal: Terminal): [string, number] {
|
||||
let lineString = '';
|
||||
let lineWrapsToNext: boolean;
|
||||
let prevLinesToWrap: boolean;
|
||||
private static _getWindowedLineStrings(lineIndex: number, terminal: Terminal): [string[], number] {
|
||||
let line: IBufferLine | undefined;
|
||||
let topIdx = lineIndex;
|
||||
let bottomIdx = lineIndex;
|
||||
let length = 0;
|
||||
let content = '';
|
||||
const lines: string[] = [];
|
||||
|
||||
do {
|
||||
const line = terminal.buffer.active.getLine(lineIndex);
|
||||
if ((line = terminal.buffer.active.getLine(lineIndex))) {
|
||||
const currentContent = line.translateToString(true);
|
||||
|
||||
// expand top, stop on whitespaces or length > 2048
|
||||
if (line.isWrapped && currentContent[0] !== ' ') {
|
||||
length = 0;
|
||||
while ((line = terminal.buffer.active.getLine(--topIdx)) && length < 2048) {
|
||||
content = line.translateToString(true);
|
||||
length += content.length;
|
||||
lines.push(content);
|
||||
if (!line.isWrapped || content.indexOf(' ') !== -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
lines.reverse();
|
||||
}
|
||||
|
||||
// append current line
|
||||
lines.push(currentContent);
|
||||
|
||||
// expand bottom, stop on whitespaces or length > 2048
|
||||
length = 0;
|
||||
while ((line = terminal.buffer.active.getLine(++bottomIdx)) && line.isWrapped && length < 2048) {
|
||||
content = line.translateToString(true);
|
||||
length += content.length;
|
||||
lines.push(content);
|
||||
if (content.indexOf(' ') !== -1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [lines, topIdx];
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a string index back to buffer positions.
|
||||
* Returns buffer position as [lineIndex, columnIndex] 0-based,
|
||||
* or [-1, -1] in case the lookup ran into a non-existing line.
|
||||
*/
|
||||
private static _mapStrIdx(terminal: Terminal, lineIndex: number, rowIndex: number, stringIndex: number): [number, number] {
|
||||
const buf = terminal.buffer.active;
|
||||
const cell = buf.getNullCell();
|
||||
let start = rowIndex;
|
||||
while (stringIndex) {
|
||||
const line = buf.getLine(lineIndex);
|
||||
if (!line) {
|
||||
break;
|
||||
return [-1, -1];
|
||||
}
|
||||
for (let i = start; i < line.length; ++i) {
|
||||
line.getCell(i, cell);
|
||||
const chars = cell.getChars();
|
||||
const width = cell.getWidth();
|
||||
if (width) {
|
||||
stringIndex -= chars.length || 1;
|
||||
|
||||
if (line.isWrapped) {
|
||||
lineIndex--;
|
||||
// correct stringIndex for early wrapped wide chars:
|
||||
// - currently only happens at last cell
|
||||
// - cells to the right are reset with chars='' and width=1 in InputHandler.print
|
||||
// - follow-up line must be wrapped and contain wide char at first cell
|
||||
// --> if all these conditions are met, correct stringIndex by +1
|
||||
if (i === line.length - 1 && chars === '') {
|
||||
const line = buf.getLine(lineIndex + 1);
|
||||
if (line && line.isWrapped) {
|
||||
line.getCell(0, cell);
|
||||
if (cell.getWidth() === 2) {
|
||||
stringIndex += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (stringIndex < 0) {
|
||||
return [lineIndex, i];
|
||||
}
|
||||
}
|
||||
|
||||
prevLinesToWrap = line.isWrapped;
|
||||
} while (prevLinesToWrap);
|
||||
|
||||
const startLineIndex = lineIndex;
|
||||
|
||||
do {
|
||||
const nextLine = terminal.buffer.active.getLine(lineIndex + 1);
|
||||
lineWrapsToNext = nextLine ? nextLine.isWrapped : false;
|
||||
const line = terminal.buffer.active.getLine(lineIndex);
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
lineString += line.translateToString(!lineWrapsToNext && trimRight).substring(0, terminal.cols);
|
||||
lineIndex++;
|
||||
} while (lineWrapsToNext);
|
||||
|
||||
return [lineString, startLineIndex];
|
||||
start = 0;
|
||||
}
|
||||
return [lineIndex, start];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,25 +6,19 @@
|
||||
import { Terminal, ITerminalAddon, IDisposable } from 'xterm';
|
||||
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
||||
|
||||
const protocolClause = '(https?:\\/\\/)';
|
||||
const domainCharacterSet = '[\\da-z\\.-]+';
|
||||
const negatedDomainCharacterSet = '[^\\da-z\\.-]+';
|
||||
const domainBodyClause = '(' + domainCharacterSet + ')';
|
||||
const tldClause = '([a-z\\.]{2,18})';
|
||||
const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})';
|
||||
const localHostClause = '(localhost)';
|
||||
const portClause = '(:\\d{1,5})';
|
||||
const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?';
|
||||
const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+@]*)*([^:"\'\\s])';
|
||||
const pathClause = '(' + pathCharacterSet + ')?';
|
||||
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
|
||||
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
|
||||
const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+';
|
||||
const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause;
|
||||
const start = '(?:^|' + negatedDomainCharacterSet + ')(';
|
||||
const end = ')($|' + negatedPathCharacterSet + ')';
|
||||
const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end);
|
||||
// consider everthing starting with http:// or https://
|
||||
// up to first whitespace, `"` or `'` as url
|
||||
// NOTE: The repeated end clause is needed to not match a dangling `:`
|
||||
// resembling the old (...)*([^:"\'\\s]) final path clause
|
||||
// additionally exclude early + final:
|
||||
// - unsafe from rfc3986: !*'()
|
||||
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
|
||||
// also exclude as finals:
|
||||
// - final interpunction like ,.!?
|
||||
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
||||
// - unsafe chars from rfc1738: {}|\^~[]`
|
||||
const strictUrlRegex = /https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
|
||||
|
||||
|
||||
function handleLink(event: MouseEvent, uri: string): void {
|
||||
const newWindow = window.open();
|
||||
|
||||
@@ -14,6 +14,22 @@ let page: Page;
|
||||
const width = 800;
|
||||
const height = 600;
|
||||
|
||||
|
||||
interface ILinkStateData {
|
||||
uri?: string;
|
||||
range?: {
|
||||
start: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
end: {
|
||||
x: number;
|
||||
y: number;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
describe('WebLinksAddon', () => {
|
||||
before(async function(): Promise<any> {
|
||||
browser = await launchBrowser();
|
||||
@@ -35,6 +51,43 @@ describe('WebLinksAddon', () => {
|
||||
it('.io', async function(): Promise<any> {
|
||||
await testHostName('foo.io');
|
||||
});
|
||||
|
||||
describe('correct buffer offsets & uri', () => {
|
||||
it('all half width', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, 'aaa http://example.com aaa http://example.com aaa');
|
||||
await resetAndHover(5, 1);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 5, y: 1 }, end: { x: 22, y: 1 } });
|
||||
await resetAndHover(1, 2);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 28, y: 1 }, end: { x: 5, y: 2 } });
|
||||
});
|
||||
it('url after full width', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥ http://example.com ¥¥¥ http://example.com aaa');
|
||||
await resetAndHover(8, 1);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 8, y: 1 }, end: { x: 25, y: 1 } });
|
||||
await resetAndHover(1, 2);
|
||||
await evalLinkStateData('http://example.com', { start: { x: 34, y: 1 }, end: { x: 11, y: 2 } });
|
||||
});
|
||||
it('full width within url and before', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥');
|
||||
await resetAndHover(8, 1);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
|
||||
await resetAndHover(1, 2);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 8, y: 1 }, end: { x: 11, y: 2 } });
|
||||
await resetAndHover(17, 2);
|
||||
await evalLinkStateData('https://ko.wikipedia.org/wiki/위키백과:대문', { start: { x: 17, y: 2 }, end: { x: 19, y: 3 } });
|
||||
});
|
||||
it('name + password url after full width and combining', async () => {
|
||||
setupCustom();
|
||||
await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path');
|
||||
await resetAndHover(12, 1);
|
||||
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
|
||||
await resetAndHover(13, 2);
|
||||
await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function testHostName(hostname: string): Promise<void> {
|
||||
@@ -64,3 +117,23 @@ async function pollForLinkAtCell(col: number, row: number, value: string): Promi
|
||||
await pollFor(page, `document.querySelectorAll('${rowSelector} > span[style]').length >= ${value.length}`, true, async () => page.hover(`${rowSelector} > :nth-child(${col})`));
|
||||
assert.equal(await page.evaluate(`Array.prototype.reduce.call(document.querySelectorAll('${rowSelector} > span[style]'), (a, b) => a + b.textContent, '');`), value);
|
||||
}
|
||||
|
||||
async function setupCustom(): Promise<void> {
|
||||
await openTerminal(page, { cols: 40 });
|
||||
await page.evaluate(`window._linkStateData = {};
|
||||
window._linkaddon = new window.WebLinksAddon();
|
||||
window._linkaddon._options.hover = (event, uri, range) => { window._linkStateData = { uri, range }; };
|
||||
window.term.loadAddon(window._linkaddon);`);
|
||||
}
|
||||
|
||||
async function resetAndHover(col: number, row: number): Promise<void> {
|
||||
await page.evaluate(`window._linkStateData = {};`);
|
||||
const rowSelector = `.xterm-rows > :nth-child(${row})`;
|
||||
await page.hover(`${rowSelector} > :nth-child(${col})`);
|
||||
}
|
||||
|
||||
async function evalLinkStateData(uri: string, range: any): Promise<void> {
|
||||
const data: ILinkStateData = await page.evaluate(`window._linkStateData`);
|
||||
assert.equal(data.uri, uri);
|
||||
assert.deepEqual(data.range, range);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "xterm-addon-webgl",
|
||||
"version": "0.13.0",
|
||||
"version": "0.14.0",
|
||||
"author": {
|
||||
"name": "The xterm.js authors",
|
||||
"url": "https://xtermjs.org/"
|
||||
|
||||
@@ -3,15 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel } from './Types';
|
||||
import { NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
|
||||
interface IVertices {
|
||||
attributes: Float32Array;
|
||||
@@ -30,8 +29,9 @@ const enum VertexAttribLocations {
|
||||
CELL_POSITION = 1,
|
||||
OFFSET = 2,
|
||||
SIZE = 3,
|
||||
TEXCOORD = 4,
|
||||
TEXSIZE = 5
|
||||
TEXPAGE = 4,
|
||||
TEXCOORD = 5,
|
||||
TEXSIZE = 6
|
||||
}
|
||||
|
||||
const vertexShaderSource = `#version 300 es
|
||||
@@ -39,6 +39,7 @@ layout (location = ${VertexAttribLocations.UNIT_QUAD}) in vec2 a_unitquad;
|
||||
layout (location = ${VertexAttribLocations.CELL_POSITION}) in vec2 a_cellpos;
|
||||
layout (location = ${VertexAttribLocations.OFFSET}) in vec2 a_offset;
|
||||
layout (location = ${VertexAttribLocations.SIZE}) in vec2 a_size;
|
||||
layout (location = ${VertexAttribLocations.TEXPAGE}) in float a_texpage;
|
||||
layout (location = ${VertexAttribLocations.TEXCOORD}) in vec2 a_texcoord;
|
||||
layout (location = ${VertexAttribLocations.TEXSIZE}) in vec2 a_texsize;
|
||||
|
||||
@@ -46,51 +47,59 @@ uniform mat4 u_projection;
|
||||
uniform vec2 u_resolution;
|
||||
|
||||
out vec2 v_texcoord;
|
||||
flat out int v_texpage;
|
||||
|
||||
void main() {
|
||||
vec2 zeroToOne = (a_offset / u_resolution) + a_cellpos + (a_unitquad * a_size);
|
||||
gl_Position = u_projection * vec4(zeroToOne, 0.0, 1.0);
|
||||
v_texpage = int(a_texpage);
|
||||
v_texcoord = a_texcoord + a_unitquad * a_texsize;
|
||||
}`;
|
||||
|
||||
const fragmentShaderSource = `#version 300 es
|
||||
function createFragmentShaderSource(maxFragmentShaderTextureUnits: number): string {
|
||||
let textureConditionals = '';
|
||||
for (let i = 1; i < maxFragmentShaderTextureUnits; i++) {
|
||||
textureConditionals += ` else if (v_texpage == ${i}) { outColor = texture(u_texture[${i}], v_texcoord); }`;
|
||||
}
|
||||
return (`#version 300 es
|
||||
precision lowp float;
|
||||
|
||||
in vec2 v_texcoord;
|
||||
flat in int v_texpage;
|
||||
|
||||
uniform sampler2D u_texture;
|
||||
uniform sampler2D u_texture[${maxFragmentShaderTextureUnits}];
|
||||
|
||||
out vec4 outColor;
|
||||
|
||||
void main() {
|
||||
outColor = texture(u_texture, v_texcoord);
|
||||
}`;
|
||||
if (v_texpage == 0) {
|
||||
outColor = texture(u_texture[0], v_texcoord);
|
||||
} ${textureConditionals}
|
||||
}`);
|
||||
}
|
||||
|
||||
const INDICES_PER_CELL = 10;
|
||||
const INDICES_PER_CELL = 11;
|
||||
const BYTES_PER_CELL = INDICES_PER_CELL * Float32Array.BYTES_PER_ELEMENT;
|
||||
const CELL_POSITION_INDICES = 2;
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { i: number, glyph: IRasterizedGlyph | undefined, leftCellPadding: number, clippedPixels: number } = {
|
||||
i: 0,
|
||||
glyph: undefined,
|
||||
leftCellPadding: 0,
|
||||
clippedPixels: 0
|
||||
};
|
||||
// Work variables to avoid garbage collection
|
||||
let $i = 0;
|
||||
let $glyph: IRasterizedGlyph | undefined = undefined;
|
||||
let $leftCellPadding = 0;
|
||||
let $clippedPixels = 0;
|
||||
|
||||
export class GlyphRenderer extends Disposable {
|
||||
private _atlas: WebglCharAtlas | undefined;
|
||||
export class GlyphRenderer extends Disposable {
|
||||
private readonly _program: WebGLProgram;
|
||||
private readonly _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private readonly _projectionLocation: WebGLUniformLocation;
|
||||
private readonly _resolutionLocation: WebGLUniformLocation;
|
||||
private readonly _textureLocation: WebGLUniformLocation;
|
||||
private readonly _atlasTextures: GLTexture[];
|
||||
private readonly _attributesBuffer: WebGLBuffer;
|
||||
|
||||
private _program: WebGLProgram;
|
||||
private _vertexArrayObject: IWebGLVertexArrayObject;
|
||||
private _projectionLocation: WebGLUniformLocation;
|
||||
private _resolutionLocation: WebGLUniformLocation;
|
||||
private _textureLocation: WebGLUniformLocation;
|
||||
private _atlasTexture: WebGLTexture;
|
||||
private _attributesBuffer: WebGLBuffer;
|
||||
private _atlas: ITextureAtlas | undefined;
|
||||
private _activeBuffer: number = 0;
|
||||
|
||||
private _vertices: IVertices = {
|
||||
private readonly _vertices: IVertices = {
|
||||
count: 0,
|
||||
attributes: new Float32Array(0),
|
||||
attributesBuffers: [
|
||||
@@ -100,15 +109,22 @@ export class GlyphRenderer extends Disposable {
|
||||
};
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
) {
|
||||
super();
|
||||
|
||||
const gl = this._gl;
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, fragmentShaderSource));
|
||||
|
||||
if (TextureAtlas.maxAtlasPages === undefined) {
|
||||
// Typically 8 or 16
|
||||
TextureAtlas.maxAtlasPages = Math.min(32, throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_IMAGE_UNITS) as number | null));
|
||||
// Almost all clients will support >= 4096
|
||||
TextureAtlas.maxTextureSize = throwIfFalsy(gl.getParameter(gl.MAX_TEXTURE_SIZE) as number | null);
|
||||
}
|
||||
|
||||
this._program = throwIfFalsy(createProgram(gl, vertexShaderSource, createFragmentShaderSource(TextureAtlas.maxAtlasPages)));
|
||||
this.register(toDisposable(() => gl.deleteProgram(this._program)));
|
||||
|
||||
// Uniform locations
|
||||
@@ -130,8 +146,9 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
|
||||
// triangle strip
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
@@ -147,30 +164,48 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.SIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.SIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 2 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.SIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXPAGE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXPAGE, 1, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXPAGE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXCOORD);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXCOORD, 2, gl.FLOAT, false, BYTES_PER_CELL, 5 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXCOORD, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.TEXSIZE);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 6 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.TEXSIZE, 2, gl.FLOAT, false, BYTES_PER_CELL, 7 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.TEXSIZE, 1);
|
||||
gl.enableVertexAttribArray(VertexAttribLocations.CELL_POSITION);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 8 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribPointer(VertexAttribLocations.CELL_POSITION, 2, gl.FLOAT, false, BYTES_PER_CELL, 9 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.CELL_POSITION, 1);
|
||||
|
||||
// Setup empty texture atlas
|
||||
this._atlasTexture = throwIfFalsy(gl.createTexture());
|
||||
this.register(toDisposable(() => gl.deleteTexture(this._atlasTexture)));
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 255, 255]));
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
// Setup static uniforms
|
||||
gl.useProgram(this._program);
|
||||
const textureUnits = new Int32Array(TextureAtlas.maxAtlasPages);
|
||||
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
|
||||
textureUnits[i] = i;
|
||||
}
|
||||
gl.uniform1iv(this._textureLocation, textureUnits);
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
|
||||
// Setup 1x1 red pixel textures for all potential atlas pages, if one of these invalid textures
|
||||
// is ever drawn it will show characters as red rectangles.
|
||||
this._atlasTextures = [];
|
||||
for (let i = 0; i < TextureAtlas.maxAtlasPages; i++) {
|
||||
const glTexture = new GLTexture(throwIfFalsy(gl.createTexture()));
|
||||
this.register(toDisposable(() => gl.deleteTexture(glTexture.texture)));
|
||||
gl.activeTexture(gl.TEXTURE0 + i);
|
||||
gl.bindTexture(gl.TEXTURE_2D, glTexture.texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([255, 0, 0, 255]));
|
||||
this._atlasTextures[i] = glTexture;
|
||||
}
|
||||
|
||||
// Allow drawing of transparent texture
|
||||
gl.enable(gl.BLEND);
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
|
||||
|
||||
// Set viewport
|
||||
this.onResize();
|
||||
this.handleResize();
|
||||
}
|
||||
|
||||
public beginFrame(): boolean {
|
||||
@@ -186,12 +221,12 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void {
|
||||
w.i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
|
||||
$i = (y * this._terminal.cols + x) * INDICES_PER_CELL;
|
||||
|
||||
// Exit early if this is a null character, allow space character to continue as it may have
|
||||
// underline/strikethrough styles
|
||||
if (code === NULL_CELL_CODE || code === undefined/* This is used for the right side of wide chars */) {
|
||||
fill(array, 0, w.i, w.i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
array.fill(0, $i, $i + INDICES_PER_CELL - 1 - CELL_POSITION_INDICES);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -201,39 +236,43 @@ export class GlyphRenderer extends Disposable {
|
||||
|
||||
// Get the glyph
|
||||
if (chars && chars.length > 1) {
|
||||
w.glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
|
||||
$glyph = this._atlas.getRasterizedGlyphCombinedChar(chars, bg, fg, ext);
|
||||
} else {
|
||||
w.glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
|
||||
$glyph = this._atlas.getRasterizedGlyph(code, bg, fg, ext);
|
||||
}
|
||||
|
||||
w.leftCellPadding = Math.floor((this._dimensions.scaledCellWidth - this._dimensions.scaledCharWidth) / 2);
|
||||
if (bg !== lastBg && w.glyph.offset.x > w.leftCellPadding) {
|
||||
w.clippedPixels = w.glyph.offset.x - w.leftCellPadding;
|
||||
$leftCellPadding = Math.floor((this._dimensions.device.cell.width - this._dimensions.device.char.width) / 2);
|
||||
if (bg !== lastBg && $glyph.offset.x > $leftCellPadding) {
|
||||
$clippedPixels = $glyph.offset.x - $leftCellPadding;
|
||||
// a_origin
|
||||
array[w.i ] = -(w.glyph.offset.x - w.clippedPixels) + this._dimensions.scaledCharLeft;
|
||||
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
|
||||
array[$i ] = -($glyph.offset.x - $clippedPixels) + this._dimensions.device.char.left;
|
||||
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
|
||||
// a_size
|
||||
array[w.i + 2] = (w.glyph.size.x - w.clippedPixels) / this._dimensions.scaledCanvasWidth;
|
||||
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
array[$i + 2] = ($glyph.size.x - $clippedPixels) / this._dimensions.device.canvas.width;
|
||||
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
|
||||
// a_texpage
|
||||
array[$i + 4] = $glyph.texturePage;
|
||||
// a_texcoord
|
||||
array[w.i + 4] = w.glyph.texturePositionClipSpace.x + w.clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.x + $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
|
||||
array[$i + 6] = $glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[w.i + 6] = w.glyph.sizeClipSpace.x - w.clippedPixels / this._atlas.cacheCanvas.width;
|
||||
array[w.i + 7] = w.glyph.sizeClipSpace.y;
|
||||
array[$i + 7] = $glyph.sizeClipSpace.x - $clippedPixels / this._atlas.pages[$glyph.texturePage].canvas.width;
|
||||
array[$i + 8] = $glyph.sizeClipSpace.y;
|
||||
} else {
|
||||
// a_origin
|
||||
array[w.i ] = -w.glyph.offset.x + this._dimensions.scaledCharLeft;
|
||||
array[w.i + 1] = -w.glyph.offset.y + this._dimensions.scaledCharTop;
|
||||
array[$i ] = -$glyph.offset.x + this._dimensions.device.char.left;
|
||||
array[$i + 1] = -$glyph.offset.y + this._dimensions.device.char.top;
|
||||
// a_size
|
||||
array[w.i + 2] = w.glyph.size.x / this._dimensions.scaledCanvasWidth;
|
||||
array[w.i + 3] = w.glyph.size.y / this._dimensions.scaledCanvasHeight;
|
||||
array[$i + 2] = $glyph.size.x / this._dimensions.device.canvas.width;
|
||||
array[$i + 3] = $glyph.size.y / this._dimensions.device.canvas.height;
|
||||
// a_texpage
|
||||
array[$i + 4] = $glyph.texturePage;
|
||||
// a_texcoord
|
||||
array[w.i + 4] = w.glyph.texturePositionClipSpace.x;
|
||||
array[w.i + 5] = w.glyph.texturePositionClipSpace.y;
|
||||
array[$i + 5] = $glyph.texturePositionClipSpace.x;
|
||||
array[$i + 6] = $glyph.texturePositionClipSpace.y;
|
||||
// a_texsize
|
||||
array[w.i + 6] = w.glyph.sizeClipSpace.x;
|
||||
array[w.i + 7] = w.glyph.sizeClipSpace.y;
|
||||
array[$i + 7] = $glyph.sizeClipSpace.x;
|
||||
array[$i + 8] = $glyph.sizeClipSpace.y;
|
||||
}
|
||||
// a_cellpos only changes on resize
|
||||
}
|
||||
@@ -248,7 +287,8 @@ export class GlyphRenderer extends Disposable {
|
||||
} else {
|
||||
this._vertices.attributes.fill(0);
|
||||
}
|
||||
for (let i = 0; i < this._vertices.attributesBuffers.length; i++) {
|
||||
let i = 0;
|
||||
for (; i < this._vertices.attributesBuffers.length; i++) {
|
||||
if (this._vertices.count !== newCount) {
|
||||
this._vertices.attributesBuffers[i] = new Float32Array(newCount);
|
||||
} else {
|
||||
@@ -256,19 +296,20 @@ export class GlyphRenderer extends Disposable {
|
||||
}
|
||||
}
|
||||
this._vertices.count = newCount;
|
||||
let i = 0;
|
||||
i = 0;
|
||||
for (let y = 0; y < terminal.rows; y++) {
|
||||
for (let x = 0; x < terminal.cols; x++) {
|
||||
this._vertices.attributes[i + 8] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 9] = y / terminal.rows;
|
||||
this._vertices.attributes[i + 9] = x / terminal.cols;
|
||||
this._vertices.attributes[i + 10] = y / terminal.rows;
|
||||
i += INDICES_PER_CELL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
public handleResize(): void {
|
||||
const gl = this._gl;
|
||||
gl.viewport(0, 0, gl.canvas.width, gl.canvas.height);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
this.clear();
|
||||
}
|
||||
|
||||
@@ -305,31 +346,32 @@ export class GlyphRenderer extends Disposable {
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, activeBuffer.subarray(0, bufferLength), gl.STREAM_DRAW);
|
||||
|
||||
// Bind the texture atlas if it's changed
|
||||
if (this._atlas.hasCanvasChanged) {
|
||||
this._atlas.hasCanvasChanged = false;
|
||||
gl.uniform1i(this._textureLocation, 0);
|
||||
gl.activeTexture(gl.TEXTURE0 + 0);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this._atlas.cacheCanvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
// Bind the atlas page texture if they have changed
|
||||
for (let i = 0; i < this._atlas.pages.length; i++) {
|
||||
if (this._atlas.pages[i].version !== this._atlasTextures[i].version) {
|
||||
this._bindAtlasPageTexture(gl, this._atlas, i);
|
||||
}
|
||||
}
|
||||
|
||||
// Set uniforms
|
||||
gl.uniformMatrix4fv(this._projectionLocation, false, PROJECTION_MATRIX);
|
||||
gl.uniform2f(this._resolutionLocation, gl.canvas.width, gl.canvas.height);
|
||||
|
||||
// Draw the viewport
|
||||
gl.drawElementsInstanced(gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
gl.drawElementsInstanced(gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, bufferLength / INDICES_PER_CELL);
|
||||
}
|
||||
|
||||
public setAtlas(atlas: WebglCharAtlas): void {
|
||||
const gl = this._gl;
|
||||
public setAtlas(atlas: ITextureAtlas): void {
|
||||
this._atlas = atlas;
|
||||
for (const glTexture of this._atlasTextures) {
|
||||
glTexture.version = -1;
|
||||
}
|
||||
}
|
||||
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTexture);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.cacheCanvas);
|
||||
private _bindAtlasPageTexture(gl: IWebGL2RenderingContext, atlas: ITextureAtlas, i: number): void {
|
||||
gl.activeTexture(gl.TEXTURE0 + i);
|
||||
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
||||
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
||||
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
|
||||
gl.generateMipmap(gl.TEXTURE_2D);
|
||||
this._atlasTextures[i].version = atlas.pages[i].version;
|
||||
}
|
||||
|
||||
public setDimensions(dimensions: IRenderDimensions): void {
|
||||
|
||||
@@ -3,16 +3,18 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX, throwIfFalsy } from './WebglUtils';
|
||||
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
|
||||
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext } from './Types';
|
||||
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { DIM_OPACITY } from 'browser/renderer/Constants';
|
||||
import { DIM_OPACITY } from 'browser/renderer/shared/Constants';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IThemeService } from 'browser/services/Services';
|
||||
|
||||
const enum VertexAttribLocations {
|
||||
POSITION = 0,
|
||||
@@ -58,17 +60,15 @@ const BYTES_PER_RECTANGLE = INDICES_PER_RECTANGLE * Float32Array.BYTES_PER_ELEME
|
||||
|
||||
const INITIAL_BUFFER_RECTANGLE_CAPACITY = 20 * INDICES_PER_RECTANGLE;
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { rgba: number, isDefault: boolean, x1: number, y1: number, r: number, g: number, b: number, a: number } = {
|
||||
rgba: 0,
|
||||
isDefault: false,
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
r: 0,
|
||||
g: 0,
|
||||
b: 0,
|
||||
a: 0
|
||||
};
|
||||
// Work variables to avoid garbage collection
|
||||
let $rgba = 0;
|
||||
let $isDefault = false;
|
||||
let $x1 = 0;
|
||||
let $y1 = 0;
|
||||
let $r = 0;
|
||||
let $g = 0;
|
||||
let $b = 0;
|
||||
let $a = 0;
|
||||
|
||||
export class RectangleRenderer extends Disposable {
|
||||
|
||||
@@ -85,9 +85,9 @@ export class RectangleRenderer extends Disposable {
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private _gl: IWebGL2RenderingContext,
|
||||
private _dimensions: IRenderDimensions
|
||||
private _dimensions: IRenderDimensions,
|
||||
private readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -113,8 +113,9 @@ export class RectangleRenderer extends Disposable {
|
||||
gl.vertexAttribPointer(VertexAttribLocations.UNIT_QUAD, 2, this._gl.FLOAT, false, 0, 0);
|
||||
|
||||
// Setup the unit quad element array buffer, this points to indices in
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 3, 0, 2, 3]);
|
||||
// unitQuadVertices to allow is to draw 2 triangles from the vertices via a
|
||||
// triangle strip
|
||||
const unitQuadElementIndices = new Uint8Array([0, 1, 2, 3]);
|
||||
const elementIndicesBuffer = gl.createBuffer();
|
||||
this.register(toDisposable(() => gl.deleteBuffer(elementIndicesBuffer)));
|
||||
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, elementIndicesBuffer);
|
||||
@@ -134,7 +135,11 @@ export class RectangleRenderer extends Disposable {
|
||||
gl.vertexAttribPointer(VertexAttribLocations.COLOR, 4, gl.FLOAT, false, BYTES_PER_RECTANGLE, 4 * Float32Array.BYTES_PER_ELEMENT);
|
||||
gl.vertexAttribDivisor(VertexAttribLocations.COLOR, 1);
|
||||
|
||||
this._updateCachedColors();
|
||||
this._updateCachedColors(_themeService.colors);
|
||||
this.register(this._themeService.onChangeColors(e => {
|
||||
this._updateCachedColors(e);
|
||||
this._updateViewportRectangle();
|
||||
}));
|
||||
}
|
||||
|
||||
public render(): void {
|
||||
@@ -149,15 +154,10 @@ export class RectangleRenderer extends Disposable {
|
||||
// Bind attributes buffer and draw
|
||||
gl.bindBuffer(gl.ARRAY_BUFFER, this._attributesBuffer);
|
||||
gl.bufferData(gl.ARRAY_BUFFER, this._vertices.attributes, gl.DYNAMIC_DRAW);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLES, 6, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
gl.drawElementsInstanced(this._gl.TRIANGLE_STRIP, 4, gl.UNSIGNED_BYTE, 0, this._vertices.count);
|
||||
}
|
||||
|
||||
public onResize(): void {
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
public setColors(): void {
|
||||
this._updateCachedColors();
|
||||
public handleResize(): void {
|
||||
this._updateViewportRectangle();
|
||||
}
|
||||
|
||||
@@ -165,8 +165,8 @@ export class RectangleRenderer extends Disposable {
|
||||
this._dimensions = dimensions;
|
||||
}
|
||||
|
||||
private _updateCachedColors(): void {
|
||||
this._bgFloat = this._colorToFloat32Array(this._colors.background);
|
||||
private _updateCachedColors(colors: ReadonlyColorSet): void {
|
||||
this._bgFloat = this._colorToFloat32Array(colors.background);
|
||||
}
|
||||
|
||||
private _updateViewportRectangle(): void {
|
||||
@@ -176,8 +176,8 @@ export class RectangleRenderer extends Disposable {
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
this._terminal.cols * this._dimensions.scaledCellWidth,
|
||||
this._terminal.rows * this._dimensions.scaledCellHeight,
|
||||
this._terminal.cols * this._dimensions.device.cell.width,
|
||||
this._terminal.rows * this._dimensions.device.cell.height,
|
||||
this._bgFloat
|
||||
);
|
||||
}
|
||||
@@ -232,54 +232,54 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _updateRectangle(vertices: IVertices, offset: number, fg: number, bg: number, startX: number, endX: number, y: number): void {
|
||||
w.isDefault = false;
|
||||
$isDefault = false;
|
||||
if (fg & FgFlags.INVERSE) {
|
||||
switch (fg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
w.rgba = this._colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
|
||||
$rgba = this._themeService.colors.ansi[fg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
w.rgba = (fg & Attributes.RGB_MASK) << 8;
|
||||
$rgba = (fg & Attributes.RGB_MASK) << 8;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
w.rgba = this._colors.foreground.rgba;
|
||||
$rgba = this._themeService.colors.foreground.rgba;
|
||||
}
|
||||
} else {
|
||||
switch (bg & Attributes.CM_MASK) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
w.rgba = this._colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
|
||||
$rgba = this._themeService.colors.ansi[bg & Attributes.PCOLOR_MASK].rgba;
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
w.rgba = (bg & Attributes.RGB_MASK) << 8;
|
||||
$rgba = (bg & Attributes.RGB_MASK) << 8;
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
w.rgba = this._colors.background.rgba;
|
||||
w.isDefault = true;
|
||||
$rgba = this._themeService.colors.background.rgba;
|
||||
$isDefault = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (vertices.attributes.length < offset + 4) {
|
||||
vertices.attributes = expandFloat32Array(vertices.attributes, this._terminal.rows * this._terminal.cols * INDICES_PER_RECTANGLE);
|
||||
}
|
||||
w.x1 = startX * this._dimensions.scaledCellWidth;
|
||||
w.y1 = y * this._dimensions.scaledCellHeight;
|
||||
w.r = ((w.rgba >> 24) & 0xFF) / 255;
|
||||
w.g = ((w.rgba >> 16) & 0xFF) / 255;
|
||||
w.b = ((w.rgba >> 8 ) & 0xFF) / 255;
|
||||
w.a = (!w.isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
|
||||
$x1 = startX * this._dimensions.device.cell.width;
|
||||
$y1 = y * this._dimensions.device.cell.height;
|
||||
$r = (($rgba >> 24) & 0xFF) / 255;
|
||||
$g = (($rgba >> 16) & 0xFF) / 255;
|
||||
$b = (($rgba >> 8 ) & 0xFF) / 255;
|
||||
$a = (!$isDefault && bg & BgFlags.DIM) ? DIM_OPACITY : 1;
|
||||
|
||||
this._addRectangle(vertices.attributes, offset, w.x1, w.y1, (endX - startX) * this._dimensions.scaledCellWidth, this._dimensions.scaledCellHeight, w.r, w.g, w.b, w.a);
|
||||
this._addRectangle(vertices.attributes, offset, $x1, $y1, (endX - startX) * this._dimensions.device.cell.width, this._dimensions.device.cell.height, $r, $g, $b, $a);
|
||||
}
|
||||
|
||||
private _addRectangle(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, r: number, g: number, b: number, a: number): void {
|
||||
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
|
||||
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
|
||||
array[offset ] = x1 / this._dimensions.device.canvas.width;
|
||||
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
|
||||
array[offset + 2] = width / this._dimensions.device.canvas.width;
|
||||
array[offset + 3] = height / this._dimensions.device.canvas.height;
|
||||
array[offset + 4] = r;
|
||||
array[offset + 5] = g;
|
||||
array[offset + 6] = b;
|
||||
@@ -287,10 +287,10 @@ export class RectangleRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _addRectangleFloat(array: Float32Array, offset: number, x1: number, y1: number, width: number, height: number, color: Float32Array): void {
|
||||
array[offset ] = x1 / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 1] = y1 / this._dimensions.scaledCanvasHeight;
|
||||
array[offset + 2] = width / this._dimensions.scaledCanvasWidth;
|
||||
array[offset + 3] = height / this._dimensions.scaledCanvasHeight;
|
||||
array[offset ] = x1 / this._dimensions.device.canvas.width;
|
||||
array[offset + 1] = y1 / this._dimensions.device.canvas.height;
|
||||
array[offset + 2] = width / this._dimensions.device.canvas.width;
|
||||
array[offset + 3] = height / this._dimensions.device.canvas.height;
|
||||
array[offset + 4] = color[0];
|
||||
array[offset + 5] = color[1];
|
||||
array[offset + 6] = color[2];
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderModel, ISelectionRenderModel } from './Types';
|
||||
import { fill } from 'common/TypedArrayUtils';
|
||||
import { IRenderModel } from './Types';
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
|
||||
|
||||
export const RENDER_MODEL_INDICIES_PER_CELL = 4;
|
||||
export const RENDER_MODEL_BG_OFFSET = 1;
|
||||
@@ -21,16 +22,7 @@ export class RenderModel implements IRenderModel {
|
||||
constructor() {
|
||||
this.cells = new Uint32Array(0);
|
||||
this.lineLengths = new Uint32Array(0);
|
||||
this.selection = {
|
||||
hasSelection: false,
|
||||
columnSelectMode: false,
|
||||
viewportStartRow: 0,
|
||||
viewportEndRow: 0,
|
||||
viewportCappedStartRow: 0,
|
||||
viewportCappedEndRow: 0,
|
||||
startCol: 0,
|
||||
endCol: 0
|
||||
};
|
||||
this.selection = createSelectionRenderModel();
|
||||
}
|
||||
|
||||
public resize(cols: number, rows: number): void {
|
||||
@@ -42,17 +34,7 @@ export class RenderModel implements IRenderModel {
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
fill(this.cells, 0, 0);
|
||||
fill(this.lineLengths, 0, 0);
|
||||
}
|
||||
|
||||
public clearSelection(): void {
|
||||
this.selection.hasSelection = false;
|
||||
this.selection.viewportStartRow = 0;
|
||||
this.selection.viewportEndRow = 0;
|
||||
this.selection.viewportCappedStartRow = 0;
|
||||
this.selection.viewportCappedEndRow = 0;
|
||||
this.selection.startCol = 0;
|
||||
this.selection.endCol = 0;
|
||||
this.cells.fill(0, 0);
|
||||
this.lineLengths.fill(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-51
@@ -3,46 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a rasterized glyph within a texture atlas. Some numbers are
|
||||
* tracked in CSS pixels as well in order to reduce calculations during the
|
||||
* render loop.
|
||||
*/
|
||||
export interface IRasterizedGlyph {
|
||||
/**
|
||||
* The x and y offset between the glyph's top/left and the top/left of a cell
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
texturePosition: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
texturePositionClipSpace: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in pixels.
|
||||
*/
|
||||
size: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
sizeClipSpace: IVector;
|
||||
}
|
||||
|
||||
export interface IVector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IBoundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
|
||||
export interface IRenderModel {
|
||||
cells: Uint32Array;
|
||||
@@ -50,17 +11,6 @@ export interface IRenderModel {
|
||||
selection: ISelectionRenderModel;
|
||||
}
|
||||
|
||||
export interface ISelectionRenderModel {
|
||||
hasSelection: boolean;
|
||||
columnSelectMode: boolean;
|
||||
viewportStartRow: number;
|
||||
viewportEndRow: number;
|
||||
viewportCappedStartRow: number;
|
||||
viewportCappedEndRow: number;
|
||||
startCol: number;
|
||||
endCol: number;
|
||||
}
|
||||
|
||||
export interface IWebGL2RenderingContext extends WebGLRenderingContext {
|
||||
vertexAttribDivisor(index: number, divisor: number): void;
|
||||
createVertexArray(): IWebGLVertexArrayObject;
|
||||
|
||||
@@ -3,56 +3,78 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal, ITerminalAddon, IEvent } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { isSafari } from 'common/Platform';
|
||||
import { ICoreService, IDecorationService } from 'common/services/Services';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { getSafariVersion, isSafari } from 'common/Platform';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { ITerminalAddon, Terminal } from 'xterm';
|
||||
import { WebglRenderer } from './WebglRenderer';
|
||||
|
||||
export class WebglAddon implements ITerminalAddon {
|
||||
export class WebglAddon extends Disposable implements ITerminalAddon {
|
||||
private _terminal?: Terminal;
|
||||
private _renderer?: WebglRenderer;
|
||||
|
||||
private _onChangeTextureAtlas = new EventEmitter<HTMLElement>();
|
||||
public get onChangeTextureAtlas(): IEvent<HTMLElement> { return this._onChangeTextureAtlas.event; }
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
|
||||
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
constructor(
|
||||
private _preserveDrawingBuffer?: boolean
|
||||
) {}
|
||||
|
||||
public activate(terminal: Terminal): void {
|
||||
if (!terminal.element) {
|
||||
throw new Error('Cannot activate WebglAddon before Terminal.open');
|
||||
) {
|
||||
if (isSafari && getSafariVersion() < 16) {
|
||||
throw new Error('Webgl2 is only supported on Safari 16 and above');
|
||||
}
|
||||
if (isSafari) {
|
||||
throw new Error('Webgl is not currently supported on Safari');
|
||||
}
|
||||
this._terminal = terminal;
|
||||
const renderService: IRenderService = (terminal as any)._core._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = (terminal as any)._core._characterJoinerService;
|
||||
const coreBrowserService: ICoreBrowserService = (terminal as any)._core._coreBrowserService;
|
||||
const coreService: ICoreService = (terminal as any)._core.coreService;
|
||||
const decorationService: IDecorationService = (terminal as any)._core._decorationService;
|
||||
const colors: IColorSet = (terminal as any)._core._colorManager.colors;
|
||||
this._renderer = new WebglRenderer(terminal, colors, characterJoinerService, coreBrowserService, coreService, decorationService, this._preserveDrawingBuffer);
|
||||
forwardEvent(this._renderer.onContextLoss, this._onContextLoss);
|
||||
forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas);
|
||||
renderService.setRenderer(this._renderer);
|
||||
super();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (!this._terminal) {
|
||||
throw new Error('Cannot dispose WebglAddon because it is activated');
|
||||
public activate(terminal: Terminal): void {
|
||||
const core = (terminal as any)._core as ITerminal;
|
||||
if (!terminal.element) {
|
||||
this.register(core.onWillOpen(() => this.activate(terminal)));
|
||||
return;
|
||||
}
|
||||
const renderService: IRenderService = (this._terminal as any)._core._renderService;
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.onResize(this._terminal.cols, this._terminal.rows);
|
||||
this._renderer?.dispose();
|
||||
this._renderer = undefined;
|
||||
|
||||
this._terminal = terminal;
|
||||
const coreService: ICoreService = core.coreService;
|
||||
const optionsService: IOptionsService = core.optionsService;
|
||||
|
||||
const unsafeCore = core as any;
|
||||
const renderService: IRenderService = unsafeCore._renderService;
|
||||
const characterJoinerService: ICharacterJoinerService = unsafeCore._characterJoinerService;
|
||||
const charSizeService: ICharSizeService = unsafeCore._charSizeService;
|
||||
const coreBrowserService: ICoreBrowserService = unsafeCore._coreBrowserService;
|
||||
const decorationService: IDecorationService = unsafeCore._decorationService;
|
||||
const themeService: IThemeService = unsafeCore._themeService;
|
||||
|
||||
this._renderer = this.register(new WebglRenderer(
|
||||
terminal,
|
||||
characterJoinerService,
|
||||
charSizeService,
|
||||
coreBrowserService,
|
||||
coreService,
|
||||
decorationService,
|
||||
optionsService,
|
||||
themeService,
|
||||
this._preserveDrawingBuffer
|
||||
));
|
||||
this.register(forwardEvent(this._renderer.onContextLoss, this._onContextLoss));
|
||||
this.register(forwardEvent(this._renderer.onChangeTextureAtlas, this._onChangeTextureAtlas));
|
||||
this.register(forwardEvent(this._renderer.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas));
|
||||
this.register(forwardEvent(this._renderer.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas));
|
||||
renderService.setRenderer(this._renderer);
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
const renderService: IRenderService = (this._terminal as any)._core._renderService;
|
||||
renderService.setRenderer((this._terminal as any)._core._createRenderer());
|
||||
renderService.handleResize(terminal.cols, terminal.rows);
|
||||
}));
|
||||
}
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
@@ -60,6 +82,6 @@ export class WebglAddon implements ITerminalAddon {
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._renderer?.clearCharAtlas();
|
||||
this._renderer?.clearTextureAtlas();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,99 +3,90 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
import { acquireCharAtlas, removeTerminalFromCache } from './atlas/CharAtlasCache';
|
||||
import { WebglCharAtlas } from './atlas/WebglCharAtlas';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
import { RenderModel, COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Attributes, BgFlags, Content, FgFlags, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { Terminal, IEvent } from 'xterm';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/DevicePixelObserver';
|
||||
import { ITerminal, IColorSet } from 'browser/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
|
||||
import { acquireTextureAtlas, removeTerminalFromCache } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { observeDevicePixelDimensions } from 'browser/renderer/shared/DevicePixelObserver';
|
||||
import { createRenderDimensions, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ITerminal } from 'browser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ICoreService, IDecorationService } from 'common/services/Services';
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { fg: number, bg: number, hasFg: boolean, hasBg: boolean, isSelected: boolean } = {
|
||||
fg: 0,
|
||||
bg: 0,
|
||||
hasFg: false,
|
||||
hasBg: false,
|
||||
isSelected: false
|
||||
};
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants';
|
||||
import { EventEmitter, forwardEvent } from 'common/EventEmitter';
|
||||
import { Disposable, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { CharData, IBufferLine, ICellData } from 'common/Types';
|
||||
import { IDisposable, Terminal } from 'xterm';
|
||||
import { GlyphRenderer } from './GlyphRenderer';
|
||||
import { RectangleRenderer } from './RectangleRenderer';
|
||||
import { CursorRenderLayer } from './renderLayer/CursorRenderLayer';
|
||||
import { LinkRenderLayer } from './renderLayer/LinkRenderLayer';
|
||||
import { IRenderLayer } from './renderLayer/Types';
|
||||
import { COMBINED_CHAR_BIT_MASK, RenderModel, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL } from './RenderModel';
|
||||
import { IWebGL2RenderingContext } from './Types';
|
||||
|
||||
export class WebglRenderer extends Disposable implements IRenderer {
|
||||
private _renderLayers: IRenderLayer[];
|
||||
private _charAtlas: WebglCharAtlas | undefined;
|
||||
private _charAtlasDisposable: IDisposable | undefined;
|
||||
private _charAtlas: ITextureAtlas | undefined;
|
||||
private _devicePixelRatio: number;
|
||||
|
||||
private _model: RenderModel = new RenderModel();
|
||||
private _workCell: CellData = new CellData();
|
||||
private _workColors: { fg: number, bg: number, ext: number } = { fg: 0, bg: 0, ext: 0 };
|
||||
private _cellColorResolver: CellColorResolver;
|
||||
|
||||
private _canvas: HTMLCanvasElement;
|
||||
private _gl: IWebGL2RenderingContext;
|
||||
private _rectangleRenderer!: RectangleRenderer;
|
||||
private _glyphRenderer!: GlyphRenderer;
|
||||
private _rectangleRenderer: RectangleRenderer;
|
||||
private _glyphRenderer: GlyphRenderer;
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
public readonly dimensions: IRenderDimensions;
|
||||
|
||||
private _core: ITerminal;
|
||||
private _isAttached: boolean;
|
||||
private _contextRestorationTimeout: number | undefined;
|
||||
|
||||
private _onChangeTextureAtlas = new EventEmitter<HTMLCanvasElement>();
|
||||
public get onChangeTextureAtlas(): IEvent<HTMLCanvasElement> { return this._onChangeTextureAtlas.event; }
|
||||
private _onRequestRedraw = new EventEmitter<IRequestRedrawEvent>();
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return this._onRequestRedraw.event; }
|
||||
|
||||
private _onContextLoss = new EventEmitter<void>();
|
||||
public get onContextLoss(): IEvent<void> { return this._onContextLoss.event; }
|
||||
private readonly _onChangeTextureAtlas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onChangeTextureAtlas = this._onChangeTextureAtlas.event;
|
||||
private readonly _onAddTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onRemoveTextureAtlasCanvas = this.register(new EventEmitter<HTMLCanvasElement>());
|
||||
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
|
||||
private readonly _onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>());
|
||||
public readonly onRequestRedraw = this._onRequestRedraw.event;
|
||||
private readonly _onContextLoss = this.register(new EventEmitter<void>());
|
||||
public readonly onContextLoss = this._onContextLoss.event;
|
||||
|
||||
constructor(
|
||||
private _terminal: Terminal,
|
||||
private _colors: IColorSet,
|
||||
private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
private readonly _charSizeService: ICharSizeService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
coreService: ICoreService,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _optionsService: IOptionsService,
|
||||
private readonly _themeService: IThemeService,
|
||||
preserveDrawingBuffer?: boolean
|
||||
) {
|
||||
super();
|
||||
|
||||
this.register(this._themeService.onChangeColors(() => this._handleColorChange()));
|
||||
|
||||
this._cellColorResolver = new CellColorResolver(this._terminal, this._model.selection, this._decorationService, this._coreBrowserService, this._themeService);
|
||||
|
||||
this._core = (this._terminal as any)._core;
|
||||
|
||||
this._renderLayers = [
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._colors, this._core, this._coreBrowserService),
|
||||
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._colors, this._onRequestRedraw, this._coreBrowserService, coreService)
|
||||
new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService),
|
||||
new CursorRenderLayer(_terminal, this._core.screenElement!, 3, this._onRequestRedraw, this._coreBrowserService, coreService, _optionsService, this._themeService)
|
||||
];
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this._updateDimensions();
|
||||
this.register(_optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
|
||||
this._canvas = document.createElement('canvas');
|
||||
|
||||
@@ -136,50 +127,40 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
this._core.screenElement!.appendChild(this._canvas);
|
||||
|
||||
this._initializeWebGLState();
|
||||
[this._rectangleRenderer, this._glyphRenderer] = this._initializeWebGLState();
|
||||
|
||||
this._isAttached = this._coreBrowserService.window.document.body.contains(this._core.screenElement!);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
this._canvas.parentElement?.removeChild(this._canvas);
|
||||
removeTerminalFromCache(this._terminal);
|
||||
super.dispose();
|
||||
this.register(toDisposable(() => {
|
||||
for (const l of this._renderLayers) {
|
||||
l.dispose();
|
||||
}
|
||||
this._canvas.parentElement?.removeChild(this._canvas);
|
||||
removeTerminalFromCache(this._terminal);
|
||||
}));
|
||||
}
|
||||
|
||||
public get textureAtlas(): HTMLCanvasElement | undefined {
|
||||
return this._charAtlas?.cacheCanvas;
|
||||
return this._charAtlas?.pages[0].canvas;
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
// Clear layers and force a full render
|
||||
for (const l of this._renderLayers) {
|
||||
l.setColors(this._terminal, this._colors);
|
||||
l.reset(this._terminal);
|
||||
}
|
||||
|
||||
this._rectangleRenderer.setColors();
|
||||
|
||||
private _handleColorChange(): void {
|
||||
this._refreshCharAtlas();
|
||||
|
||||
// Force a full refresh
|
||||
this._clearModel(true);
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
// If the device pixel ratio changed, the char atlas needs to be regenerated
|
||||
// and the terminal needs to refreshed
|
||||
if (this._devicePixelRatio !== this._coreBrowserService.dpr) {
|
||||
this._devicePixelRatio = this._coreBrowserService.dpr;
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
this.handleResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
// Update character and canvas dimensions
|
||||
this._updateDimensions();
|
||||
|
||||
@@ -191,19 +172,19 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
|
||||
// Resize the canvas
|
||||
this._canvas.width = this.dimensions.scaledCanvasWidth;
|
||||
this._canvas.height = this.dimensions.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._canvas.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._canvas.width = this.dimensions.device.canvas.width;
|
||||
this._canvas.height = this.dimensions.device.canvas.height;
|
||||
this._canvas.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._canvas.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
|
||||
// Resize the screen
|
||||
this._core.screenElement!.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._core.screenElement!.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._core.screenElement!.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._core.screenElement!.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
|
||||
this._rectangleRenderer.setDimensions(this.dimensions);
|
||||
this._rectangleRenderer.onResize();
|
||||
this._rectangleRenderer.handleResize();
|
||||
this._glyphRenderer.setDimensions(this.dimensions);
|
||||
this._glyphRenderer.onResize();
|
||||
this._glyphRenderer.handleResize();
|
||||
|
||||
this._refreshCharAtlas();
|
||||
|
||||
@@ -212,44 +193,41 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._clearModel(false);
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
this.onResize(this._terminal.cols, this._terminal.rows);
|
||||
public handleCharSizeChanged(): void {
|
||||
this.handleResize(this._terminal.cols, this._terminal.rows);
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
public handleBlur(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onBlur(this._terminal);
|
||||
l.handleBlur(this._terminal);
|
||||
}
|
||||
// Request a redraw for active/inactive selection background
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
public handleFocus(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onFocus(this._terminal);
|
||||
l.handleFocus(this._terminal);
|
||||
}
|
||||
// Request a redraw for active/inactive selection background
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
l.handleSelectionChanged(this._terminal, start, end, columnSelectMode);
|
||||
}
|
||||
this._updateSelectionModel(start, end, columnSelectMode);
|
||||
this._model.selection.update(this._terminal, start, end, columnSelectMode);
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
public handleCursorMove(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onCursorMove(this._terminal);
|
||||
l.handleCursorMove(this._terminal);
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
for (const l of this._renderLayers) {
|
||||
l.onOptionsChanged(this._terminal);
|
||||
}
|
||||
private _handleOptionsChanged(): void {
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
}
|
||||
@@ -257,36 +235,47 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
/**
|
||||
* Initializes members dependent on WebGL context state.
|
||||
*/
|
||||
private _initializeWebGLState(): void {
|
||||
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
|
||||
// Dispose any previous rectangle and glyph renderers before creating new ones.
|
||||
this._rectangleRenderer?.dispose();
|
||||
this._glyphRenderer?.dispose();
|
||||
|
||||
this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
this._glyphRenderer = new GlyphRenderer(this._terminal, this._colors, this._gl, this.dimensions);
|
||||
this._rectangleRenderer = this.register(new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService));
|
||||
this._glyphRenderer = this.register(new GlyphRenderer(this._terminal, this._gl, this.dimensions));
|
||||
|
||||
// Update dimensions and acquire char atlas
|
||||
this.onCharSizeChanged();
|
||||
this.handleCharSizeChanged();
|
||||
|
||||
return [this._rectangleRenderer, this._glyphRenderer];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes the char atlas, aquiring a new one if necessary.
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(): void {
|
||||
if (this.dimensions.scaledCharWidth <= 0 && this.dimensions.scaledCharHeight <= 0) {
|
||||
if (this.dimensions.device.char.width <= 0 && this.dimensions.device.char.height <= 0) {
|
||||
// Mark as not attached so char atlas gets refreshed on next render
|
||||
this._isAttached = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const atlas = acquireCharAtlas(this._terminal, this._colors, this.dimensions.scaledCellWidth, this.dimensions.scaledCellHeight, this.dimensions.scaledCharWidth, this.dimensions.scaledCharHeight, this._coreBrowserService.dpr);
|
||||
if (!('getRasterizedGlyph' in atlas)) {
|
||||
throw new Error('The webgl renderer only works with the webgl char atlas');
|
||||
}
|
||||
const atlas = acquireTextureAtlas(
|
||||
this._terminal,
|
||||
this._optionsService.rawOptions,
|
||||
this._themeService.colors,
|
||||
this.dimensions.device.cell.width,
|
||||
this.dimensions.device.cell.height,
|
||||
this.dimensions.device.char.width,
|
||||
this.dimensions.device.char.height,
|
||||
this._coreBrowserService.dpr
|
||||
);
|
||||
if (this._charAtlas !== atlas) {
|
||||
this._onChangeTextureAtlas.fire(atlas.cacheCanvas);
|
||||
this._charAtlasDisposable?.dispose();
|
||||
this._onChangeTextureAtlas.fire(atlas.pages[0].canvas);
|
||||
this._charAtlasDisposable = getDisposeArrayDisposable([
|
||||
forwardEvent(atlas.onAddTextureAtlasCanvas, this._onAddTextureAtlasCanvas),
|
||||
forwardEvent(atlas.onRemoveTextureAtlasCanvas, this._onRemoveTextureAtlasCanvas)
|
||||
]);
|
||||
}
|
||||
this._charAtlas = atlas;
|
||||
this._charAtlas.warmUp();
|
||||
@@ -305,10 +294,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public clearCharAtlas(): void {
|
||||
public clearTextureAtlas(): void {
|
||||
this._charAtlas?.clearTexture();
|
||||
this._clearModel(true);
|
||||
this._updateModel(0, this._terminal.rows - 1);
|
||||
this._requestRedrawViewport();
|
||||
}
|
||||
|
||||
@@ -329,7 +317,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
public renderRows(start: number, end: number): void {
|
||||
if (!this._isAttached) {
|
||||
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && (this._core as any)._charSizeService.width && (this._core as any)._charSizeService.height) {
|
||||
if (this._coreBrowserService.window.document.body.contains(this._core.screenElement!) && this._charSizeService.width && this._charSizeService.height) {
|
||||
this._updateDimensions();
|
||||
this._refreshCharAtlas();
|
||||
this._isAttached = true;
|
||||
@@ -340,13 +328,12 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Update render layers
|
||||
for (const l of this._renderLayers) {
|
||||
l.onGridChanged(this._terminal, start, end);
|
||||
l.handleGridChanged(this._terminal, start, end);
|
||||
}
|
||||
|
||||
// Tell renderer the frame is beginning
|
||||
if (this._glyphRenderer.beginFrame()) {
|
||||
this._clearModel(true);
|
||||
this._updateSelectionModel(undefined, undefined);
|
||||
}
|
||||
|
||||
// Update model to reflect what's drawn
|
||||
@@ -365,7 +352,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
let lastBg: number;
|
||||
let y: number;
|
||||
let row: number;
|
||||
let line: IBufferLine;
|
||||
let line: IBufferLine | undefined;
|
||||
let joinedRanges: [number, number][];
|
||||
let isJoined: boolean;
|
||||
let lastCharX: number;
|
||||
@@ -378,15 +365,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
for (y = start; y <= end; y++) {
|
||||
row = y + terminal.buffer.ydisp;
|
||||
line = terminal.buffer.lines.get(row)!;
|
||||
line = terminal.buffer.lines.get(row);
|
||||
if (!line) {
|
||||
break;
|
||||
}
|
||||
this._model.lineLengths[y] = 0;
|
||||
joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
for (x = 0; x < terminal.cols; x++) {
|
||||
lastBg = this._workColors.bg;
|
||||
lastBg = this._cellColorResolver.result.bg;
|
||||
line.loadCell(x, cell);
|
||||
|
||||
if (x === 0) {
|
||||
lastBg = this._workColors.bg;
|
||||
lastBg = this._cellColorResolver.result.bg;
|
||||
}
|
||||
|
||||
// If true, indicates that the current character(s) to draw were joined.
|
||||
@@ -417,7 +407,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
i = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
|
||||
// Load colors/resolve overrides into work colors
|
||||
this._loadColorsForCell(x, row);
|
||||
this._cellColorResolver.resolve(cell, x, row);
|
||||
|
||||
if (code !== NULL_CELL_CODE) {
|
||||
this._model.lineLengths[y] = x + 1;
|
||||
@@ -425,9 +415,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Nothing has changed, no updates needed
|
||||
if (this._model.cells[i] === code &&
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._workColors.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._workColors.fg &&
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._workColors.ext) {
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] === this._cellColorResolver.result.bg &&
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] === this._cellColorResolver.result.fg &&
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] === this._cellColorResolver.result.ext) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -438,11 +428,11 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
|
||||
// Cache the results in the model
|
||||
this._model.cells[i] = code;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
|
||||
this._model.cells[i + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
|
||||
this._glyphRenderer.updateCell(x, y, code, this._workColors.bg, this._workColors.fg, this._workColors.ext, chars, lastBg);
|
||||
this._glyphRenderer.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg);
|
||||
|
||||
if (isJoined) {
|
||||
// Restore work cell
|
||||
@@ -453,9 +443,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL;
|
||||
this._glyphRenderer.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0);
|
||||
this._model.cells[j] = NULL_CELL_CODE;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._workColors.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._workColors.fg;
|
||||
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._workColors.ext;
|
||||
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
|
||||
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
|
||||
this._model.cells[j + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -463,215 +453,66 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this._rectangleRenderer.updateBackgrounds(this._model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads colors for the cell into the work colors object. This resolves overrides/inverse if
|
||||
* necessary which is why the work cell object is not used.
|
||||
*/
|
||||
private _loadColorsForCell(x: number, y: number): void {
|
||||
this._workColors.bg = this._workCell.bg;
|
||||
this._workColors.fg = this._workCell.fg;
|
||||
this._workColors.ext = this._workCell.bg & BgFlags.HAS_EXTENDED ? this._workCell.extended.ext : 0;
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
|
||||
// Reset overrides work variables
|
||||
w.bg = 0;
|
||||
w.fg = 0;
|
||||
w.hasBg = false;
|
||||
w.hasFg = false;
|
||||
w.isSelected = false;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasFg = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the selection color if needed
|
||||
w.isSelected = this._isCellSelected(x, y);
|
||||
if (w.isSelected) {
|
||||
w.bg = (this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
|
||||
w.hasBg = true;
|
||||
if (this._colors.selectionForeground) {
|
||||
w.fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasFg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
w.bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
w.fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
w.hasFg = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
||||
// ahead of time in order to use the correct cache key
|
||||
if (w.hasBg) {
|
||||
if (w.isSelected) {
|
||||
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
|
||||
w.bg = (this._workCell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | w.bg | Attributes.CM_RGB;
|
||||
} else {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
w.bg = (this._workCell.bg & ~Attributes.RGB_MASK) | w.bg | Attributes.CM_RGB;
|
||||
}
|
||||
}
|
||||
if (w.hasFg) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
w.fg = (this._workCell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | w.fg | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bg override or fg override was set,
|
||||
// resolving the other inverse color and setting the inverse flag if needed.
|
||||
if (this._workColors.fg & FgFlags.INVERSE) {
|
||||
if (w.hasBg && !w.hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | ((this._colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
w.fg = (this._workColors.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this._workColors.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
w.hasFg = true;
|
||||
}
|
||||
if (!w.hasBg && w.hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this._workColors.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | ((this._colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
w.bg = (this._workColors.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this._workColors.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
w.hasBg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Use the override if it exists
|
||||
this._workColors.bg = w.hasBg ? w.bg : this._workColors.bg;
|
||||
this._workColors.fg = w.hasFg ? w.fg : this._workColors.fg;
|
||||
}
|
||||
|
||||
private _isCellSelected(x: number, y: number): boolean {
|
||||
if (!this._model.selection.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
y -= this._terminal.buffer.active.viewportY;
|
||||
if (this._model.selection.columnSelectMode) {
|
||||
if (this._model.selection.startCol <= this._model.selection.endCol) {
|
||||
return x >= this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x < this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return x < this._model.selection.startCol && y >= this._model.selection.viewportCappedStartRow &&
|
||||
x >= this._model.selection.endCol && y <= this._model.selection.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this._model.selection.viewportStartRow && y < this._model.selection.viewportEndRow) ||
|
||||
(this._model.selection.viewportStartRow === this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportEndRow && x < this._model.selection.endCol) ||
|
||||
(this._model.selection.viewportStartRow < this._model.selection.viewportEndRow && y === this._model.selection.viewportStartRow && x >= this._model.selection.startCol);
|
||||
}
|
||||
|
||||
private _updateSelectionModel(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
const terminal = this._terminal;
|
||||
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
|
||||
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this._model.clearSelection();
|
||||
return;
|
||||
}
|
||||
|
||||
this._model.selection.hasSelection = true;
|
||||
this._model.selection.columnSelectMode = columnSelectMode;
|
||||
this._model.selection.viewportStartRow = viewportStartRow;
|
||||
this._model.selection.viewportEndRow = viewportEndRow;
|
||||
this._model.selection.viewportCappedStartRow = viewportCappedStartRow;
|
||||
this._model.selection.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this._model.selection.startCol = start[0];
|
||||
this._model.selection.endCol = end[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Recalculates the character and canvas dimensions.
|
||||
*/
|
||||
private _updateDimensions(): void {
|
||||
// TODO: Acquire CharSizeService properly
|
||||
|
||||
// Perform a new measure if the CharMeasure dimensions are not yet available
|
||||
if (!(this._core as any)._charSizeService.width || !(this._core as any)._charSizeService.height) {
|
||||
if (!this._charSizeService.width || !this._charSizeService.height) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate the scaled character width. Width is floored as it must be drawn to an integer grid
|
||||
// Calculate the device character width. Width is floored as it must be drawn to an integer grid
|
||||
// in order for the char atlas glyphs to not be blurry.
|
||||
this.dimensions.scaledCharWidth = Math.floor((this._core as any)._charSizeService.width * this._devicePixelRatio);
|
||||
this.dimensions.device.char.width = Math.floor(this._charSizeService.width * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled character height. Height is ceiled in case devicePixelRatio is a
|
||||
// Calculate the device character height. Height is ceiled in case devicePixelRatio is a
|
||||
// floating point number in order to ensure there is enough space to draw the character to the
|
||||
// cell.
|
||||
this.dimensions.scaledCharHeight = Math.ceil((this._core as any)._charSizeService.height * this._devicePixelRatio);
|
||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * this._devicePixelRatio);
|
||||
|
||||
// Calculate the scaled cell height, if lineHeight is _not_ 1, the resulting value will be
|
||||
// floored since lineHeight can never be lower then 1, this guarentees the scaled cell height
|
||||
// will always be larger than scaled char height.
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
|
||||
// Calculate the device cell height, if lineHeight is _not_ 1, the resulting value will be
|
||||
// floored since lineHeight can never be lower then 1, this guarentees the device cell height
|
||||
// will always be larger than device char height.
|
||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
||||
|
||||
// Calculate the y offset within a cell that glyph should draw at in order for it to be centered
|
||||
// correctly within the cell.
|
||||
this.dimensions.scaledCharTop = this._terminal.options.lineHeight === 1 ? 0 : Math.round((this.dimensions.scaledCellHeight - this.dimensions.scaledCharHeight) / 2);
|
||||
this.dimensions.device.char.top = this._optionsService.rawOptions.lineHeight === 1 ? 0 : Math.round((this.dimensions.device.cell.height - this.dimensions.device.char.height) / 2);
|
||||
|
||||
// Calculate the scaled cell width, taking the letterSpacing into account.
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
|
||||
// Calculate the device cell width, taking the letterSpacing into account.
|
||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
|
||||
// Calculate the x offset with a cell that text should draw from in order for it to be centered
|
||||
// correctly within the cell.
|
||||
this.dimensions.scaledCharLeft = Math.floor(this._terminal.options.letterSpacing / 2);
|
||||
this.dimensions.device.char.left = Math.floor(this._optionsService.rawOptions.letterSpacing / 2);
|
||||
|
||||
// Recalculate the canvas dimensions, the scaled dimensions define the actual number of pixel in
|
||||
// Recalculate the canvas dimensions, the device dimensions define the actual number of pixel in
|
||||
// the canvas
|
||||
this.dimensions.scaledCanvasHeight = this._terminal.rows * this.dimensions.scaledCellHeight;
|
||||
this.dimensions.scaledCanvasWidth = this._terminal.cols * this.dimensions.scaledCellWidth;
|
||||
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
||||
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
||||
|
||||
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// and not ceils as browsers often have floating point precision issues where
|
||||
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
||||
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
||||
// large for the canvas element size.
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / this._devicePixelRatio);
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / this._devicePixelRatio);
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
||||
|
||||
// Get the CSS dimensions of an individual cell. This needs to be derived from the calculated
|
||||
// device pixel canvas value above. CharMeasure.width/height by itself is insufficient when the
|
||||
// page is not at 100% zoom level as CharMeasure is measured in CSS pixels, but the actual char
|
||||
// size on the canvas can differ.
|
||||
this.dimensions.actualCellHeight = this.dimensions.scaledCellHeight / this._devicePixelRatio;
|
||||
this.dimensions.actualCellWidth = this.dimensions.scaledCellWidth / this._devicePixelRatio;
|
||||
this.dimensions.css.cell.height = this.dimensions.device.cell.height / this._devicePixelRatio;
|
||||
this.dimensions.css.cell.width = this.dimensions.device.cell.width / this._devicePixelRatio;
|
||||
}
|
||||
|
||||
private _setCanvasDevicePixelDimensions(width: number, height: number): void {
|
||||
if (this._canvas.width === width && this._canvas.height === height) {
|
||||
return;
|
||||
}
|
||||
// While the actual canvas size has changed, keep scaledCanvasWidth/Height as the value before
|
||||
// While the actual canvas size has changed, keep device canvas dimensions as the value before
|
||||
// the change as it's an exact multiple of the cell sizes.
|
||||
this._canvas.width = width;
|
||||
this._canvas.height = height;
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
/**
|
||||
* A matrix that when multiplies will translate 0-1 coordinates (left to right,
|
||||
* top to bottom) to clip space.
|
||||
@@ -50,9 +52,12 @@ export function expandFloat32Array(source: Float32Array, max: number): Float32Ar
|
||||
return newArray;
|
||||
}
|
||||
|
||||
export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
if (!value) {
|
||||
throw new Error('value must not be falsy');
|
||||
export class GLTexture {
|
||||
public texture: WebGLTexture;
|
||||
public version: number;
|
||||
|
||||
constructor(texture: WebGLTexture) {
|
||||
this.texture = texture;
|
||||
this.version = -1;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
|
||||
export interface IGlyphIdentifier {
|
||||
chars: string;
|
||||
code: number;
|
||||
bg: number;
|
||||
fg: number;
|
||||
bold: boolean;
|
||||
dim: boolean;
|
||||
italic: boolean;
|
||||
}
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
customGlyphs: boolean;
|
||||
devicePixelRatio: number;
|
||||
letterSpacing: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
drawBoldTextInBrightColors: boolean;
|
||||
minimumContrastRatio: number;
|
||||
colors: IColorSet;
|
||||
}
|
||||
@@ -4,48 +4,53 @@
|
||||
*/
|
||||
|
||||
import { IRenderLayer } from './Types';
|
||||
import { acquireCharAtlas } from '../atlas/CharAtlasCache';
|
||||
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/Constants';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { WebglCharAtlas } from 'atlas/WebglCharAtlas';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
|
||||
export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
export abstract class BaseRenderLayer extends Disposable implements IRenderLayer {
|
||||
private _canvas: HTMLCanvasElement;
|
||||
protected _ctx!: CanvasRenderingContext2D;
|
||||
private _scaledCharWidth: number = 0;
|
||||
private _scaledCharHeight: number = 0;
|
||||
private _scaledCellWidth: number = 0;
|
||||
private _scaledCellHeight: number = 0;
|
||||
private _scaledCharLeft: number = 0;
|
||||
private _scaledCharTop: number = 0;
|
||||
private _deviceCharWidth: number = 0;
|
||||
private _deviceCharHeight: number = 0;
|
||||
private _deviceCellWidth: number = 0;
|
||||
private _deviceCellHeight: number = 0;
|
||||
private _deviceCharLeft: number = 0;
|
||||
private _deviceCharTop: number = 0;
|
||||
|
||||
protected _charAtlas: WebglCharAtlas | undefined;
|
||||
protected _charAtlas: ITextureAtlas | undefined;
|
||||
|
||||
constructor(
|
||||
terminal: Terminal,
|
||||
private _container: HTMLElement,
|
||||
id: string,
|
||||
zIndex: number,
|
||||
private _alpha: boolean,
|
||||
protected _colors: IColorSet,
|
||||
protected readonly _coreBrowserService: ICoreBrowserService
|
||||
protected readonly _coreBrowserService: ICoreBrowserService,
|
||||
protected readonly _optionsService: IOptionsService,
|
||||
protected readonly _themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
this._canvas = document.createElement('canvas');
|
||||
this._canvas.classList.add(`xterm-${id}-layer`);
|
||||
this._canvas.style.zIndex = zIndex.toString();
|
||||
this._initCanvas();
|
||||
this._container.appendChild(this._canvas);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._canvas.remove();
|
||||
if (this._charAtlas) {
|
||||
this._charAtlas.dispose();
|
||||
}
|
||||
this.register(this._themeService.onChangeColors(e => {
|
||||
this._refreshCharAtlas(terminal, e);
|
||||
this.reset(terminal);
|
||||
}));
|
||||
this.register(toDisposable(() => {
|
||||
this._canvas.remove();
|
||||
this._charAtlas?.dispose();
|
||||
}));
|
||||
}
|
||||
|
||||
private _initCanvas(): void {
|
||||
@@ -56,16 +61,11 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {}
|
||||
public onBlur(terminal: Terminal): void {}
|
||||
public onFocus(terminal: Terminal): void {}
|
||||
public onCursorMove(terminal: Terminal): void {}
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
||||
public onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||
|
||||
public setColors(terminal: Terminal, colorSet: IColorSet): void {
|
||||
this._refreshCharAtlas(terminal, colorSet);
|
||||
}
|
||||
public handleBlur(terminal: Terminal): void {}
|
||||
public handleFocus(terminal: Terminal): void {}
|
||||
public handleCursorMove(terminal: Terminal): void {}
|
||||
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {}
|
||||
public handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {}
|
||||
|
||||
protected _setTransparency(terminal: Terminal, alpha: boolean): void {
|
||||
// Do nothing when alpha doesn't change
|
||||
@@ -82,8 +82,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._container.replaceChild(this._canvas, oldCanvas);
|
||||
|
||||
// Regenerate char atlas and force a full redraw
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this.onGridChanged(terminal, 0, terminal.rows - 1);
|
||||
this._refreshCharAtlas(terminal, this._themeService.colors);
|
||||
this.handleGridChanged(terminal, 0, terminal.rows - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -91,32 +91,32 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param terminal The terminal.
|
||||
* @param colorSet The color set to use for the char atlas.
|
||||
*/
|
||||
private _refreshCharAtlas(terminal: Terminal, colorSet: IColorSet): void {
|
||||
if (this._scaledCharWidth <= 0 && this._scaledCharHeight <= 0) {
|
||||
private _refreshCharAtlas(terminal: Terminal, colorSet: ReadonlyColorSet): void {
|
||||
if (this._deviceCharWidth <= 0 && this._deviceCharHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
this._charAtlas = acquireCharAtlas(terminal, colorSet, this._scaledCellWidth, this._scaledCellHeight, this._scaledCharWidth, this._scaledCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlas = acquireTextureAtlas(terminal, this._optionsService.rawOptions, colorSet, this._deviceCellWidth, this._deviceCellHeight, this._deviceCharWidth, this._deviceCharHeight, this._coreBrowserService.dpr);
|
||||
this._charAtlas.warmUp();
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
this._scaledCellWidth = dim.scaledCellWidth;
|
||||
this._scaledCellHeight = dim.scaledCellHeight;
|
||||
this._scaledCharWidth = dim.scaledCharWidth;
|
||||
this._scaledCharHeight = dim.scaledCharHeight;
|
||||
this._scaledCharLeft = dim.scaledCharLeft;
|
||||
this._scaledCharTop = dim.scaledCharTop;
|
||||
this._canvas.width = dim.scaledCanvasWidth;
|
||||
this._canvas.height = dim.scaledCanvasHeight;
|
||||
this._canvas.style.width = `${dim.canvasWidth}px`;
|
||||
this._canvas.style.height = `${dim.canvasHeight}px`;
|
||||
this._deviceCellWidth = dim.device.cell.width;
|
||||
this._deviceCellHeight = dim.device.cell.height;
|
||||
this._deviceCharWidth = dim.device.char.width;
|
||||
this._deviceCharHeight = dim.device.char.height;
|
||||
this._deviceCharLeft = dim.device.char.left;
|
||||
this._deviceCharTop = dim.device.char.top;
|
||||
this._canvas.width = dim.device.canvas.width;
|
||||
this._canvas.height = dim.device.canvas.height;
|
||||
this._canvas.style.width = `${dim.css.canvas.width}px`;
|
||||
this._canvas.style.height = `${dim.css.canvas.height}px`;
|
||||
|
||||
// Draw the background if this is an opaque layer
|
||||
if (!this._alpha) {
|
||||
this._clearAll();
|
||||
}
|
||||
|
||||
this._refreshCharAtlas(terminal, this._colors);
|
||||
this._refreshCharAtlas(terminal, this._themeService.colors);
|
||||
}
|
||||
|
||||
public abstract reset(terminal: Terminal): void;
|
||||
@@ -130,10 +130,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillCells(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,9 +144,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillBottomLineAtCells(x: number, y: number, width: number = 1): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
(y + 1) * this._scaledCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._scaledCellWidth,
|
||||
x * this._deviceCellWidth,
|
||||
(y + 1) * this._deviceCellHeight - this._coreBrowserService.dpr - 1 /* Ensure it's drawn within the cell */,
|
||||
width * this._deviceCellWidth,
|
||||
this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
@@ -158,10 +158,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
*/
|
||||
protected _fillLeftLineAtCell(x: number, y: number, width: number): void {
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
this._coreBrowserService.dpr * width,
|
||||
this._scaledCellHeight);
|
||||
this._deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,10 +173,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected _strokeRectAtCell(x: number, y: number, width: number, height: number): void {
|
||||
this._ctx.lineWidth = this._coreBrowserService.dpr;
|
||||
this._ctx.strokeRect(
|
||||
x * this._scaledCellWidth + this._coreBrowserService.dpr / 2,
|
||||
y * this._scaledCellHeight + (this._coreBrowserService.dpr / 2),
|
||||
width * this._scaledCellWidth - this._coreBrowserService.dpr,
|
||||
(height * this._scaledCellHeight) - this._coreBrowserService.dpr);
|
||||
x * this._deviceCellWidth + this._coreBrowserService.dpr / 2,
|
||||
y * this._deviceCellHeight + (this._coreBrowserService.dpr / 2),
|
||||
width * this._deviceCellWidth - this._coreBrowserService.dpr,
|
||||
(height * this._deviceCellHeight) - this._coreBrowserService.dpr);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -186,7 +186,7 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(0, 0, this._canvas.width, this._canvas.height);
|
||||
}
|
||||
}
|
||||
@@ -201,17 +201,17 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
protected _clearCells(x: number, y: number, width: number, height: number): void {
|
||||
if (this._alpha) {
|
||||
this._ctx.clearRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
this._ctx.fillRect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
height * this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
height * this._deviceCellHeight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -223,7 +223,6 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
* @param cell The cell data for the character to draw.
|
||||
* @param x The column to draw at.
|
||||
* @param y The row to draw at.
|
||||
* @param color The color of the character.
|
||||
*/
|
||||
protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void {
|
||||
this._ctx.font = this._getFont(terminal, false, false);
|
||||
@@ -231,8 +230,8 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
this._clipCell(x, y, cell.getWidth());
|
||||
this._ctx.fillText(
|
||||
cell.getChars(),
|
||||
x * this._scaledCellWidth + this._scaledCharLeft,
|
||||
y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight);
|
||||
x * this._deviceCellWidth + this._deviceCharLeft,
|
||||
y * this._deviceCellHeight + this._deviceCharTop + this._deviceCharHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -244,10 +243,10 @@ export abstract class BaseRenderLayer implements IRenderLayer {
|
||||
private _clipCell(x: number, y: number, width: number): void {
|
||||
this._ctx.beginPath();
|
||||
this._ctx.rect(
|
||||
x * this._scaledCellWidth,
|
||||
y * this._scaledCellHeight,
|
||||
width * this._scaledCellWidth,
|
||||
this._scaledCellHeight);
|
||||
x * this._deviceCellWidth,
|
||||
y * this._deviceCellHeight,
|
||||
width * this._deviceCellWidth,
|
||||
this._deviceCellHeight);
|
||||
this._ctx.clip();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,11 @@ import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { IEventEmitter } from 'common/EventEmitter';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
import { ICoreService } from 'common/services/Services';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ICoreService, IOptionsService } from 'common/services/Services';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
|
||||
interface ICursorState {
|
||||
x: number;
|
||||
@@ -36,12 +36,13 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
terminal: Terminal,
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
private _onRequestRefreshRowsEvent: IEventEmitter<IRequestRedrawEvent>,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
private readonly _coreService: ICoreService
|
||||
private readonly _coreService: ICoreService,
|
||||
optionsService: IOptionsService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'cursor', zIndex, true, colors, coreBrowserService);
|
||||
super(terminal, container, 'cursor', zIndex, true, coreBrowserService, optionsService, themeService);
|
||||
this._state = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
@@ -54,13 +55,12 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
'block': this._renderBlockCursor.bind(this),
|
||||
'underline': this._renderUnderlineCursor.bind(this)
|
||||
};
|
||||
this.onOptionsChanged(terminal);
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
super.dispose();
|
||||
this._handleOptionsChanged(terminal);
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged(terminal)));
|
||||
this.register(toDisposable(() => {
|
||||
this._cursorBlinkStateManager?.dispose();
|
||||
this._cursorBlinkStateManager = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
@@ -78,20 +78,20 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
public reset(terminal: Terminal): void {
|
||||
this._clearCursor();
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
|
||||
this.onOptionsChanged(terminal);
|
||||
this._handleOptionsChanged(terminal);
|
||||
}
|
||||
|
||||
public onBlur(terminal: Terminal): void {
|
||||
public handleBlur(terminal: Terminal): void {
|
||||
this._cursorBlinkStateManager?.pause();
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onFocus(terminal: Terminal): void {
|
||||
public handleFocus(terminal: Terminal): void {
|
||||
this._cursorBlinkStateManager?.resume(terminal);
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onOptionsChanged(terminal: Terminal): void {
|
||||
private _handleOptionsChanged(terminal: Terminal): void {
|
||||
if (terminal.options.cursorBlink) {
|
||||
if (!this._cursorBlinkStateManager) {
|
||||
this._cursorBlinkStateManager = new CursorBlinkStateManager(() => {
|
||||
@@ -107,11 +107,11 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
this._onRequestRefreshRowsEvent.fire({ start: terminal.buffer.active.cursorY, end: terminal.buffer.active.cursorY });
|
||||
}
|
||||
|
||||
public onCursorMove(terminal: Terminal): void {
|
||||
public handleCursorMove(terminal: Terminal): void {
|
||||
this._cursorBlinkStateManager?.restartBlinkAnimation(terminal);
|
||||
}
|
||||
|
||||
public onGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
|
||||
public handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void {
|
||||
if (!this._cursorBlinkStateManager || this._cursorBlinkStateManager.isPaused) {
|
||||
this._render(terminal, false);
|
||||
} else {
|
||||
@@ -147,7 +147,7 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
if (!this._coreBrowserService.isFocused) {
|
||||
this._clearCursor();
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
const cursorStyle = terminal.options.cursorStyle;
|
||||
if (cursorStyle && cursorStyle !== 'block') {
|
||||
this._cursorRenderers[cursorStyle](terminal, cursorX, viewportRelativeCursorY, this._cell);
|
||||
@@ -212,30 +212,30 @@ export class CursorRenderLayer extends BaseRenderLayer {
|
||||
|
||||
private _renderBarCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, terminal.options.cursorWidth);
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillLeftLineAtCell(x, y, this._optionsService.rawOptions.cursorWidth);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlockCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillCells(x, y, cell.getWidth(), 1);
|
||||
this._ctx.fillStyle = this._colors.cursorAccent.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursorAccent.css;
|
||||
this._fillCharTrueColor(terminal, cell, x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderUnderlineCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.fillStyle = this._colors.cursor.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.cursor.css;
|
||||
this._fillBottomLineAtCells(x, y);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
private _renderBlurCursor(terminal: Terminal, x: number, y: number, cell: ICellData): void {
|
||||
this._ctx.save();
|
||||
this._ctx.strokeStyle = this._colors.cursor.css;
|
||||
this._ctx.strokeStyle = this._themeService.colors.cursor.css;
|
||||
this._strokeRectAtCell(x, y, cell.getWidth(), 1);
|
||||
this._ctx.restore();
|
||||
}
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { is256Color } from 'browser/renderer/shared/CharAtlasUtils';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2, ILinkifierEvent } from 'browser/Types';
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { Terminal } from 'xterm';
|
||||
import { BaseRenderLayer } from './BaseRenderLayer';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
||||
import { is256Color } from '../atlas/CharAtlasUtils';
|
||||
import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { ICoreBrowserService } from 'browser/services/Services';
|
||||
|
||||
export class LinkRenderLayer extends BaseRenderLayer {
|
||||
private _state: ILinkifierEvent | undefined;
|
||||
@@ -17,14 +18,16 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
zIndex: number,
|
||||
colors: IColorSet,
|
||||
terminal: ITerminal,
|
||||
coreBrowserService: ICoreBrowserService
|
||||
terminal: Terminal,
|
||||
linkifier2: ILinkifier2,
|
||||
coreBrowserService: ICoreBrowserService,
|
||||
optionsService: IOptionsService,
|
||||
themeService: IThemeService
|
||||
) {
|
||||
super(container, 'link', zIndex, true, colors, coreBrowserService);
|
||||
super(terminal, container, 'link', zIndex, true, coreBrowserService, optionsService, themeService);
|
||||
|
||||
terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e));
|
||||
terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e));
|
||||
this.register(linkifier2.onShowLinkUnderline(e => this._handleShowLinkUnderline(e)));
|
||||
this.register(linkifier2.onHideLinkUnderline(e => this._handleHideLinkUnderline(e)));
|
||||
}
|
||||
|
||||
public resize(terminal: Terminal, dim: IRenderDimensions): void {
|
||||
@@ -49,14 +52,14 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
}
|
||||
}
|
||||
|
||||
private _onShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleShowLinkUnderline(e: ILinkifierEvent): void {
|
||||
if (e.fg === INVERTED_DEFAULT_COLOR) {
|
||||
this._ctx.fillStyle = this._colors.background.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.background.css;
|
||||
} else if (e.fg !== undefined && is256Color(e.fg)) {
|
||||
// 256 color support
|
||||
this._ctx.fillStyle = this._colors.ansi[e.fg!].css;
|
||||
this._ctx.fillStyle = this._themeService.colors.ansi[e.fg!].css;
|
||||
} else {
|
||||
this._ctx.fillStyle = this._colors.foreground.css;
|
||||
this._ctx.fillStyle = this._themeService.colors.foreground.css;
|
||||
}
|
||||
|
||||
if (e.y1 === e.y2) {
|
||||
@@ -73,7 +76,7 @@ export class LinkRenderLayer extends BaseRenderLayer {
|
||||
this._state = e;
|
||||
}
|
||||
|
||||
private _onHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
private _handleHideLinkUnderline(e: ILinkifierEvent): void {
|
||||
this._clearCurrentLink();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,45 +4,34 @@
|
||||
*/
|
||||
|
||||
import { IDisposable, Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
export interface IRenderLayer extends IDisposable {
|
||||
/**
|
||||
* Called when the terminal loses focus.
|
||||
*/
|
||||
onBlur(terminal: Terminal): void;
|
||||
handleBlur(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* * Called when the terminal gets focus.
|
||||
* Called when the terminal gets focus.
|
||||
*/
|
||||
onFocus(terminal: Terminal): void;
|
||||
handleFocus(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the cursor is moved.
|
||||
*/
|
||||
onCursorMove(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when options change.
|
||||
*/
|
||||
onOptionsChanged(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the theme changes.
|
||||
*/
|
||||
setColors(terminal: Terminal, colorSet: IColorSet): void;
|
||||
handleCursorMove(terminal: Terminal): void;
|
||||
|
||||
/**
|
||||
* Called when the data in the grid has changed (or needs to be rendered
|
||||
* again).
|
||||
*/
|
||||
onGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
|
||||
handleGridChanged(terminal: Terminal, startRow: number, endRow: number): void;
|
||||
|
||||
/**
|
||||
* Calls when the selection changes.
|
||||
*/
|
||||
onSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleSelectionChanged(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
|
||||
/**
|
||||
* Registers a handler to join characters to render as a group
|
||||
|
||||
@@ -992,8 +992,8 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
window.result = new Uint8Array(4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor((${col - 0.5}) * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.scaledCellHeight),
|
||||
Math.floor((${col - 0.5}) * window.d.device.cell.width),
|
||||
Math.floor(window.gl.drawingBufferHeight - 1 - (${row - 0.5}) * window.d.device.cell.height),
|
||||
1, 1, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
@@ -1003,12 +1003,12 @@ async function getCellColor(col: number, row: number): Promise<number[]> {
|
||||
async function getCellPixels(col: number, row: number): Promise<number[]> {
|
||||
await page.evaluate(`
|
||||
window.gl = window.term._core._renderService._renderer._gl;
|
||||
window.result = new Uint8Array(window.d.scaledCellWidth * window.d.scaledCellHeight * 4);
|
||||
window.result = new Uint8Array(window.d.device.cell.width * window.d.device.cell.height * 4);
|
||||
window.d = window.term._core._renderService.dimensions;
|
||||
window.gl.readPixels(
|
||||
Math.floor(${col - 1} * window.d.scaledCellWidth),
|
||||
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.scaledCellHeight),
|
||||
window.d.scaledCellWidth, window.d.scaledCellHeight, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
Math.floor(${col - 1} * window.d.device.cell.width),
|
||||
Math.floor(window.gl.drawingBufferHeight - ${row} * window.d.device.cell.height),
|
||||
window.d.device.cell.width, window.d.device.cell.height, window.gl.RGBA, window.gl.UNSIGNED_BYTE, window.result
|
||||
);
|
||||
`);
|
||||
return await page.evaluate(`Array.from(window.result)`);
|
||||
|
||||
+7
-2
@@ -15,12 +15,17 @@ declare module 'xterm-addon-webgl' {
|
||||
/**
|
||||
* An event that is fired when the renderer loses its canvas context.
|
||||
*/
|
||||
public get onContextLoss(): IEvent<void>;
|
||||
public readonly onContextLoss: IEvent<void>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the texture atlas of the renderer changes.
|
||||
*/
|
||||
public get onChangeTextureAtlas(): IEvent<HTMLCanvasElement>;
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
constructor(preserveDrawingBuffer?: boolean);
|
||||
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@ trigger:
|
||||
jobs:
|
||||
- job: Linux
|
||||
pool:
|
||||
vmImage: 'ubuntu-18.04'
|
||||
vmImage: 'ubuntu-20.04'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
|
||||
+2
-7
@@ -98,13 +98,8 @@ function getNextBetaVersion(packageJson) {
|
||||
process.exit(1);
|
||||
}
|
||||
const tag = 'beta';
|
||||
let nextStableVersion;
|
||||
if (packageJson.name === 'xterm') {
|
||||
nextStableVersion = `5.0.0`;
|
||||
} else {
|
||||
const stableVersion = packageJson.version.split('.');
|
||||
nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
|
||||
}
|
||||
const stableVersion = packageJson.version.split('.');
|
||||
const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`;
|
||||
const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag);
|
||||
if (publishedVersions.length === 0) {
|
||||
return `${nextStableVersion}-${tag}.1`;
|
||||
|
||||
@@ -146,7 +146,6 @@
|
||||
left: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
color: transparent;
|
||||
}
|
||||
|
||||
+339
-88
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
/**
|
||||
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
@@ -36,15 +37,15 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
|
||||
|
||||
export interface IWindowWithTerminal extends Window {
|
||||
term: TerminalType;
|
||||
Terminal?: typeof TerminalType;
|
||||
AttachAddon?: typeof AttachAddon;
|
||||
FitAddon?: typeof FitAddon;
|
||||
SearchAddon?: typeof SearchAddon;
|
||||
SerializeAddon?: typeof SerializeAddon;
|
||||
WebLinksAddon?: typeof WebLinksAddon;
|
||||
WebglAddon?: typeof WebglAddon;
|
||||
Unicode11Addon?: typeof Unicode11Addon;
|
||||
LigaturesAddon?: typeof LigaturesAddon;
|
||||
Terminal?: typeof TerminalType; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
AttachAddon?: typeof AttachAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
FitAddon?: typeof FitAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
SearchAddon?: typeof SearchAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
SerializeAddon?: typeof SerializeAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
WebLinksAddon?: typeof WebLinksAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
WebglAddon?: typeof WebglAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
Unicode11Addon?: typeof Unicode11Addon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
LigaturesAddon?: typeof LigaturesAddon; // eslint-disable-line @typescript-eslint/naming-convention
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
@@ -59,27 +60,29 @@ type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode
|
||||
interface IDemoAddon<T extends AddonType> {
|
||||
name: T;
|
||||
canChange: boolean;
|
||||
ctor:
|
||||
ctor: (
|
||||
T extends 'attach' ? typeof AttachAddon :
|
||||
T extends 'canvas' ? typeof CanvasAddon :
|
||||
T extends 'fit' ? typeof FitAddon :
|
||||
T extends 'search' ? typeof SearchAddon :
|
||||
T extends 'serialize' ? typeof SerializeAddon :
|
||||
T extends 'web-links' ? typeof WebLinksAddon :
|
||||
T extends 'unicode11' ? typeof Unicode11Addon :
|
||||
T extends 'ligatures' ? typeof LigaturesAddon :
|
||||
typeof WebglAddon;
|
||||
instance?:
|
||||
T extends 'canvas' ? typeof CanvasAddon :
|
||||
T extends 'fit' ? typeof FitAddon :
|
||||
T extends 'search' ? typeof SearchAddon :
|
||||
T extends 'serialize' ? typeof SerializeAddon :
|
||||
T extends 'web-links' ? typeof WebLinksAddon :
|
||||
T extends 'unicode11' ? typeof Unicode11Addon :
|
||||
T extends 'ligatures' ? typeof LigaturesAddon :
|
||||
typeof WebglAddon
|
||||
);
|
||||
instance?: (
|
||||
T extends 'attach' ? AttachAddon :
|
||||
T extends 'canvas' ? CanvasAddon :
|
||||
T extends 'fit' ? FitAddon :
|
||||
T extends 'search' ? SearchAddon :
|
||||
T extends 'serialize' ? SerializeAddon :
|
||||
T extends 'web-links' ? WebLinksAddon :
|
||||
T extends 'webgl' ? WebglAddon :
|
||||
T extends 'unicode11' ? typeof Unicode11Addon :
|
||||
T extends 'ligatures' ? typeof LigaturesAddon :
|
||||
never;
|
||||
T extends 'canvas' ? CanvasAddon :
|
||||
T extends 'fit' ? FitAddon :
|
||||
T extends 'search' ? SearchAddon :
|
||||
T extends 'serialize' ? SerializeAddon :
|
||||
T extends 'web-links' ? WebLinksAddon :
|
||||
T extends 'webgl' ? WebglAddon :
|
||||
T extends 'unicode11' ? typeof Unicode11Addon :
|
||||
T extends 'ligatures' ? typeof LigaturesAddon :
|
||||
never
|
||||
);
|
||||
}
|
||||
|
||||
const addons: { [T in AddonType]: IDemoAddon<T> } = {
|
||||
@@ -96,12 +99,12 @@ const addons: { [T in AddonType]: IDemoAddon<T> } = {
|
||||
|
||||
let terminalContainer = document.getElementById('terminal-container');
|
||||
const actionElements = {
|
||||
find: <HTMLInputElement>document.querySelector('#find'),
|
||||
findNext: <HTMLInputElement>document.querySelector('#find-next'),
|
||||
findPrevious: <HTMLInputElement>document.querySelector('#find-previous'),
|
||||
find: document.querySelector('#find') as HTMLInputElement,
|
||||
findNext: document.querySelector('#find-next') as HTMLInputElement,
|
||||
findPrevious: document.querySelector('#find-previous') as HTMLInputElement,
|
||||
findResults: document.querySelector('#find-results')
|
||||
};
|
||||
const paddingElement = <HTMLInputElement>document.getElementById('padding');
|
||||
const paddingElement = document.getElementById('padding') as HTMLInputElement;
|
||||
|
||||
const xtermjsTheme = {
|
||||
foreground: '#F8F8F8',
|
||||
@@ -146,7 +149,7 @@ function getSearchOptions(e: KeyboardEvent): ISearchOptions {
|
||||
};
|
||||
}
|
||||
|
||||
const disposeRecreateButtonHandler = () => {
|
||||
const disposeRecreateButtonHandler: () => void = () => {
|
||||
// If the terminal exists dispose of it, otherwise recreate it
|
||||
if (term) {
|
||||
term.dispose();
|
||||
@@ -169,7 +172,7 @@ const disposeRecreateButtonHandler = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const createNewWindowButtonHandler = () => {
|
||||
const createNewWindowButtonHandler: () => void = () => {
|
||||
if (term) {
|
||||
disposeRecreateButtonHandler();
|
||||
}
|
||||
@@ -196,7 +199,7 @@ const createNewWindowButtonHandler = () => {
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (document.location.pathname === '/test') {
|
||||
window.Terminal = Terminal;
|
||||
@@ -216,12 +219,17 @@ if (document.location.pathname === '/test') {
|
||||
document.getElementById('htmlserialize').addEventListener('click', htmlSerializeButtonHandler);
|
||||
document.getElementById('custom-glyph').addEventListener('click', writeCustomGlyphHandler);
|
||||
document.getElementById('load-test').addEventListener('click', loadTest);
|
||||
document.getElementById('print-cjk').addEventListener('click', addCjk);
|
||||
document.getElementById('print-cjk-sgr').addEventListener('click', addCjkRandomSgr);
|
||||
document.getElementById('powerline-symbol-test').addEventListener('click', powerlineSymbolTest);
|
||||
document.getElementById('underline-test').addEventListener('click', underlineTest);
|
||||
document.getElementById('ansi-colors').addEventListener('click', ansiColorsTest);
|
||||
document.getElementById('osc-hyperlinks').addEventListener('click', addAnsiHyperlink);
|
||||
document.getElementById('sgr-test').addEventListener('click', sgrTest);
|
||||
document.getElementById('add-decoration').addEventListener('click', addDecoration);
|
||||
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler);
|
||||
document.getElementById('weblinks-test').addEventListener('click', testWeblinks);
|
||||
addVtButtons();
|
||||
}
|
||||
|
||||
function createTerminal(): void {
|
||||
@@ -244,9 +252,12 @@ function createTerminal(): void {
|
||||
addons.serialize.instance = new SerializeAddon();
|
||||
addons.fit.instance = new FitAddon();
|
||||
addons.unicode11.instance = new Unicode11Addon();
|
||||
addons.webgl.instance = new WebglAddon();
|
||||
// TODO: Remove arguments when link provider API is the default
|
||||
addons['web-links'].instance = new WebLinksAddon(undefined, undefined, true);
|
||||
try { // try to start with webgl renderer (might throw on older safari/webkit)
|
||||
addons.webgl.instance = new WebglAddon();
|
||||
} catch (e) {
|
||||
console.warn(e);
|
||||
}
|
||||
addons['web-links'].instance = new WebLinksAddon();
|
||||
typedTerm.loadAddon(addons.fit.instance);
|
||||
typedTerm.loadAddon(addons.search.instance);
|
||||
typedTerm.loadAddon(addons.serialize.instance);
|
||||
@@ -267,18 +278,27 @@ function createTerminal(): void {
|
||||
protocol = (location.protocol === 'https:') ? 'wss://' : 'ws://';
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
addons.fit.instance!.fit();
|
||||
try {
|
||||
typedTerm.loadAddon(addons.webgl.instance);
|
||||
setTimeout(() => {
|
||||
addTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => addTextureAtlas(e));
|
||||
}, 0);
|
||||
|
||||
if (addons.webgl.instance) {
|
||||
try {
|
||||
typedTerm.loadAddon(addons.webgl.instance);
|
||||
term.open(terminalContainer);
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
addons.webgl.instance.onRemoveTextureAtlasCanvas(e => removeTextureAtlas(e));
|
||||
} catch (e) {
|
||||
console.warn('error during loading webgl addon:', e);
|
||||
addons.webgl.instance.dispose();
|
||||
addons.webgl.instance = undefined;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
addons.webgl.instance = undefined;
|
||||
if (!typedTerm.element) {
|
||||
// webgl loading failed for some reason, attach with DOM renderer
|
||||
term.open(terminalContainer);
|
||||
}
|
||||
|
||||
term.focus();
|
||||
|
||||
addDomListener(paddingElement, 'change', setPadding);
|
||||
@@ -297,26 +317,24 @@ function createTerminal(): void {
|
||||
});
|
||||
|
||||
// fit is called within a setTimeout, cols and rows need this.
|
||||
setTimeout(() => {
|
||||
setTimeout(async () => {
|
||||
initOptions(term);
|
||||
// TODO: Clean this up, opt-cols/rows doesn't exist anymore
|
||||
(<HTMLInputElement>document.getElementById(`opt-cols`)).value = term.cols;
|
||||
(<HTMLInputElement>document.getElementById(`opt-rows`)).value = term.rows;
|
||||
(document.getElementById(`opt-cols`) as HTMLInputElement).value = term.cols;
|
||||
(document.getElementById(`opt-rows`) as HTMLInputElement).value = term.rows;
|
||||
paddingElement.value = '0';
|
||||
|
||||
// Set terminal size again to set the specific dimensions on the demo
|
||||
updateTerminalSize();
|
||||
|
||||
fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' }).then((res) => {
|
||||
res.text().then((processId) => {
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
});
|
||||
});
|
||||
const res = await fetch('/terminals?cols=' + term.cols + '&rows=' + term.rows, { method: 'POST' });
|
||||
const processId = await res.text();
|
||||
pid = processId;
|
||||
socketURL += processId;
|
||||
socket = new WebSocket(socketURL);
|
||||
socket.onopen = runRealTerminal;
|
||||
socket.onclose = runFakeTerminal;
|
||||
socket.onerror = runFakeTerminal;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
@@ -427,14 +445,14 @@ function initOptions(term: TerminalType): void {
|
||||
|
||||
// Attach listeners
|
||||
booleanOptions.forEach(o => {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.checked);
|
||||
term.options[o] = input.checked;
|
||||
});
|
||||
});
|
||||
numberOptions.forEach(o => {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
if (o === 'rows') {
|
||||
@@ -456,7 +474,7 @@ function initOptions(term: TerminalType): void {
|
||||
});
|
||||
});
|
||||
Object.keys(stringOptions).forEach(o => {
|
||||
const input = <HTMLInputElement>document.getElementById(`opt-${o}`);
|
||||
const input = document.getElementById(`opt-${o}`) as HTMLInputElement;
|
||||
addDomListener(input, 'change', () => {
|
||||
console.log('change', o, input.value);
|
||||
let value: any = input.value;
|
||||
@@ -489,7 +507,7 @@ function initOptions(term: TerminalType): void {
|
||||
magenta: '#b168df',
|
||||
red: '#da6771',
|
||||
white: '#efefef',
|
||||
yellow: '#fff099',
|
||||
yellow: '#fff099'
|
||||
};
|
||||
break;
|
||||
case 'light':
|
||||
@@ -513,7 +531,7 @@ function initOptions(term: TerminalType): void {
|
||||
magenta: '#bc05bc',
|
||||
red: '#cd3131',
|
||||
white: '#555555',
|
||||
yellow: '#949800',
|
||||
yellow: '#949800'
|
||||
};
|
||||
break;
|
||||
}
|
||||
@@ -545,7 +563,17 @@ function initAddons(term: TerminalType): void {
|
||||
try {
|
||||
term.loadAddon(addon.instance);
|
||||
if (name === 'webgl') {
|
||||
(addon.instance as WebglAddon).onChangeTextureAtlas(e => addTextureAtlas(e));
|
||||
setTimeout(() => {
|
||||
setTextureAtlas(addons.webgl.instance.textureAtlas);
|
||||
addons.webgl.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.webgl.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 0);
|
||||
} else if (name === 'canvas') {
|
||||
setTimeout(() => {
|
||||
setTextureAtlas(addons.canvas.instance.textureAtlas);
|
||||
addons.canvas.instance.onChangeTextureAtlas(e => setTextureAtlas(e));
|
||||
addons.canvas.instance.onAddTextureAtlasCanvas(e => appendTextureAtlas(e));
|
||||
}, 0);
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '11';
|
||||
} else if (name === 'search') {
|
||||
@@ -559,7 +587,9 @@ function initAddons(term: TerminalType): void {
|
||||
}
|
||||
} else {
|
||||
if (name === 'webgl') {
|
||||
(addon.instance as WebglAddon).textureAtlas.remove();
|
||||
addons.webgl.instance.textureAtlas.remove();
|
||||
} else if (name === 'canvas') {
|
||||
addons.canvas.instance.textureAtlas.remove();
|
||||
} else if (name === 'unicode11') {
|
||||
term.unicode.activeVersion = '6';
|
||||
}
|
||||
@@ -584,7 +614,7 @@ function initAddons(term: TerminalType): void {
|
||||
container.appendChild(fragment);
|
||||
}
|
||||
|
||||
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined) {
|
||||
function updateFindResults(e: { resultIndex: number, resultCount: number } | undefined): void {
|
||||
let content: string;
|
||||
if (e === undefined) {
|
||||
content = 'undefined';
|
||||
@@ -600,10 +630,8 @@ function addDomListener(element: HTMLElement, type: string, handler: (...args: a
|
||||
}
|
||||
|
||||
function updateTerminalSize(): void {
|
||||
const cols = parseInt((<HTMLInputElement>document.getElementById(`opt-cols`)).value, 10);
|
||||
const rows = parseInt((<HTMLInputElement>document.getElementById(`opt-rows`)).value, 10);
|
||||
const width = (cols * term._core._renderService.dimensions.actualCellWidth + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (rows * term._core._renderService.dimensions.actualCellHeight).toString() + 'px';
|
||||
const width = (term._core._renderService.dimensions.css.canvas.width + term._core.viewport.scrollBarWidth).toString() + 'px';
|
||||
const height = (term._core._renderService.dimensions.css.canvas.height).toString() + 'px';
|
||||
terminalContainer.style.width = width;
|
||||
terminalContainer.style.height = height;
|
||||
addons.fit.instance.fit();
|
||||
@@ -625,21 +653,33 @@ function htmlSerializeButtonHandler(): void {
|
||||
document.getElementById('htmlserialize-output').innerText = output;
|
||||
|
||||
// Deprecated, but the most supported for now.
|
||||
function listener(e: any) {
|
||||
e.clipboardData.setData("text/html", output);
|
||||
function listener(e: any): void {
|
||||
e.clipboardData.setData('text/html', output);
|
||||
e.preventDefault();
|
||||
}
|
||||
document.addEventListener("copy", listener);
|
||||
document.execCommand("copy");
|
||||
document.removeEventListener("copy", listener);
|
||||
document.getElementById("htmlserialize-output-result").innerText = "Copied to clipboard";
|
||||
document.addEventListener('copy', listener);
|
||||
document.execCommand('copy');
|
||||
document.removeEventListener('copy', listener);
|
||||
document.getElementById('htmlserialize-output-result').innerText = 'Copied to clipboard';
|
||||
}
|
||||
|
||||
function addTextureAtlas(e: HTMLCanvasElement) {
|
||||
function setTextureAtlas(e: HTMLCanvasElement): void {
|
||||
styleAtlasPage(e);
|
||||
document.querySelector('#texture-atlas').replaceChildren(e);
|
||||
}
|
||||
function appendTextureAtlas(e: HTMLCanvasElement): void {
|
||||
styleAtlasPage(e);
|
||||
document.querySelector('#texture-atlas').appendChild(e);
|
||||
}
|
||||
function removeTextureAtlas(e: HTMLCanvasElement): void {
|
||||
e.remove();
|
||||
}
|
||||
function styleAtlasPage(e: HTMLCanvasElement): void {
|
||||
e.style.width = `${e.width / window.devicePixelRatio}px`;
|
||||
e.style.height = `${e.height / window.devicePixelRatio}px`;
|
||||
}
|
||||
|
||||
function writeCustomGlyphHandler() {
|
||||
function writeCustomGlyphHandler(): void {
|
||||
term.write('\n\r');
|
||||
term.write('\n\r');
|
||||
term.write('Box styles: ┎┰┒┍┯┑╓╥╖╒╤╕ ┏┳┓┌┲┓┌┬┐┏┱┐\n\r');
|
||||
@@ -681,10 +721,11 @@ function writeCustomGlyphHandler() {
|
||||
term.write(' ║│╱ ╲│║ │║ ║│ ││ │ ││ │║ ┃ ║│ ┃│ ╽ │┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▎\n\r');
|
||||
term.write(' ║└─╥─┘║ │╚═╤═╝│ │╘═╪═╛│ │╙─╀─╜│ ┃└─╂─┘┃ ░░▒▒▓▓██ ┊ ┆ ╎ ╏ ┇ ┋ ▏\n\r');
|
||||
term.write(' ╚══╩══╝ └──┴──┘ ╰──┴──╯ ╰──┴──╯ ┗━━┻━━┛ └╌╌┘ ╎ ┗╍╍┛ ┋ ▁▂▃▄▅▆▇█\n\r');
|
||||
term.write('\x1b[0m');
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
function loadTest() {
|
||||
function loadTest(): void {
|
||||
const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom';
|
||||
const testData = [];
|
||||
let byteCount = 0;
|
||||
@@ -717,7 +758,7 @@ function loadTest() {
|
||||
});
|
||||
}
|
||||
|
||||
function powerlineSymbolTest() {
|
||||
function powerlineSymbolTest(): void {
|
||||
function s(char: string): string {
|
||||
return `${char} \x1b[7m${char}\x1b[0m `;
|
||||
}
|
||||
@@ -791,7 +832,7 @@ function powerlineSymbolTest() {
|
||||
term.writeln('nf-mdi-github_face (\\uFbd9) \ufbd9');
|
||||
}
|
||||
|
||||
function underlineTest() {
|
||||
function underlineTest(): void {
|
||||
function u(style: number): string {
|
||||
return `\x1b[4:${style}m`;
|
||||
}
|
||||
@@ -801,7 +842,7 @@ function underlineTest() {
|
||||
term.write('\n\n\r');
|
||||
term.writeln('Underline styles:');
|
||||
term.writeln('');
|
||||
function showSequence(id: number, name: string) {
|
||||
function showSequence(id: number, name: string): string {
|
||||
let alphabet = '';
|
||||
for (let i = 97; i < 123; i++) {
|
||||
alphabet += String.fromCharCode(i);
|
||||
@@ -848,7 +889,7 @@ function underlineTest() {
|
||||
term.write('\x1b[0m\n\r');
|
||||
}
|
||||
|
||||
function ansiColorsTest() {
|
||||
function ansiColorsTest(): void {
|
||||
term.writeln(`\x1b[0m\n\n\rStandard colors: Bright colors:`);
|
||||
for (let i = 0; i < 16; i++) {
|
||||
term.write(`\x1b[48;5;${i}m ${i.toString().padEnd(2, ' ').padStart(3, ' ')} \x1b[0m`);
|
||||
@@ -873,7 +914,80 @@ function ansiColorsTest() {
|
||||
}
|
||||
}
|
||||
|
||||
function addAnsiHyperlink() {
|
||||
function writeTestString(): string {
|
||||
let alphabet = '';
|
||||
for (let i = 97; i < 123; i++) {
|
||||
alphabet += String.fromCharCode(i);
|
||||
}
|
||||
let numbers = '';
|
||||
for (let i = 0; i < 10; i++) {
|
||||
numbers += i.toString();
|
||||
}
|
||||
return `${alphabet} ${numbers} 汉语 한국어 👽`;
|
||||
}
|
||||
const testString = writeTestString();
|
||||
|
||||
function sgrTest(): void {
|
||||
term.write('\n\n\r');
|
||||
term.writeln(`Character Attributes (SGR, Select Graphic Rendition)`);
|
||||
const entries: { ps: number, name: string }[] = [
|
||||
{ ps: 0, name: 'Normal' },
|
||||
{ ps: 1, name: 'Bold' },
|
||||
{ ps: 2, name: 'Faint/dim' },
|
||||
{ ps: 3, name: 'Italicized' },
|
||||
{ ps: 4, name: 'Underlined' },
|
||||
{ ps: 5, name: 'Blink' },
|
||||
{ ps: 7, name: 'Inverse' },
|
||||
{ ps: 8, name: 'Invisible' },
|
||||
{ ps: 9, name: 'Crossed-out characters' },
|
||||
{ ps: 21, name: 'Doubly-underlined' },
|
||||
{ ps: 22, name: 'Normal' },
|
||||
{ ps: 23, name: 'Not italicized' },
|
||||
{ ps: 24, name: 'Not underlined' },
|
||||
{ ps: 25, name: 'Steady (not blink)' },
|
||||
{ ps: 27, name: 'Positive (not inverse)' },
|
||||
{ ps: 28, name: 'Visible (not hidden)' },
|
||||
{ ps: 29, name: 'Not crossed-out' },
|
||||
{ ps: 30, name: 'Foreground Black' },
|
||||
{ ps: 31, name: 'Foreground Red' },
|
||||
{ ps: 32, name: 'Foreground Green' },
|
||||
{ ps: 33, name: 'Foreground Yellow' },
|
||||
{ ps: 34, name: 'Foreground Blue' },
|
||||
{ ps: 35, name: 'Foreground Magenta' },
|
||||
{ ps: 36, name: 'Foreground Cyan' },
|
||||
{ ps: 37, name: 'Foreground White' },
|
||||
{ ps: 39, name: 'Foreground default' },
|
||||
{ ps: 40, name: 'Background Black' },
|
||||
{ ps: 41, name: 'Background Red' },
|
||||
{ ps: 42, name: 'Background Green' },
|
||||
{ ps: 43, name: 'Background Yellow' },
|
||||
{ ps: 44, name: 'Background Blue' },
|
||||
{ ps: 45, name: 'Background Magenta' },
|
||||
{ ps: 46, name: 'Background Cyan' },
|
||||
{ ps: 47, name: 'Background White' },
|
||||
{ ps: 49, name: 'Background default' }
|
||||
];
|
||||
const maxNameLength = entries.reduce<number>((p, c) => Math.max(c.name.length, p), 0);
|
||||
for (const e of entries) {
|
||||
term.writeln(`\x1b[0m\x1b[${e.ps}m ${e.ps.toString().padEnd(2, ' ')} ${e.name.padEnd(maxNameLength, ' ')} - ${testString}\x1b[0m`);
|
||||
}
|
||||
const entriesByPs: Map<number, string> = new Map();
|
||||
for (const e of entries) {
|
||||
entriesByPs.set(e.ps, e.name);
|
||||
}
|
||||
const comboEntries: { ps: number[] }[] = [
|
||||
{ ps: [1, 2, 3, 4, 5, 6, 7, 9] },
|
||||
{ ps: [2, 41] }
|
||||
];
|
||||
term.write('\n\n\r');
|
||||
term.writeln(`Combinations`);
|
||||
for (const e of comboEntries) {
|
||||
const name = e.ps.map(e => entriesByPs.get(e)).join(', ');
|
||||
term.writeln(`\x1b[0m\x1b[${e.ps.join(';')}m ${name}\n\r${testString}\x1b[0m`);
|
||||
}
|
||||
}
|
||||
|
||||
function addAnsiHyperlink(): void {
|
||||
term.write('\n\n\r');
|
||||
term.writeln(`Regular link with no id:`);
|
||||
term.writeln('\x1b]8;;https://github.com\x07GitHub\x1b]8;;\x07');
|
||||
@@ -893,7 +1007,36 @@ function addAnsiHyperlink() {
|
||||
term.write('\x1b[3A\x1b[1C\x1b]8;;https://xtermjs.org\x07xter\x1b[B\x1b[4Dm.js\x1b]8;;\x07\x1b[2B\x1b[5D');
|
||||
}
|
||||
|
||||
function addDecoration() {
|
||||
/**
|
||||
* Prints the 20977 characters from the CJK Unified Ideographs unicode block.
|
||||
*/
|
||||
function addCjk(): void {
|
||||
term.write('\n\n\r');
|
||||
for (let i = 0x4E00; i < 0x9FCC; i++) {
|
||||
term.write(String.fromCharCode(i));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized styles.
|
||||
*/
|
||||
function addCjkRandomSgr(): void {
|
||||
term.write('\n\n\r');
|
||||
for (let i = 0x4E00; i < 0x9FCC; i++) {
|
||||
term.write(`\x1b[${getRandomSgr()}m${String.fromCharCode(i)}\x1b[0m`);
|
||||
}
|
||||
}
|
||||
const randomSgrAttributes = [
|
||||
'1', '2', '3', '4', '5', '6', '7', '9',
|
||||
'21', '22', '23', '24', '25', '26', '27', '28', '29',
|
||||
'30', '31', '32', '33', '34', '35', '36', '37', '38', '39',
|
||||
'40', '41', '42', '43', '44', '45', '46', '47', '48', '49'
|
||||
];
|
||||
function getRandomSgr(): string {
|
||||
return randomSgrAttributes[Math.floor(Math.random() * randomSgrAttributes.length)];
|
||||
}
|
||||
|
||||
function addDecoration(): void {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
const marker = term.registerMarker(1);
|
||||
const decoration = term.registerDecoration({
|
||||
@@ -908,7 +1051,7 @@ function addDecoration() {
|
||||
});
|
||||
}
|
||||
|
||||
function addOverviewRuler() {
|
||||
function addOverviewRuler(): void {
|
||||
term.options['overviewRulerWidth'] = 15;
|
||||
term.registerDecoration({ marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' } });
|
||||
term.registerDecoration({ marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' } });
|
||||
@@ -920,3 +1063,111 @@ function addOverviewRuler() {
|
||||
term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } });
|
||||
}
|
||||
|
||||
(console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
|
||||
function getBox(width: number, height: number): any {
|
||||
return {
|
||||
string: '+',
|
||||
style: 'font-size: 1px; padding: ' + Math.floor(height/2) + 'px ' + Math.floor(width/2) + 'px; line-height: ' + height + 'px;'
|
||||
};
|
||||
}
|
||||
if (source instanceof HTMLCanvasElement) {
|
||||
source = source.getContext('2d')?.getImageData(0, 0, source.width, source.height)!;
|
||||
}
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = source.width;
|
||||
canvas.height = source.height;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.putImageData(source, 0, 0);
|
||||
|
||||
const sw = source.width * scale;
|
||||
const sh = source.height * scale;
|
||||
const dim = getBox(sw, sh);
|
||||
console.log(
|
||||
`Image: ${source.width} x ${source.height}\n%c${dim.string}`,
|
||||
`${dim.style}background: url(${canvas.toDataURL()}); background-size: ${sw}px ${sh}px; background-repeat: no-repeat; color: transparent;`
|
||||
);
|
||||
console.groupCollapsed('Zoomed');
|
||||
console.log(
|
||||
`%c${dim.string}`,
|
||||
`${getBox(sw * 10, sh * 10).style}background: url(${canvas.toDataURL()}); background-size: ${sw * 10}px ${sh * 10}px; background-repeat: no-repeat; color: transparent; image-rendering: pixelated;-ms-interpolation-mode: nearest-neighbor;`
|
||||
);
|
||||
console.groupEnd();
|
||||
};
|
||||
|
||||
function addVtButtons(): void {
|
||||
function csi(e: string): string {
|
||||
return `\x1b[${e}`;
|
||||
}
|
||||
|
||||
function createButton(name: string, description: string, writeCsi: string, paramCount: number = 1): HTMLElement {
|
||||
const inputs: HTMLInputElement[] = [];
|
||||
for (let i = 0; i < paramCount; i++) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'number';
|
||||
input.title = `Input #${i + 1}`;
|
||||
inputs.push(input);
|
||||
}
|
||||
|
||||
const element = document.createElement('button');
|
||||
element.textContent = name;
|
||||
writeCsi.split('');
|
||||
const prefix = writeCsi.length === 2 ? writeCsi[0] : '';
|
||||
const suffix = writeCsi[writeCsi.length - 1];
|
||||
element.addEventListener(`click`, () => term.write(csi(`${prefix}${inputs.map(e => e.value).join(';')}${suffix}`)));
|
||||
|
||||
const desc = document.createElement('span');
|
||||
desc.textContent = description;
|
||||
|
||||
const container = document.createElement('div');
|
||||
container.classList.add('vt-button');
|
||||
container.append(element, ...inputs, desc);
|
||||
return container;
|
||||
}
|
||||
const vtFragment = document.createDocumentFragment();
|
||||
const buttonSpecs: { [key: string]: { label: string, description: string, paramCount?: number }} = {
|
||||
A: { label: 'CUU ↑', description: 'Cursor Up Ps Times' },
|
||||
B: { label: 'CUD ↓', description: 'Cursor Down Ps Times' },
|
||||
C: { label: 'CUF →', description: 'Cursor Forward Ps Times' },
|
||||
D: { label: 'CUB ←', description: 'Cursor Backward Ps Times' },
|
||||
E: { label: 'CNL', description: 'Cursor Next Line Ps Times' },
|
||||
F: { label: 'CPL', description: 'Cursor Preceding Line Ps Times' },
|
||||
G: { label: 'CHA', description: 'Cursor Character Absolute' },
|
||||
H: { label: 'CUP', description: 'Cursor Position [row;column]', paramCount: 2 },
|
||||
I: { label: 'CHT', description: 'Cursor Forward Tabulation Ps tab stops' },
|
||||
J: { label: 'ED', description: 'Erase in Display' },
|
||||
'?J': { label: 'DECSED', description: 'Erase in Display' },
|
||||
K: { label: 'EL', description: 'Erase in Line' },
|
||||
'?K': { label: 'DECSEL', description: 'Erase in Line' },
|
||||
L: { label: 'IL', description: 'Insert Ps Line(s)' },
|
||||
M: { label: 'DL', description: 'Delete Ps Line(s)' },
|
||||
P: { label: 'DCH', description: 'Delete Ps Character(s)' }
|
||||
};
|
||||
for (const s of Object.keys(buttonSpecs)) {
|
||||
const spec = buttonSpecs[s];
|
||||
vtFragment.appendChild(createButton(spec.label, spec.description, s, spec.paramCount));
|
||||
}
|
||||
|
||||
document.querySelector('#vt-container').appendChild(vtFragment);
|
||||
}
|
||||
|
||||
function testWeblinks(): void {
|
||||
const linkExamples = `
|
||||
aaa http://example.com aaa http://example.com aaa
|
||||
¥¥¥ http://example.com aaa http://example.com aaa
|
||||
aaa http://example.com ¥¥¥ http://example.com aaa
|
||||
¥¥¥ http://example.com ¥¥¥ http://example.com aaa
|
||||
aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 aaa
|
||||
¥¥¥ https://ko.wikipedia.org/wiki/위키백과:대문 aaa https://ko.wikipedia.org/wiki/위키백과:대문 ¥¥¥
|
||||
aaa http://test:password@example.com/some_path aaa
|
||||
brackets enclosed:
|
||||
aaa [http://example.de] aaa
|
||||
aaa (http://example.de) aaa
|
||||
aaa <http://example.de> aaa
|
||||
aaa {http://example.de} aaa
|
||||
ipv6 https://[::1]/with/some?vars=and&a#hash aaa
|
||||
stop at final '.': This is a sentence with an url to http://example.com.
|
||||
stop at final '?': Is this the right url http://example.com/?
|
||||
stop at final '?': Maybe this one http://example.com/with?arguments=false?
|
||||
`;
|
||||
term.write(linkExamples.split('\n').join('\r\n'));
|
||||
}
|
||||
|
||||
+13
-2
@@ -10,8 +10,6 @@
|
||||
<link rel="shortcut icon" type="image/png" href="/logo.png">
|
||||
<link rel="stylesheet" href="/xterm.css" />
|
||||
<link rel="stylesheet" href="/style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js" integrity="sha384-kM3+BR0fKGmv8mDasMGLSHdqcbgWHUNWV1rAL+tkqnH578xS82vhlu9gR5BHhBai" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js" integrity="sha384-bR8my2pcqqtUkMMTomrgTyDVACSJz1M9D70XdkeK2BaYbCYxG3CfngYdSAOcPkUZ" crossorigin="anonymous"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1 style="color: #2D2E2C">xterm.js: A terminal for the <em style="color: #5DA5D5">web</em></h1>
|
||||
@@ -25,6 +23,7 @@
|
||||
<button id= "addonsbutton" class="tabLinks" onclick="openSection(event, 'addons')">Addons</button>
|
||||
<button id= "stylebutton" class="tabLinks" onclick="openSection(event, 'style')">Style</button>
|
||||
<button id= "testbutton" class="tabLinks" onclick="openSection(event, 'test')">Test</button>
|
||||
<button id= "vtbutton" class="tabLinks" onclick="openSection(event, 'vt')">VT</button>
|
||||
</div>
|
||||
<div id="options" class="tabContent">
|
||||
<h3>Options</h3>
|
||||
@@ -74,22 +73,34 @@
|
||||
|
||||
<dt>Performance</dt>
|
||||
<dd><button id="load-test" title="Write several MB of data to simulate a lot of data coming from the process">Load test</button></dd>
|
||||
<dd><button id="print-cjk" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block">CJK Unified Ideographs</button></dd>
|
||||
<dd><button id="print-cjk-sgr" title="Prints the 20977 characters from the CJK Unified Ideographs unicode block with randomized SGR attributes">CJK Unified Ideographs (random SGR)</button></dd>
|
||||
|
||||
<dt>Styles</dt>
|
||||
<dd><button id="custom-glyph" title="Write custom box drawing and block element characters to the terminal">Test custom glyphs</button></dd>
|
||||
<dd><button id="powerline-symbol-test" title="Write powerline symbol characters to the terminal (\ue0a0+)">Powerline symbol test</button></dd>
|
||||
<dd><button id="underline-test" title="Write text with Kitty's extended underline sequences">Underline test</button></dd>
|
||||
<dd><button id="sgr-test" title="Write text with SGR attribute">SGR test</button></dd>
|
||||
<dd><button id="ansi-colors" title="Write a wide range of ansi colors">Ansi colors test</button></dd>
|
||||
<dd><button id="osc-hyperlinks" title="Write some OSC 8 hyperlinks">Ansi hyperlinks test</button></dd>
|
||||
|
||||
<dt>Decorations</dt>
|
||||
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
|
||||
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
|
||||
|
||||
<dt>Weblinks Addon</dt>
|
||||
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div id="vt" class="tabContent">
|
||||
<h3>VT</h3>
|
||||
<div id="vt-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input type="checkbox" id="texture-atlas-zoom"/>
|
||||
<label for="texture-atlas-zoom">Zoom texture atlas</label>
|
||||
<div id="texture-atlas"></div>
|
||||
<script src="dist/client-bundle.js" defer ></script>
|
||||
<script>
|
||||
|
||||
+47
-20
@@ -17,7 +17,8 @@ function startServer() {
|
||||
expressWs(app);
|
||||
|
||||
var terminals = {},
|
||||
logs = {};
|
||||
unsentOutput = {},
|
||||
temporaryDisposable = {};
|
||||
|
||||
app.use('/xterm.css', express.static(__dirname + '/../css/xterm.css'));
|
||||
app.get('/logo.png', (req, res) => {
|
||||
@@ -44,7 +45,7 @@ function startServer() {
|
||||
env['COLORTERM'] = 'truecolor';
|
||||
var cols = parseInt(req.query.cols),
|
||||
rows = parseInt(req.query.rows),
|
||||
term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], {
|
||||
term = pty.spawn(process.platform === 'win32' ? 'pwsh.exe' : 'bash', [], {
|
||||
name: 'xterm-256color',
|
||||
cols: cols || 80,
|
||||
rows: rows || 24,
|
||||
@@ -55,9 +56,9 @@ function startServer() {
|
||||
|
||||
console.log('Created terminal with PID: ' + term.pid);
|
||||
terminals[term.pid] = term;
|
||||
logs[term.pid] = '';
|
||||
term.on('data', function(data) {
|
||||
logs[term.pid] += data;
|
||||
unsentOutput[term.pid] = '';
|
||||
temporaryDisposable[term.pid] = term.onData(function(data) {
|
||||
unsentOutput[term.pid] += data;
|
||||
});
|
||||
res.send(term.pid.toString());
|
||||
res.end();
|
||||
@@ -77,15 +78,29 @@ function startServer() {
|
||||
app.ws('/terminals/:pid', function (ws, req) {
|
||||
var term = terminals[parseInt(req.params.pid)];
|
||||
console.log('Connected to terminal ' + term.pid);
|
||||
ws.send(logs[term.pid]);
|
||||
temporaryDisposable[term.pid].dispose();
|
||||
delete temporaryDisposable[term.pid];
|
||||
ws.send(unsentOutput[term.pid]);
|
||||
delete unsentOutput[term.pid];
|
||||
|
||||
// unbuffered delivery after user input
|
||||
let userInput = false;
|
||||
|
||||
// string message buffering
|
||||
function buffer(socket, timeout) {
|
||||
function buffer(socket, timeout, maxSize) {
|
||||
let s = '';
|
||||
let sender = null;
|
||||
return (data) => {
|
||||
s += data;
|
||||
if (!sender) {
|
||||
if (s.length > maxSize || userInput) {
|
||||
userInput = false;
|
||||
socket.send(s);
|
||||
s = '';
|
||||
if (sender) {
|
||||
clearTimeout(sender);
|
||||
sender = null;
|
||||
}
|
||||
} else if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(s);
|
||||
s = '';
|
||||
@@ -95,29 +110,41 @@ function startServer() {
|
||||
};
|
||||
}
|
||||
// binary message buffering
|
||||
function bufferUtf8(socket, timeout) {
|
||||
let buffer = [];
|
||||
function bufferUtf8(socket, timeout, maxSize) {
|
||||
const dataBuffer = new Uint8Array(maxSize);
|
||||
let sender = null;
|
||||
let length = 0;
|
||||
return (data) => {
|
||||
buffer.push(data);
|
||||
length += data.length;
|
||||
if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
socket.send(Buffer.concat(buffer, length));
|
||||
buffer = [];
|
||||
function flush() {
|
||||
socket.send(Buffer.from(dataBuffer.buffer, 0, length));
|
||||
length = 0;
|
||||
if (sender) {
|
||||
clearTimeout(sender);
|
||||
sender = null;
|
||||
length = 0;
|
||||
}
|
||||
}
|
||||
if (length + data.length > maxSize) {
|
||||
flush();
|
||||
}
|
||||
dataBuffer.set(data, length);
|
||||
length += data.length;
|
||||
if (length > maxSize || userInput) {
|
||||
userInput = false;
|
||||
flush();
|
||||
} else if (!sender) {
|
||||
sender = setTimeout(() => {
|
||||
sender = null;
|
||||
flush();
|
||||
}, timeout);
|
||||
}
|
||||
};
|
||||
}
|
||||
const send = USE_BINARY ? bufferUtf8(ws, 5) : buffer(ws, 5);
|
||||
const send = (USE_BINARY ? bufferUtf8 : buffer)(ws, 5, 262144);
|
||||
|
||||
// WARNING: This is a naive implementation that will not throttle the flow of data. This means
|
||||
// it could flood the communication channel and make the terminal unresponsive. Learn more about
|
||||
// the problem and how to implement flow control at https://xtermjs.org/docs/guides/flowcontrol/
|
||||
term.on('data', function(data) {
|
||||
term.onData(function(data) {
|
||||
try {
|
||||
send(data);
|
||||
} catch (ex) {
|
||||
@@ -126,13 +153,13 @@ function startServer() {
|
||||
});
|
||||
ws.on('message', function(msg) {
|
||||
term.write(msg);
|
||||
userInput = true;
|
||||
});
|
||||
ws.on('close', function () {
|
||||
term.kill();
|
||||
console.log('Closed terminal ' + term.pid);
|
||||
// Clean things up
|
||||
delete terminals[term.pid];
|
||||
delete logs[term.pid];
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+9
-6
@@ -90,16 +90,19 @@ pre {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
#texture-atlas-zoom:checked + label + #texture-atlas canvas {
|
||||
/* Zoom atlas to the width of the container*/
|
||||
width: 100% !important;
|
||||
height: auto !important;
|
||||
}
|
||||
#texture-atlas {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
overflow: scroll;
|
||||
}
|
||||
#texture-atlas canvas {
|
||||
image-rendering: pixelated;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
#texture-atlas canvas:hover {
|
||||
/* zoom to 4x on hover */
|
||||
width: 4096px;
|
||||
height: 4096px;
|
||||
|
||||
.vt-button * {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "xterm",
|
||||
"description": "Full xterm terminal, in your browser",
|
||||
"version": "5.0.0",
|
||||
"version": "5.1.0",
|
||||
"main": "lib/xterm.js",
|
||||
"style": "css/xterm.css",
|
||||
"types": "typings/xterm.d.ts",
|
||||
@@ -68,11 +68,12 @@
|
||||
"cross-env": "^7.0.3",
|
||||
"deep-equal": "^2.0.5",
|
||||
"eslint": "^8.1.0",
|
||||
"eslint-plugin-jsdoc": "^39.3.6",
|
||||
"express": "^4.17.1",
|
||||
"express-ws": "^5.0.2",
|
||||
"glob": "^7.2.0",
|
||||
"jsdom": "^18.0.1",
|
||||
"mocha": "^9.2.0",
|
||||
"mocha": "^10.1.0",
|
||||
"mustache": "^4.2.0",
|
||||
"node-pty": "^0.10.1",
|
||||
"nyc": "^15.1.0",
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IBuffer } from 'common/buffer/Types';
|
||||
import { isMac } from 'common/Platform';
|
||||
import { TimeBasedDebouncer } from 'browser/TimeBasedDebouncer';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
@@ -65,8 +65,8 @@ export class AccessibilityManager extends Disposable {
|
||||
this._rowContainer.appendChild(this._rowElements[i]);
|
||||
}
|
||||
|
||||
this._topBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.TOP);
|
||||
this._bottomBoundaryFocusListener = e => this._onBoundaryFocus(e, BoundaryPosition.BOTTOM);
|
||||
this._topBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.TOP);
|
||||
this._bottomBoundaryFocusListener = e => this._handleBoundaryFocus(e, BoundaryPosition.BOTTOM);
|
||||
this._rowElements[0].addEventListener('focus', this._topBoundaryFocusListener);
|
||||
this._rowElements[this._rowElements.length - 1].addEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
@@ -87,14 +87,14 @@ export class AccessibilityManager extends Disposable {
|
||||
this._terminal.element.insertAdjacentElement('afterbegin', this._accessibilityTreeRoot);
|
||||
|
||||
this.register(this._renderRowsDebouncer);
|
||||
this.register(this._terminal.onResize(e => this._onResize(e.rows)));
|
||||
this.register(this._terminal.onResize(e => this._handleResize(e.rows)));
|
||||
this.register(this._terminal.onRender(e => this._refreshRows(e.start, e.end)));
|
||||
this.register(this._terminal.onScroll(() => this._refreshRows()));
|
||||
// Line feed is an issue as the prompt won't be read out after a command is run
|
||||
this.register(this._terminal.onA11yChar(char => this._onChar(char)));
|
||||
this.register(this._terminal.onLineFeed(() => this._onChar('\n')));
|
||||
this.register(this._terminal.onA11yTab(spaceCount => this._onTab(spaceCount)));
|
||||
this.register(this._terminal.onKey(e => this._onKey(e.key)));
|
||||
this.register(this._terminal.onA11yChar(char => this._handleChar(char)));
|
||||
this.register(this._terminal.onLineFeed(() => this._handleChar('\n')));
|
||||
this.register(this._terminal.onA11yTab(spaceCount => this._handleTab(spaceCount)));
|
||||
this.register(this._terminal.onKey(e => this._handleKey(e.key)));
|
||||
this.register(this._terminal.onBlur(() => this._clearLiveRegion()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this._refreshRowsDimensions()));
|
||||
|
||||
@@ -104,15 +104,13 @@ export class AccessibilityManager extends Disposable {
|
||||
// This shouldn't be needed on modern browsers but is present in case the
|
||||
// media query that drives the ScreenDprMonitor isn't supported
|
||||
this.register(addDisposableDomListener(window, 'resize', () => this._refreshRowsDimensions()));
|
||||
this.register(toDisposable(() => {
|
||||
removeElementFromParent(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
removeElementFromParent(this._accessibilityTreeRoot);
|
||||
this._rowElements.length = 0;
|
||||
}
|
||||
|
||||
private _onBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
private _handleBoundaryFocus(e: FocusEvent, position: BoundaryPosition): void {
|
||||
const boundaryElement = e.target as HTMLElement;
|
||||
const beforeBoundaryElement = this._rowElements[position === BoundaryPosition.TOP ? 1 : this._rowElements.length - 2];
|
||||
|
||||
@@ -172,7 +170,7 @@ export class AccessibilityManager extends Disposable {
|
||||
e.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
private _onResize(rows: number): void {
|
||||
private _handleResize(rows: number): void {
|
||||
// Remove bottom boundary listener
|
||||
this._rowElements[this._rowElements.length - 1].removeEventListener('focus', this._bottomBoundaryFocusListener);
|
||||
|
||||
@@ -200,13 +198,13 @@ export class AccessibilityManager extends Disposable {
|
||||
return element;
|
||||
}
|
||||
|
||||
private _onTab(spaceCount: number): void {
|
||||
private _handleTab(spaceCount: number): void {
|
||||
for (let i = 0; i < spaceCount; i++) {
|
||||
this._onChar(' ');
|
||||
this._handleChar(' ');
|
||||
}
|
||||
}
|
||||
|
||||
private _onChar(char: string): void {
|
||||
private _handleChar(char: string): void {
|
||||
if (this._liveRegionLineCount < MAX_ROWS_TO_READ + 1) {
|
||||
if (this._charsToConsume.length > 0) {
|
||||
// Have the screen reader ignore the char if it was just input
|
||||
@@ -246,9 +244,12 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _onKey(keyChar: string): void {
|
||||
private _handleKey(keyChar: string): void {
|
||||
this._clearLiveRegion();
|
||||
this._charsToConsume.push(keyChar);
|
||||
// Only add the char if there is no control character.
|
||||
if (!/\p{Control}/u.test(keyChar)) {
|
||||
this._charsToConsume.push(keyChar);
|
||||
}
|
||||
}
|
||||
|
||||
private _refreshRows(start?: number, end?: number): void {
|
||||
@@ -276,11 +277,12 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshRowsDimensions(): void {
|
||||
if (!this._renderService.dimensions.actualCellHeight) {
|
||||
if (!this._renderService.dimensions.css.cell.height) {
|
||||
return;
|
||||
}
|
||||
this._accessibilityTreeRoot.style.width = `${this._renderService.dimensions.css.canvas.width}px`;
|
||||
if (this._rowElements.length !== this._terminal.rows) {
|
||||
this._onResize(this._terminal.rows);
|
||||
this._handleResize(this._terminal.rows);
|
||||
}
|
||||
for (let i = 0; i < this._terminal.rows; i++) {
|
||||
this._refreshRowDimensions(this._rowElements[i]);
|
||||
@@ -288,7 +290,7 @@ export class AccessibilityManager extends Disposable {
|
||||
}
|
||||
|
||||
private _refreshRowDimensions(element: HTMLElement): void {
|
||||
element.style.height = `${this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.height = `${this._renderService.dimensions.css.cell.height}px`;
|
||||
}
|
||||
|
||||
private _announceCharacters(): void {
|
||||
|
||||
@@ -39,8 +39,6 @@ export function copyHandler(ev: ClipboardEvent, selectionService: ISelectionServ
|
||||
|
||||
/**
|
||||
* Redirect the clipboard's data to the terminal's input handler.
|
||||
* @param ev The original paste event to be handled
|
||||
* @param term The terminal on which to apply the handled paste event
|
||||
*/
|
||||
export function handlePasteEvent(ev: ClipboardEvent, textarea: HTMLTextAreaElement, coreService: ICoreService): void {
|
||||
ev.stopPropagation();
|
||||
@@ -81,10 +79,6 @@ export function moveTextAreaUnderMouseCursor(ev: MouseEvent, textarea: HTMLTextA
|
||||
|
||||
/**
|
||||
* Bind to right-click event and allow right-click copy and paste.
|
||||
* @param ev The original right click event to be handled.
|
||||
* @param textarea The terminal's textarea.
|
||||
* @param selectionService The terminal's selection manager.
|
||||
* @param shouldSelectWord If true and there is no selection the current word will be selected
|
||||
*/
|
||||
export function rightClickHandler(ev: MouseEvent, textarea: HTMLTextAreaElement, screenElement: HTMLElement, selectionService: ISelectionService, shouldSelectWord: boolean): void {
|
||||
moveTextAreaUnderMouseCursor(ev, textarea, screenElement);
|
||||
|
||||
@@ -1,366 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ColorManager, DEFAULT_ANSI_COLORS } from 'browser/ColorManager';
|
||||
|
||||
describe('ColorManager', () => {
|
||||
let cm: ColorManager;
|
||||
let dom: jsdom.JSDOM;
|
||||
let document: Document;
|
||||
let window: jsdom.DOMWindow;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
document = window.document;
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
|
||||
fillRect(): void { },
|
||||
|
||||
getImageData(): any {
|
||||
return {data: [0, 0, 0, 0xFF]};
|
||||
}
|
||||
});
|
||||
cm = new ColorManager(document, false);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should fill all colors with values', () => {
|
||||
for (const key of Object.keys(cm.colors)) {
|
||||
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok((cm.colors as any)[key].css.length >= 7);
|
||||
}
|
||||
}
|
||||
assert.equal(cm.colors.ansi.length, 256);
|
||||
});
|
||||
|
||||
it('should fill 240 colors with expected values', () => {
|
||||
assert.equal(cm.colors.ansi[16].css, '#000000');
|
||||
assert.equal(cm.colors.ansi[17].css, '#00005f');
|
||||
assert.equal(cm.colors.ansi[18].css, '#000087');
|
||||
assert.equal(cm.colors.ansi[19].css, '#0000af');
|
||||
assert.equal(cm.colors.ansi[20].css, '#0000d7');
|
||||
assert.equal(cm.colors.ansi[21].css, '#0000ff');
|
||||
assert.equal(cm.colors.ansi[22].css, '#005f00');
|
||||
assert.equal(cm.colors.ansi[23].css, '#005f5f');
|
||||
assert.equal(cm.colors.ansi[24].css, '#005f87');
|
||||
assert.equal(cm.colors.ansi[25].css, '#005faf');
|
||||
assert.equal(cm.colors.ansi[26].css, '#005fd7');
|
||||
assert.equal(cm.colors.ansi[27].css, '#005fff');
|
||||
assert.equal(cm.colors.ansi[28].css, '#008700');
|
||||
assert.equal(cm.colors.ansi[29].css, '#00875f');
|
||||
assert.equal(cm.colors.ansi[30].css, '#008787');
|
||||
assert.equal(cm.colors.ansi[31].css, '#0087af');
|
||||
assert.equal(cm.colors.ansi[32].css, '#0087d7');
|
||||
assert.equal(cm.colors.ansi[33].css, '#0087ff');
|
||||
assert.equal(cm.colors.ansi[34].css, '#00af00');
|
||||
assert.equal(cm.colors.ansi[35].css, '#00af5f');
|
||||
assert.equal(cm.colors.ansi[36].css, '#00af87');
|
||||
assert.equal(cm.colors.ansi[37].css, '#00afaf');
|
||||
assert.equal(cm.colors.ansi[38].css, '#00afd7');
|
||||
assert.equal(cm.colors.ansi[39].css, '#00afff');
|
||||
assert.equal(cm.colors.ansi[40].css, '#00d700');
|
||||
assert.equal(cm.colors.ansi[41].css, '#00d75f');
|
||||
assert.equal(cm.colors.ansi[42].css, '#00d787');
|
||||
assert.equal(cm.colors.ansi[43].css, '#00d7af');
|
||||
assert.equal(cm.colors.ansi[44].css, '#00d7d7');
|
||||
assert.equal(cm.colors.ansi[45].css, '#00d7ff');
|
||||
assert.equal(cm.colors.ansi[46].css, '#00ff00');
|
||||
assert.equal(cm.colors.ansi[47].css, '#00ff5f');
|
||||
assert.equal(cm.colors.ansi[48].css, '#00ff87');
|
||||
assert.equal(cm.colors.ansi[49].css, '#00ffaf');
|
||||
assert.equal(cm.colors.ansi[50].css, '#00ffd7');
|
||||
assert.equal(cm.colors.ansi[51].css, '#00ffff');
|
||||
assert.equal(cm.colors.ansi[52].css, '#5f0000');
|
||||
assert.equal(cm.colors.ansi[53].css, '#5f005f');
|
||||
assert.equal(cm.colors.ansi[54].css, '#5f0087');
|
||||
assert.equal(cm.colors.ansi[55].css, '#5f00af');
|
||||
assert.equal(cm.colors.ansi[56].css, '#5f00d7');
|
||||
assert.equal(cm.colors.ansi[57].css, '#5f00ff');
|
||||
assert.equal(cm.colors.ansi[58].css, '#5f5f00');
|
||||
assert.equal(cm.colors.ansi[59].css, '#5f5f5f');
|
||||
assert.equal(cm.colors.ansi[60].css, '#5f5f87');
|
||||
assert.equal(cm.colors.ansi[61].css, '#5f5faf');
|
||||
assert.equal(cm.colors.ansi[62].css, '#5f5fd7');
|
||||
assert.equal(cm.colors.ansi[63].css, '#5f5fff');
|
||||
assert.equal(cm.colors.ansi[64].css, '#5f8700');
|
||||
assert.equal(cm.colors.ansi[65].css, '#5f875f');
|
||||
assert.equal(cm.colors.ansi[66].css, '#5f8787');
|
||||
assert.equal(cm.colors.ansi[67].css, '#5f87af');
|
||||
assert.equal(cm.colors.ansi[68].css, '#5f87d7');
|
||||
assert.equal(cm.colors.ansi[69].css, '#5f87ff');
|
||||
assert.equal(cm.colors.ansi[70].css, '#5faf00');
|
||||
assert.equal(cm.colors.ansi[71].css, '#5faf5f');
|
||||
assert.equal(cm.colors.ansi[72].css, '#5faf87');
|
||||
assert.equal(cm.colors.ansi[73].css, '#5fafaf');
|
||||
assert.equal(cm.colors.ansi[74].css, '#5fafd7');
|
||||
assert.equal(cm.colors.ansi[75].css, '#5fafff');
|
||||
assert.equal(cm.colors.ansi[76].css, '#5fd700');
|
||||
assert.equal(cm.colors.ansi[77].css, '#5fd75f');
|
||||
assert.equal(cm.colors.ansi[78].css, '#5fd787');
|
||||
assert.equal(cm.colors.ansi[79].css, '#5fd7af');
|
||||
assert.equal(cm.colors.ansi[80].css, '#5fd7d7');
|
||||
assert.equal(cm.colors.ansi[81].css, '#5fd7ff');
|
||||
assert.equal(cm.colors.ansi[82].css, '#5fff00');
|
||||
assert.equal(cm.colors.ansi[83].css, '#5fff5f');
|
||||
assert.equal(cm.colors.ansi[84].css, '#5fff87');
|
||||
assert.equal(cm.colors.ansi[85].css, '#5fffaf');
|
||||
assert.equal(cm.colors.ansi[86].css, '#5fffd7');
|
||||
assert.equal(cm.colors.ansi[87].css, '#5fffff');
|
||||
assert.equal(cm.colors.ansi[88].css, '#870000');
|
||||
assert.equal(cm.colors.ansi[89].css, '#87005f');
|
||||
assert.equal(cm.colors.ansi[90].css, '#870087');
|
||||
assert.equal(cm.colors.ansi[91].css, '#8700af');
|
||||
assert.equal(cm.colors.ansi[92].css, '#8700d7');
|
||||
assert.equal(cm.colors.ansi[93].css, '#8700ff');
|
||||
assert.equal(cm.colors.ansi[94].css, '#875f00');
|
||||
assert.equal(cm.colors.ansi[95].css, '#875f5f');
|
||||
assert.equal(cm.colors.ansi[96].css, '#875f87');
|
||||
assert.equal(cm.colors.ansi[97].css, '#875faf');
|
||||
assert.equal(cm.colors.ansi[98].css, '#875fd7');
|
||||
assert.equal(cm.colors.ansi[99].css, '#875fff');
|
||||
assert.equal(cm.colors.ansi[100].css, '#878700');
|
||||
assert.equal(cm.colors.ansi[101].css, '#87875f');
|
||||
assert.equal(cm.colors.ansi[102].css, '#878787');
|
||||
assert.equal(cm.colors.ansi[103].css, '#8787af');
|
||||
assert.equal(cm.colors.ansi[104].css, '#8787d7');
|
||||
assert.equal(cm.colors.ansi[105].css, '#8787ff');
|
||||
assert.equal(cm.colors.ansi[106].css, '#87af00');
|
||||
assert.equal(cm.colors.ansi[107].css, '#87af5f');
|
||||
assert.equal(cm.colors.ansi[108].css, '#87af87');
|
||||
assert.equal(cm.colors.ansi[109].css, '#87afaf');
|
||||
assert.equal(cm.colors.ansi[110].css, '#87afd7');
|
||||
assert.equal(cm.colors.ansi[111].css, '#87afff');
|
||||
assert.equal(cm.colors.ansi[112].css, '#87d700');
|
||||
assert.equal(cm.colors.ansi[113].css, '#87d75f');
|
||||
assert.equal(cm.colors.ansi[114].css, '#87d787');
|
||||
assert.equal(cm.colors.ansi[115].css, '#87d7af');
|
||||
assert.equal(cm.colors.ansi[116].css, '#87d7d7');
|
||||
assert.equal(cm.colors.ansi[117].css, '#87d7ff');
|
||||
assert.equal(cm.colors.ansi[118].css, '#87ff00');
|
||||
assert.equal(cm.colors.ansi[119].css, '#87ff5f');
|
||||
assert.equal(cm.colors.ansi[120].css, '#87ff87');
|
||||
assert.equal(cm.colors.ansi[121].css, '#87ffaf');
|
||||
assert.equal(cm.colors.ansi[122].css, '#87ffd7');
|
||||
assert.equal(cm.colors.ansi[123].css, '#87ffff');
|
||||
assert.equal(cm.colors.ansi[124].css, '#af0000');
|
||||
assert.equal(cm.colors.ansi[125].css, '#af005f');
|
||||
assert.equal(cm.colors.ansi[126].css, '#af0087');
|
||||
assert.equal(cm.colors.ansi[127].css, '#af00af');
|
||||
assert.equal(cm.colors.ansi[128].css, '#af00d7');
|
||||
assert.equal(cm.colors.ansi[129].css, '#af00ff');
|
||||
assert.equal(cm.colors.ansi[130].css, '#af5f00');
|
||||
assert.equal(cm.colors.ansi[131].css, '#af5f5f');
|
||||
assert.equal(cm.colors.ansi[132].css, '#af5f87');
|
||||
assert.equal(cm.colors.ansi[133].css, '#af5faf');
|
||||
assert.equal(cm.colors.ansi[134].css, '#af5fd7');
|
||||
assert.equal(cm.colors.ansi[135].css, '#af5fff');
|
||||
assert.equal(cm.colors.ansi[136].css, '#af8700');
|
||||
assert.equal(cm.colors.ansi[137].css, '#af875f');
|
||||
assert.equal(cm.colors.ansi[138].css, '#af8787');
|
||||
assert.equal(cm.colors.ansi[139].css, '#af87af');
|
||||
assert.equal(cm.colors.ansi[140].css, '#af87d7');
|
||||
assert.equal(cm.colors.ansi[141].css, '#af87ff');
|
||||
assert.equal(cm.colors.ansi[142].css, '#afaf00');
|
||||
assert.equal(cm.colors.ansi[143].css, '#afaf5f');
|
||||
assert.equal(cm.colors.ansi[144].css, '#afaf87');
|
||||
assert.equal(cm.colors.ansi[145].css, '#afafaf');
|
||||
assert.equal(cm.colors.ansi[146].css, '#afafd7');
|
||||
assert.equal(cm.colors.ansi[147].css, '#afafff');
|
||||
assert.equal(cm.colors.ansi[148].css, '#afd700');
|
||||
assert.equal(cm.colors.ansi[149].css, '#afd75f');
|
||||
assert.equal(cm.colors.ansi[150].css, '#afd787');
|
||||
assert.equal(cm.colors.ansi[151].css, '#afd7af');
|
||||
assert.equal(cm.colors.ansi[152].css, '#afd7d7');
|
||||
assert.equal(cm.colors.ansi[153].css, '#afd7ff');
|
||||
assert.equal(cm.colors.ansi[154].css, '#afff00');
|
||||
assert.equal(cm.colors.ansi[155].css, '#afff5f');
|
||||
assert.equal(cm.colors.ansi[156].css, '#afff87');
|
||||
assert.equal(cm.colors.ansi[157].css, '#afffaf');
|
||||
assert.equal(cm.colors.ansi[158].css, '#afffd7');
|
||||
assert.equal(cm.colors.ansi[159].css, '#afffff');
|
||||
assert.equal(cm.colors.ansi[160].css, '#d70000');
|
||||
assert.equal(cm.colors.ansi[161].css, '#d7005f');
|
||||
assert.equal(cm.colors.ansi[162].css, '#d70087');
|
||||
assert.equal(cm.colors.ansi[163].css, '#d700af');
|
||||
assert.equal(cm.colors.ansi[164].css, '#d700d7');
|
||||
assert.equal(cm.colors.ansi[165].css, '#d700ff');
|
||||
assert.equal(cm.colors.ansi[166].css, '#d75f00');
|
||||
assert.equal(cm.colors.ansi[167].css, '#d75f5f');
|
||||
assert.equal(cm.colors.ansi[168].css, '#d75f87');
|
||||
assert.equal(cm.colors.ansi[169].css, '#d75faf');
|
||||
assert.equal(cm.colors.ansi[170].css, '#d75fd7');
|
||||
assert.equal(cm.colors.ansi[171].css, '#d75fff');
|
||||
assert.equal(cm.colors.ansi[172].css, '#d78700');
|
||||
assert.equal(cm.colors.ansi[173].css, '#d7875f');
|
||||
assert.equal(cm.colors.ansi[174].css, '#d78787');
|
||||
assert.equal(cm.colors.ansi[175].css, '#d787af');
|
||||
assert.equal(cm.colors.ansi[176].css, '#d787d7');
|
||||
assert.equal(cm.colors.ansi[177].css, '#d787ff');
|
||||
assert.equal(cm.colors.ansi[178].css, '#d7af00');
|
||||
assert.equal(cm.colors.ansi[179].css, '#d7af5f');
|
||||
assert.equal(cm.colors.ansi[180].css, '#d7af87');
|
||||
assert.equal(cm.colors.ansi[181].css, '#d7afaf');
|
||||
assert.equal(cm.colors.ansi[182].css, '#d7afd7');
|
||||
assert.equal(cm.colors.ansi[183].css, '#d7afff');
|
||||
assert.equal(cm.colors.ansi[184].css, '#d7d700');
|
||||
assert.equal(cm.colors.ansi[185].css, '#d7d75f');
|
||||
assert.equal(cm.colors.ansi[186].css, '#d7d787');
|
||||
assert.equal(cm.colors.ansi[187].css, '#d7d7af');
|
||||
assert.equal(cm.colors.ansi[188].css, '#d7d7d7');
|
||||
assert.equal(cm.colors.ansi[189].css, '#d7d7ff');
|
||||
assert.equal(cm.colors.ansi[190].css, '#d7ff00');
|
||||
assert.equal(cm.colors.ansi[191].css, '#d7ff5f');
|
||||
assert.equal(cm.colors.ansi[192].css, '#d7ff87');
|
||||
assert.equal(cm.colors.ansi[193].css, '#d7ffaf');
|
||||
assert.equal(cm.colors.ansi[194].css, '#d7ffd7');
|
||||
assert.equal(cm.colors.ansi[195].css, '#d7ffff');
|
||||
assert.equal(cm.colors.ansi[196].css, '#ff0000');
|
||||
assert.equal(cm.colors.ansi[197].css, '#ff005f');
|
||||
assert.equal(cm.colors.ansi[198].css, '#ff0087');
|
||||
assert.equal(cm.colors.ansi[199].css, '#ff00af');
|
||||
assert.equal(cm.colors.ansi[200].css, '#ff00d7');
|
||||
assert.equal(cm.colors.ansi[201].css, '#ff00ff');
|
||||
assert.equal(cm.colors.ansi[202].css, '#ff5f00');
|
||||
assert.equal(cm.colors.ansi[203].css, '#ff5f5f');
|
||||
assert.equal(cm.colors.ansi[204].css, '#ff5f87');
|
||||
assert.equal(cm.colors.ansi[205].css, '#ff5faf');
|
||||
assert.equal(cm.colors.ansi[206].css, '#ff5fd7');
|
||||
assert.equal(cm.colors.ansi[207].css, '#ff5fff');
|
||||
assert.equal(cm.colors.ansi[208].css, '#ff8700');
|
||||
assert.equal(cm.colors.ansi[209].css, '#ff875f');
|
||||
assert.equal(cm.colors.ansi[210].css, '#ff8787');
|
||||
assert.equal(cm.colors.ansi[211].css, '#ff87af');
|
||||
assert.equal(cm.colors.ansi[212].css, '#ff87d7');
|
||||
assert.equal(cm.colors.ansi[213].css, '#ff87ff');
|
||||
assert.equal(cm.colors.ansi[214].css, '#ffaf00');
|
||||
assert.equal(cm.colors.ansi[215].css, '#ffaf5f');
|
||||
assert.equal(cm.colors.ansi[216].css, '#ffaf87');
|
||||
assert.equal(cm.colors.ansi[217].css, '#ffafaf');
|
||||
assert.equal(cm.colors.ansi[218].css, '#ffafd7');
|
||||
assert.equal(cm.colors.ansi[219].css, '#ffafff');
|
||||
assert.equal(cm.colors.ansi[220].css, '#ffd700');
|
||||
assert.equal(cm.colors.ansi[221].css, '#ffd75f');
|
||||
assert.equal(cm.colors.ansi[222].css, '#ffd787');
|
||||
assert.equal(cm.colors.ansi[223].css, '#ffd7af');
|
||||
assert.equal(cm.colors.ansi[224].css, '#ffd7d7');
|
||||
assert.equal(cm.colors.ansi[225].css, '#ffd7ff');
|
||||
assert.equal(cm.colors.ansi[226].css, '#ffff00');
|
||||
assert.equal(cm.colors.ansi[227].css, '#ffff5f');
|
||||
assert.equal(cm.colors.ansi[228].css, '#ffff87');
|
||||
assert.equal(cm.colors.ansi[229].css, '#ffffaf');
|
||||
assert.equal(cm.colors.ansi[230].css, '#ffffd7');
|
||||
assert.equal(cm.colors.ansi[231].css, '#ffffff');
|
||||
assert.equal(cm.colors.ansi[232].css, '#080808');
|
||||
assert.equal(cm.colors.ansi[233].css, '#121212');
|
||||
assert.equal(cm.colors.ansi[234].css, '#1c1c1c');
|
||||
assert.equal(cm.colors.ansi[235].css, '#262626');
|
||||
assert.equal(cm.colors.ansi[236].css, '#303030');
|
||||
assert.equal(cm.colors.ansi[237].css, '#3a3a3a');
|
||||
assert.equal(cm.colors.ansi[238].css, '#444444');
|
||||
assert.equal(cm.colors.ansi[239].css, '#4e4e4e');
|
||||
assert.equal(cm.colors.ansi[240].css, '#585858');
|
||||
assert.equal(cm.colors.ansi[241].css, '#626262');
|
||||
assert.equal(cm.colors.ansi[242].css, '#6c6c6c');
|
||||
assert.equal(cm.colors.ansi[243].css, '#767676');
|
||||
assert.equal(cm.colors.ansi[244].css, '#808080');
|
||||
assert.equal(cm.colors.ansi[245].css, '#8a8a8a');
|
||||
assert.equal(cm.colors.ansi[246].css, '#949494');
|
||||
assert.equal(cm.colors.ansi[247].css, '#9e9e9e');
|
||||
assert.equal(cm.colors.ansi[248].css, '#a8a8a8');
|
||||
assert.equal(cm.colors.ansi[249].css, '#b2b2b2');
|
||||
assert.equal(cm.colors.ansi[250].css, '#bcbcbc');
|
||||
assert.equal(cm.colors.ansi[251].css, '#c6c6c6');
|
||||
assert.equal(cm.colors.ansi[252].css, '#d0d0d0');
|
||||
assert.equal(cm.colors.ansi[253].css, '#dadada');
|
||||
assert.equal(cm.colors.ansi[254].css, '#e4e4e4');
|
||||
assert.equal(cm.colors.ansi[255].css, '#eeeeee');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTheme', () => {
|
||||
it('should not throw when not setting all colors', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
cm.setTheme({});
|
||||
});
|
||||
});
|
||||
|
||||
it('should set a partial set of colors, using the default if not present', () => {
|
||||
assert.equal(cm.colors.background.css, '#000000');
|
||||
assert.equal(cm.colors.foreground.css, '#ffffff');
|
||||
cm.setTheme({
|
||||
background: '#FF0000',
|
||||
foreground: '#00FF00'
|
||||
});
|
||||
assert.equal(cm.colors.background.css, '#FF0000');
|
||||
assert.equal(cm.colors.foreground.css, '#00FF00');
|
||||
cm.setTheme({
|
||||
background: '#0000FF'
|
||||
});
|
||||
assert.equal(cm.colors.background.css, '#0000FF');
|
||||
// FG reverts back to default
|
||||
assert.equal(cm.colors.foreground.css, '#ffffff');
|
||||
});
|
||||
|
||||
it('should set all extended ansi colors in reverse order', () => {
|
||||
cm.setTheme({
|
||||
extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse()
|
||||
});
|
||||
|
||||
for (let ansiColor = 16; ansiColor <= 255; ansiColor++) {
|
||||
assert.equal(cm.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css);
|
||||
}
|
||||
});
|
||||
|
||||
it('should set one extended ansi color and keep the other default', () => {
|
||||
cm.setTheme({
|
||||
extendedAnsi: ['#ffffff']
|
||||
});
|
||||
|
||||
assert.equal(cm.colors.ansi[16].css, '#ffffff');
|
||||
assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
|
||||
});
|
||||
|
||||
it('should set extended ansi colors to the default when they are unset', () => {
|
||||
cm.setTheme({
|
||||
extendedAnsi: ['#ffffff']
|
||||
});
|
||||
assert.equal(cm.colors.ansi[16].css, '#ffffff');
|
||||
|
||||
cm.setTheme({
|
||||
extendedAnsi: []
|
||||
});
|
||||
assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
|
||||
|
||||
cm.setTheme({
|
||||
extendedAnsi: ['#ffffff']
|
||||
});
|
||||
assert.equal(cm.colors.ansi[16].css, '#ffffff');
|
||||
|
||||
cm.setTheme({});
|
||||
assert.equal(cm.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
|
||||
});
|
||||
|
||||
it('should set extended ansi colors to the default when they are partially unset', () => {
|
||||
cm.setTheme({
|
||||
extendedAnsi: ['#ffffff', '#000000']
|
||||
});
|
||||
assert.equal(cm.colors.ansi[16].css, '#ffffff');
|
||||
assert.equal(cm.colors.ansi[17].css, '#000000');
|
||||
|
||||
cm.setTheme({
|
||||
extendedAnsi: ['#ffffff']
|
||||
});
|
||||
assert.equal(cm.colors.ansi[16].css, '#ffffff');
|
||||
assert.equal(cm.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,288 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IColorManager, IColorSet, IColorContrastCache } from 'browser/Types';
|
||||
import { ITheme } from 'common/services/Services';
|
||||
import { channels, color, css } from 'common/Color';
|
||||
import { ColorContrastCache } from 'browser/ColorContrastCache';
|
||||
import { ColorIndex, IColor } from 'common/Types';
|
||||
|
||||
|
||||
interface IRestoreColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
|
||||
const DEFAULT_BACKGROUND = css.toColor('#000000');
|
||||
const DEFAULT_CURSOR = css.toColor('#ffffff');
|
||||
const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
|
||||
const DEFAULT_SELECTION = {
|
||||
css: 'rgba(255, 255, 255, 0.3)',
|
||||
rgba: 0xFFFFFF4D
|
||||
};
|
||||
|
||||
// An IIFE to generate DEFAULT_ANSI_COLORS.
|
||||
export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
|
||||
const colors = [
|
||||
// dark:
|
||||
css.toColor('#2e3436'),
|
||||
css.toColor('#cc0000'),
|
||||
css.toColor('#4e9a06'),
|
||||
css.toColor('#c4a000'),
|
||||
css.toColor('#3465a4'),
|
||||
css.toColor('#75507b'),
|
||||
css.toColor('#06989a'),
|
||||
css.toColor('#d3d7cf'),
|
||||
// bright:
|
||||
css.toColor('#555753'),
|
||||
css.toColor('#ef2929'),
|
||||
css.toColor('#8ae234'),
|
||||
css.toColor('#fce94f'),
|
||||
css.toColor('#729fcf'),
|
||||
css.toColor('#ad7fa8'),
|
||||
css.toColor('#34e2e2'),
|
||||
css.toColor('#eeeeec')
|
||||
];
|
||||
|
||||
// Fill in the remaining 240 ANSI colors.
|
||||
// Generate colors (16-231)
|
||||
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
|
||||
for (let i = 0; i < 216; i++) {
|
||||
const r = v[(i / 36) % 6 | 0];
|
||||
const g = v[(i / 6) % 6 | 0];
|
||||
const b = v[i % 6];
|
||||
colors.push({
|
||||
css: channels.toCss(r, g, b),
|
||||
rgba: channels.toRgba(r, g, b)
|
||||
});
|
||||
}
|
||||
|
||||
// Generate greys (232-255)
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const c = 8 + i * 10;
|
||||
colors.push({
|
||||
css: channels.toCss(c, c, c),
|
||||
rgba: channels.toRgba(c, c, c)
|
||||
});
|
||||
}
|
||||
|
||||
return colors;
|
||||
})());
|
||||
|
||||
/**
|
||||
* Manages the source of truth for a terminal's colors.
|
||||
*/
|
||||
export class ColorManager implements IColorManager {
|
||||
public colors: IColorSet;
|
||||
private _ctx: CanvasRenderingContext2D;
|
||||
private _litmusColor: CanvasGradient;
|
||||
private _contrastCache: IColorContrastCache;
|
||||
private _restoreColors!: IRestoreColorSet;
|
||||
|
||||
constructor(document: Document, public allowTransparency: boolean) {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 1;
|
||||
canvas.height = 1;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
throw new Error('Could not get rendering context');
|
||||
}
|
||||
this._ctx = ctx;
|
||||
this._ctx.globalCompositeOperation = 'copy';
|
||||
this._litmusColor = this._ctx.createLinearGradient(0, 0, 1, 1);
|
||||
this._contrastCache = new ColorContrastCache();
|
||||
this.colors = {
|
||||
foreground: DEFAULT_FOREGROUND,
|
||||
background: DEFAULT_BACKGROUND,
|
||||
cursor: DEFAULT_CURSOR,
|
||||
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||
selectionForeground: undefined,
|
||||
selectionBackgroundTransparent: DEFAULT_SELECTION,
|
||||
selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
|
||||
selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
ansi: DEFAULT_ANSI_COLORS.slice(),
|
||||
contrastCache: this._contrastCache
|
||||
};
|
||||
this._updateRestoreColors();
|
||||
}
|
||||
|
||||
public onOptionsChange(key: string, value: any): void {
|
||||
switch (key) {
|
||||
case 'minimumContrastRatio':
|
||||
this._contrastCache.clear();
|
||||
break;
|
||||
case 'allowTransparency':
|
||||
this.allowTransparency = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the terminal's theme.
|
||||
* @param theme The theme to use. If a partial theme is provided then default
|
||||
* colors will be used where colors are not defined.
|
||||
*/
|
||||
public setTheme(theme: ITheme = {}): void {
|
||||
this.colors.foreground = this._parseColor(theme.foreground, DEFAULT_FOREGROUND);
|
||||
this.colors.background = this._parseColor(theme.background, DEFAULT_BACKGROUND);
|
||||
this.colors.cursor = this._parseColor(theme.cursor, DEFAULT_CURSOR, true);
|
||||
this.colors.cursorAccent = this._parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT, true);
|
||||
this.colors.selectionBackgroundTransparent = this._parseColor(theme.selectionBackground, DEFAULT_SELECTION, true);
|
||||
this.colors.selectionBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionBackgroundTransparent);
|
||||
this.colors.selectionInactiveBackgroundTransparent = this._parseColor(theme.selectionInactiveBackground, this.colors.selectionBackgroundTransparent, true);
|
||||
this.colors.selectionInactiveBackgroundOpaque = color.blend(this.colors.background, this.colors.selectionInactiveBackgroundTransparent);
|
||||
const nullColor: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
this.colors.selectionForeground = theme.selectionForeground ? this._parseColor(theme.selectionForeground, nullColor) : undefined;
|
||||
if (this.colors.selectionForeground === nullColor) {
|
||||
this.colors.selectionForeground = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If selection color is opaque, blend it with background with 0.3 opacity
|
||||
* Issue #2737
|
||||
*/
|
||||
if (color.isOpaque(this.colors.selectionBackgroundTransparent)) {
|
||||
const opacity = 0.3;
|
||||
this.colors.selectionBackgroundTransparent = color.opacity(this.colors.selectionBackgroundTransparent, opacity);
|
||||
}
|
||||
if (color.isOpaque(this.colors.selectionInactiveBackgroundTransparent)) {
|
||||
const opacity = 0.3;
|
||||
this.colors.selectionInactiveBackgroundTransparent = color.opacity(this.colors.selectionInactiveBackgroundTransparent, opacity);
|
||||
}
|
||||
this.colors.ansi = DEFAULT_ANSI_COLORS.slice();
|
||||
this.colors.ansi[0] = this._parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
|
||||
this.colors.ansi[1] = this._parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
|
||||
this.colors.ansi[2] = this._parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
|
||||
this.colors.ansi[3] = this._parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
|
||||
this.colors.ansi[4] = this._parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
|
||||
this.colors.ansi[5] = this._parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
|
||||
this.colors.ansi[6] = this._parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
|
||||
this.colors.ansi[7] = this._parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
|
||||
this.colors.ansi[8] = this._parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
|
||||
this.colors.ansi[9] = this._parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
|
||||
this.colors.ansi[10] = this._parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
|
||||
this.colors.ansi[11] = this._parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
|
||||
this.colors.ansi[12] = this._parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
|
||||
this.colors.ansi[13] = this._parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
|
||||
this.colors.ansi[14] = this._parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
|
||||
this.colors.ansi[15] = this._parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
|
||||
if (theme.extendedAnsi) {
|
||||
const colorCount = Math.min(this.colors.ansi.length - 16, theme.extendedAnsi.length);
|
||||
for (let i = 0; i < colorCount; i++) {
|
||||
this.colors.ansi[i + 16] = this._parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
|
||||
}
|
||||
}
|
||||
// Clear our the cache
|
||||
this._contrastCache.clear();
|
||||
this._updateRestoreColors();
|
||||
}
|
||||
|
||||
public restoreColor(slot?: ColorIndex): void {
|
||||
// unset slot restores all ansi colors
|
||||
if (slot === undefined) {
|
||||
for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
|
||||
this.colors.ansi[i] = this._restoreColors.ansi[i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (slot) {
|
||||
case ColorIndex.FOREGROUND:
|
||||
this.colors.foreground = this._restoreColors.foreground;
|
||||
break;
|
||||
case ColorIndex.BACKGROUND:
|
||||
this.colors.background = this._restoreColors.background;
|
||||
break;
|
||||
case ColorIndex.CURSOR:
|
||||
this.colors.cursor = this._restoreColors.cursor;
|
||||
break;
|
||||
default:
|
||||
this.colors.ansi[slot] = this._restoreColors.ansi[slot];
|
||||
}
|
||||
}
|
||||
|
||||
private _updateRestoreColors(): void {
|
||||
this._restoreColors = {
|
||||
foreground: this.colors.foreground,
|
||||
background: this.colors.background,
|
||||
cursor: this.colors.cursor,
|
||||
ansi: this.colors.ansi.slice()
|
||||
};
|
||||
}
|
||||
|
||||
private _parseColor(
|
||||
css: string | undefined,
|
||||
fallback: IColor,
|
||||
allowTransparency: boolean = this.allowTransparency
|
||||
): IColor {
|
||||
if (css === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// If parsing the value results in failure, then it must be ignored, and the attribute must
|
||||
// retain its previous value.
|
||||
// -- https://html.spec.whatwg.org/multipage/canvas.html#fill-and-stroke-styles
|
||||
this._ctx.fillStyle = this._litmusColor;
|
||||
this._ctx.fillStyle = css;
|
||||
if (typeof this._ctx.fillStyle !== 'string') {
|
||||
console.warn(`Color: ${css} is invalid using fallback ${fallback.css}`);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
this._ctx.fillRect(0, 0, 1, 1);
|
||||
const data = this._ctx.getImageData(0, 0, 1, 1).data;
|
||||
|
||||
// Check if the printed color was transparent
|
||||
if (data[3] !== 0xFF) {
|
||||
if (!allowTransparency) {
|
||||
// Ideally we'd just ignore the alpha channel, but...
|
||||
//
|
||||
// Browsers may not give back exactly the same RGB values we put in, because most/all
|
||||
// convert the color to a pre-multiplied representation. getImageData converts that back to
|
||||
// a un-premultipled representation, but the precision loss may make the RGB channels unuable
|
||||
// on their own.
|
||||
//
|
||||
// E.g. In Chrome #12345610 turns into #10305010, and in the extreme case, 0xFFFFFF00 turns
|
||||
// into 0x00000000.
|
||||
//
|
||||
// "Note: Due to the lossy nature of converting to and from premultiplied alpha color values,
|
||||
// pixels that have just been set using putImageData() might be returned to an equivalent
|
||||
// getImageData() as different values."
|
||||
// -- https://html.spec.whatwg.org/multipage/canvas.html#pixel-manipulation
|
||||
//
|
||||
// So let's just use the fallback color in this case instead.
|
||||
console.warn(
|
||||
`Color: ${css} is using transparency, but allowTransparency is false. ` +
|
||||
`Using fallback ${fallback.css}.`
|
||||
);
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
|
||||
// the color value has alpha less than 1.0, and the string is the color value in the CSS rgba()
|
||||
const [r, g, b, a] = this._ctx.fillStyle.substring(5, this._ctx.fillStyle.length - 1).split(',').map(component => Number(component));
|
||||
const alpha = Math.round(a * 255);
|
||||
const rgba: number = channels.toRgba(r, g, b, alpha);
|
||||
return {
|
||||
rgba,
|
||||
css
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
// https://html.spec.whatwg.org/multipage/canvas.html#serialisation-of-a-color
|
||||
// if it has alpha equal to 1.0, then the string is a lowercase six-digit hex value, prefixed with a "#" character
|
||||
css: this._ctx.fillStyle,
|
||||
rgba: channels.toRgba(data[0], data[1], data[2], data[3])
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@ import { IDisposable } from 'common/Types';
|
||||
|
||||
/**
|
||||
* Adds a disposable listener to a node in the DOM, returning the disposable.
|
||||
* @param node The node to add a listener to.
|
||||
* @param type The event type.
|
||||
* @param handler The handler for the listener.
|
||||
* @param options The boolean or options object to pass on to the event
|
||||
* listener.
|
||||
*/
|
||||
export function addDisposableDomListener(
|
||||
node: Element | Window | Document,
|
||||
|
||||
+24
-26
@@ -8,7 +8,7 @@ import { IDisposable } from 'common/Types';
|
||||
import { IMouseService, IRenderService } from './services/Services';
|
||||
import { IBufferService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable, getDisposeArrayDisposable, disposeArray } from 'common/Lifecycle';
|
||||
import { Disposable, getDisposeArrayDisposable, disposeArray, toDisposable } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
|
||||
export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
@@ -26,21 +26,19 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
private _activeProviderReplies: Map<Number, ILinkWithState[] | undefined> | undefined;
|
||||
private _activeLine: number = -1;
|
||||
|
||||
private _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public get onShowLinkUnderline(): IEvent<ILinkifierEvent> { return this._onShowLinkUnderline.event; }
|
||||
private _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public get onHideLinkUnderline(): IEvent<ILinkifierEvent> { return this._onHideLinkUnderline.event; }
|
||||
private readonly _onShowLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public readonly onShowLinkUnderline = this._onShowLinkUnderline.event;
|
||||
private readonly _onHideLinkUnderline = this.register(new EventEmitter<ILinkifierEvent>());
|
||||
public readonly onHideLinkUnderline = this._onHideLinkUnderline.event;
|
||||
|
||||
constructor(
|
||||
@IBufferService private readonly _bufferService: IBufferService
|
||||
) {
|
||||
super();
|
||||
this.register(getDisposeArrayDisposable(this._linkCacheDisposables));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this._lastMouseEvent = undefined;
|
||||
this.register(toDisposable(() => {
|
||||
this._lastMouseEvent = undefined;
|
||||
}));
|
||||
}
|
||||
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
@@ -66,12 +64,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
this._isMouseOut = true;
|
||||
this._clearCurrentLink();
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._element, 'mousemove', this._onMouseMove.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mousemove', this._handleMouseMove.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mousedown', this._handleMouseDown.bind(this)));
|
||||
this.register(addDisposableDomListener(this._element, 'mouseup', this._handleMouseUp.bind(this)));
|
||||
}
|
||||
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
private _handleMouseMove(event: MouseEvent): void {
|
||||
this._lastMouseEvent = event;
|
||||
|
||||
if (!this._element || !this._mouseService) {
|
||||
@@ -99,12 +97,12 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
}
|
||||
|
||||
if (!this._lastBufferCell || (position.x !== this._lastBufferCell.x || position.y !== this._lastBufferCell.y)) {
|
||||
this._onHover(position);
|
||||
this._handleHover(position);
|
||||
this._lastBufferCell = position;
|
||||
}
|
||||
}
|
||||
|
||||
private _onHover(position: IBufferCellPosition): void {
|
||||
private _handleHover(position: IBufferCellPosition): void {
|
||||
// TODO: This currently does not cache link provider results across wrapped lines, activeLine should be something like `activeRange: {startY, endY}`
|
||||
// Check if we need to clear the link
|
||||
if (this._activeLine !== position.y) {
|
||||
@@ -317,7 +315,15 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
// When start is 0 a scroll most likely occurred, make sure links above the fold also get
|
||||
// cleared.
|
||||
const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp;
|
||||
const oldEvent = this._currentLink ? this._lastMouseEvent : undefined;
|
||||
this._clearCurrentLink(start, e.end + 1 + this._bufferService.buffer.ydisp);
|
||||
if (oldEvent && this._element) {
|
||||
// re-eval previously active link after changes
|
||||
const position = this._positionFromMouseEvent(oldEvent, this._element, this._mouseService!);
|
||||
if (position) {
|
||||
this._askForLink(position, false);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -369,18 +375,10 @@ export class Linkifier2 extends Disposable implements ILinkifier2 {
|
||||
* @param position
|
||||
*/
|
||||
private _linkAtPosition(link: ILink, position: IBufferCellPosition): boolean {
|
||||
const sameLine = link.range.start.y === link.range.end.y;
|
||||
const wrappedFromLeft = link.range.start.y < position.y;
|
||||
const wrappedToRight = link.range.end.y > position.y;
|
||||
|
||||
// If the start and end have the same y, then the position must be between start and end x
|
||||
// If not, then handle each case seperately, depending on which way it wraps
|
||||
return ((sameLine && link.range.start.x <= position.x && link.range.end.x >= position.x) ||
|
||||
(wrappedFromLeft && link.range.end.x >= position.x) ||
|
||||
(wrappedToRight && link.range.start.x <= position.x) ||
|
||||
(wrappedFromLeft && wrappedToRight)) &&
|
||||
link.range.start.y <= position.y &&
|
||||
link.range.end.y >= position.y;
|
||||
const lower = link.range.start.y * this._bufferService.cols + link.range.start.x;
|
||||
const upper = link.range.end.y * this._bufferService.cols + link.range.end.x;
|
||||
const current = position.y * this._bufferService.cols + position.x;
|
||||
return (lower <= current && current <= upper);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -66,14 +66,30 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
y
|
||||
}
|
||||
};
|
||||
// OSC links always use underline and pointer decorations
|
||||
result.push({
|
||||
text,
|
||||
range,
|
||||
activate: (e, text) => (linkHandler ? linkHandler.activate(e, text, range) : defaultActivate(e, text)),
|
||||
hover: (e, text) => linkHandler?.hover?.(e, text, range),
|
||||
leave: (e, text) => linkHandler?.leave?.(e, text, range)
|
||||
});
|
||||
|
||||
let ignoreLink = false;
|
||||
if (!linkHandler?.allowNonHttpProtocols) {
|
||||
try {
|
||||
const parsed = new URL(text);
|
||||
if (!['http:', 'https:'].includes(parsed.protocol)) {
|
||||
ignoreLink = true;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore invalid URLs to prevent unexpected behaviors
|
||||
ignoreLink = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ignoreLink) {
|
||||
// OSC links always use underline and pointer decorations
|
||||
result.push({
|
||||
text,
|
||||
range,
|
||||
activate: (e, text) => (linkHandler ? linkHandler.activate(e, text, range) : defaultActivate(e, text)),
|
||||
hover: (e, text) => linkHandler?.hover?.(e, text, range),
|
||||
leave: (e, text) => linkHandler?.leave?.(e, text, range)
|
||||
});
|
||||
}
|
||||
}
|
||||
finishLink = false;
|
||||
|
||||
@@ -94,7 +110,7 @@ export class OscLinkProvider implements ILinkProvider {
|
||||
}
|
||||
|
||||
function defaultActivate(e: MouseEvent, uri: string): void {
|
||||
const answer = confirm(`Do you want to navigate to ${uri}?`);
|
||||
const answer = confirm(`Do you want to navigate to ${uri}?\n\nWARNING: This link could potentially be dangerous`);
|
||||
if (answer) {
|
||||
const newWindow = window.open();
|
||||
if (newWindow) {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
|
||||
export type ScreenDprListener = (newDevicePixelRatio?: number, oldDevicePixelRatio?: number) => void;
|
||||
|
||||
@@ -26,6 +26,9 @@ export class ScreenDprMonitor extends Disposable {
|
||||
constructor(private _parentWindow: Window) {
|
||||
super();
|
||||
this._currentDevicePixelRatio = this._parentWindow.devicePixelRatio;
|
||||
this.register(toDisposable(() => {
|
||||
this.clearListener();
|
||||
}));
|
||||
}
|
||||
|
||||
public setListener(listener: ScreenDprListener): void {
|
||||
@@ -43,11 +46,6 @@ export class ScreenDprMonitor extends Disposable {
|
||||
this._updateDpr();
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
this.clearListener();
|
||||
}
|
||||
|
||||
private _updateDpr(): void {
|
||||
if (!this._outerListener) {
|
||||
return;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockUnicodeService } from 'common/TestUtils.test';
|
||||
import { IMarker } from 'common/Types';
|
||||
import { ICoreService } from 'common/services/Services';
|
||||
|
||||
const INIT_COLS = 80;
|
||||
const INIT_ROWS = 24;
|
||||
@@ -47,11 +48,10 @@ describe('Terminal', () => {
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
// TODO: Add an onData test back
|
||||
// it('should fire the onData evnet', (done) => {
|
||||
// term.onData(() => done());
|
||||
// term.handler('fake');
|
||||
// });
|
||||
it('should fire the onData evnet', (done) => {
|
||||
term.onData(() => done());
|
||||
term.coreService.triggerDataEvent('fake');
|
||||
});
|
||||
it('should fire the onCursorMove event', () => {
|
||||
return new Promise<void>(async r => {
|
||||
term.onCursorMove(() => r());
|
||||
@@ -1378,7 +1378,6 @@ describe('Terminal', () => {
|
||||
assert.deepEqual(disposeStack, [markers[0], markers[1]]);
|
||||
// trimmed marker objs should be disposed
|
||||
assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]);
|
||||
assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]);
|
||||
// trimmed markers should contain line -1
|
||||
assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]);
|
||||
});
|
||||
|
||||
+102
-143
@@ -22,7 +22,7 @@
|
||||
*/
|
||||
|
||||
import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
|
||||
import { IRenderer } from 'browser/renderer/Types';
|
||||
import { IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { CompositionHelper } from 'browser/input/CompositionHelper';
|
||||
import { Viewport } from 'browser/Viewport';
|
||||
import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard';
|
||||
@@ -39,9 +39,8 @@ import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseActio
|
||||
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
|
||||
import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter';
|
||||
import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
|
||||
import { ColorManager } from 'browser/ColorManager';
|
||||
import { RenderService } from 'browser/services/RenderService';
|
||||
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services';
|
||||
import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService, IThemeService } from 'browser/services/Services';
|
||||
import { CharSizeService } from 'browser/services/CharSizeService';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { MouseService } from 'browser/services/MouseService';
|
||||
@@ -56,6 +55,8 @@ import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
import { OscLinkProvider } from 'browser/OscLinkProvider';
|
||||
import { toDisposable } from 'common/Lifecycle';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
// Let it work inside Node.js for automated testing purposes.
|
||||
const document: Document = (typeof window !== 'undefined') ? window.document : null as any;
|
||||
@@ -85,6 +86,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
private _coreBrowserService: ICoreBrowserService | undefined;
|
||||
private _mouseService: IMouseService | undefined;
|
||||
private _renderService: IRenderService | undefined;
|
||||
private _themeService: IThemeService | undefined;
|
||||
private _characterJoinerService: ICharacterJoinerService | undefined;
|
||||
private _selectionService: ISelectionService | undefined;
|
||||
|
||||
@@ -119,30 +121,30 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
public viewport: IViewport | undefined;
|
||||
private _compositionHelper: ICompositionHelper | undefined;
|
||||
private _accessibilityManager: AccessibilityManager | undefined;
|
||||
private _colorManager: ColorManager | undefined;
|
||||
private _theme: ITheme | undefined;
|
||||
|
||||
private _onCursorMove = new EventEmitter<void>();
|
||||
public get onCursorMove(): IEvent<void> { return this._onCursorMove.event; }
|
||||
private _onKey = new EventEmitter<{ key: string, domEvent: KeyboardEvent }>();
|
||||
public get onKey(): IEvent<{ key: string, domEvent: KeyboardEvent }> { return this._onKey.event; }
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onSelectionChange = new EventEmitter<void>();
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
private _onTitleChange = new EventEmitter<string>();
|
||||
public get onTitleChange(): IEvent<string> { return this._onTitleChange.event; }
|
||||
private _onBell = new EventEmitter<void>();
|
||||
public get onBell(): IEvent<void> { return this._onBell.event; }
|
||||
private readonly _onCursorMove = this.register(new EventEmitter<void>());
|
||||
public readonly onCursorMove = this._onCursorMove.event;
|
||||
private readonly _onKey = this.register(new EventEmitter<{ key: string, domEvent: KeyboardEvent }>());
|
||||
public readonly onKey = this._onKey.event;
|
||||
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
|
||||
public readonly onRender = this._onRender.event;
|
||||
private readonly _onSelectionChange = this.register(new EventEmitter<void>());
|
||||
public readonly onSelectionChange = this._onSelectionChange.event;
|
||||
private readonly _onTitleChange = this.register(new EventEmitter<string>());
|
||||
public readonly onTitleChange = this._onTitleChange.event;
|
||||
private readonly _onBell = this.register(new EventEmitter<void>());
|
||||
public readonly onBell = this._onBell.event;
|
||||
|
||||
private _onFocus = new EventEmitter<void>();
|
||||
private _onFocus = this.register(new EventEmitter<void>());
|
||||
public get onFocus(): IEvent<void> { return this._onFocus.event; }
|
||||
private _onBlur = new EventEmitter<void>();
|
||||
private _onBlur = this.register(new EventEmitter<void>());
|
||||
public get onBlur(): IEvent<void> { return this._onBlur.event; }
|
||||
private _onA11yCharEmitter = new EventEmitter<string>();
|
||||
private _onA11yCharEmitter = this.register(new EventEmitter<string>());
|
||||
public get onA11yChar(): IEvent<string> { return this._onA11yCharEmitter.event; }
|
||||
private _onA11yTabEmitter = new EventEmitter<number>();
|
||||
private _onA11yTabEmitter = this.register(new EventEmitter<number>());
|
||||
public get onA11yTab(): IEvent<number> { return this._onA11yTabEmitter.event; }
|
||||
private _onWillOpen = this.register(new EventEmitter<HTMLElement>());
|
||||
public get onWillOpen(): IEvent<HTMLElement> { return this._onWillOpen.event; }
|
||||
|
||||
/**
|
||||
* Creates a new `Terminal` object.
|
||||
@@ -182,6 +184,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
// Setup listeners
|
||||
this.register(this._bufferService.onResize(e => this._afterResize(e.cols, e.rows)));
|
||||
|
||||
this.register(toDisposable(() => {
|
||||
this._customKeyEventHandler = undefined;
|
||||
this.element?.parentNode?.removeChild(this.element);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -191,9 +198,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
* while an event from OSC 10|110 | 11|111 | 12|112 always contains a single request.
|
||||
*/
|
||||
private _handleColorEvent(event: IColorEvent): void {
|
||||
if (!this._colorManager) return;
|
||||
if (!this._themeService) return;
|
||||
for (const req of event) {
|
||||
let acc: 'foreground' | 'background' | 'cursor' | 'ansi' | undefined = undefined;
|
||||
let acc: 'foreground' | 'background' | 'cursor' | 'ansi';
|
||||
let ident = '';
|
||||
switch (req.index) {
|
||||
case ColorIndex.FOREGROUND: // OSC 10 | 110
|
||||
@@ -216,32 +223,23 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
switch (req.type) {
|
||||
case ColorRequestType.REPORT:
|
||||
const channels = color.toColorRGB(acc === 'ansi'
|
||||
? this._colorManager.colors.ansi[req.index]
|
||||
: this._colorManager.colors[acc]);
|
||||
? this._themeService.colors.ansi[req.index]
|
||||
: this._themeService.colors[acc]);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`);
|
||||
break;
|
||||
case ColorRequestType.SET:
|
||||
if (acc === 'ansi') this._colorManager.colors.ansi[req.index] = rgba.toColor(...req.color);
|
||||
else this._colorManager.colors[acc] = rgba.toColor(...req.color);
|
||||
if (acc === 'ansi') {
|
||||
this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color));
|
||||
} else {
|
||||
const narrowedAcc = acc;
|
||||
this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color));
|
||||
}
|
||||
break;
|
||||
case ColorRequestType.RESTORE:
|
||||
this._colorManager.restoreColor(req.index);
|
||||
this._themeService.restoreColor(req.index);
|
||||
break;
|
||||
}
|
||||
}
|
||||
this._renderService?.setColors(this._colorManager.colors);
|
||||
this.viewport?.onThemeChange(this._colorManager.colors);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._isDisposed) {
|
||||
return;
|
||||
}
|
||||
super.dispose();
|
||||
this._renderService?.dispose();
|
||||
this._customKeyEventHandler = undefined;
|
||||
this.write = () => { };
|
||||
this.element?.parentNode?.removeChild(this.element);
|
||||
}
|
||||
|
||||
protected _setup(): void {
|
||||
@@ -266,60 +264,21 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
}
|
||||
|
||||
protected _updateOptions(key: string): void {
|
||||
super._updateOptions(key);
|
||||
|
||||
// TODO: These listeners should be owned by individual components
|
||||
switch (key) {
|
||||
case 'fontFamily':
|
||||
case 'fontSize':
|
||||
// When the font changes the size of the cells may change which requires a renderer clear
|
||||
this._renderService?.clear();
|
||||
this._charSizeService?.measure();
|
||||
break;
|
||||
case 'cursorBlink':
|
||||
case 'cursorStyle':
|
||||
// The DOM renderer needs a row refresh to update the cursor styles
|
||||
this.refresh(this.buffer.y, this.buffer.y);
|
||||
break;
|
||||
case 'customGlyphs':
|
||||
case 'drawBoldTextInBrightColors':
|
||||
case 'letterSpacing':
|
||||
case 'lineHeight':
|
||||
case 'fontWeight':
|
||||
case 'fontWeightBold':
|
||||
case 'minimumContrastRatio':
|
||||
// When the font changes the size of the cells may change which requires a renderer clear
|
||||
if (this._renderService) {
|
||||
this._renderService.clear();
|
||||
this._renderService.onResize(this.cols, this.rows);
|
||||
this.refresh(0, this.rows - 1);
|
||||
}
|
||||
break;
|
||||
case 'scrollback':
|
||||
this.viewport?.syncScrollArea();
|
||||
break;
|
||||
case 'screenReaderMode':
|
||||
if (this.optionsService.rawOptions.screenReaderMode) {
|
||||
if (!this._accessibilityManager && this._renderService) {
|
||||
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
|
||||
}
|
||||
} else {
|
||||
this._accessibilityManager?.dispose();
|
||||
this._accessibilityManager = undefined;
|
||||
}
|
||||
break;
|
||||
case 'tabStopWidth': this.buffers.setupTabStops(); break;
|
||||
case 'theme':
|
||||
this._setTheme(this.optionsService.rawOptions.theme);
|
||||
break;
|
||||
private _handleScreenReaderModeOptionChange(value: boolean): void {
|
||||
if (value) {
|
||||
if (!this._accessibilityManager && this._renderService) {
|
||||
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
|
||||
}
|
||||
} else {
|
||||
this._accessibilityManager?.dispose();
|
||||
this._accessibilityManager = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Binds the desired focus behavior on a given terminal object.
|
||||
*/
|
||||
private _onTextAreaFocus(ev: KeyboardEvent): void {
|
||||
private _handleTextAreaFocus(ev: KeyboardEvent): void {
|
||||
if (this.coreService.decPrivateModes.sendFocus) {
|
||||
this.coreService.triggerDataEvent(C0.ESC + '[I');
|
||||
}
|
||||
@@ -340,7 +299,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
/**
|
||||
* Binds the desired blur behavior on a given terminal object.
|
||||
*/
|
||||
private _onTextAreaBlur(): void {
|
||||
private _handleTextAreaBlur(): void {
|
||||
// Text can safely be removed on blur. Doing it earlier could interfere with
|
||||
// screen readers reading it out.
|
||||
this.textarea!.value = '';
|
||||
@@ -362,11 +321,11 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
return;
|
||||
}
|
||||
const cursorX = Math.min(this.buffer.x, this.cols - 1);
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight;
|
||||
const cellHeight = this._renderService.dimensions.css.cell.height;
|
||||
const width = bufferLine.getWidth(cursorX);
|
||||
const cellWidth = this._renderService.dimensions.actualCellWidth * width;
|
||||
const cursorTop = this.buffer.y * this._renderService.dimensions.actualCellHeight;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
|
||||
const cellWidth = this._renderService.dimensions.css.cell.width * width;
|
||||
const cursorTop = this.buffer.y * this._renderService.dimensions.css.cell.height;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;
|
||||
|
||||
// Sync the textarea to the exact position of the composition view so the IME knows where the
|
||||
// text is.
|
||||
@@ -486,31 +445,36 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.textarea = document.createElement('textarea');
|
||||
this.textarea.classList.add('xterm-helper-textarea');
|
||||
this.textarea.setAttribute('aria-label', Strings.promptLabel);
|
||||
this.textarea.setAttribute('aria-multiline', 'false');
|
||||
if (!Browser.isChromeOS) {
|
||||
// ChromeVox on ChromeOS does not like this. See
|
||||
// https://issuetracker.google.com/issues/260170397
|
||||
this.textarea.setAttribute('aria-multiline', 'false');
|
||||
}
|
||||
this.textarea.setAttribute('autocorrect', 'off');
|
||||
this.textarea.setAttribute('autocapitalize', 'off');
|
||||
this.textarea.setAttribute('spellcheck', 'false');
|
||||
this.textarea.tabIndex = 0;
|
||||
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._onTextAreaFocus(ev)));
|
||||
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._onTextAreaBlur()));
|
||||
this._helperContainer.appendChild(this.textarea);
|
||||
|
||||
// Register the core browser service before the generic textarea handlers are registered so it
|
||||
// handles them first. Otherwise the renderers may use the wrong focus state.
|
||||
this._coreBrowserService = this._instantiationService.createInstance(CoreBrowserService, this.textarea, this._document.defaultView ?? window);
|
||||
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
|
||||
|
||||
this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev)));
|
||||
this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur()));
|
||||
this._helperContainer.appendChild(this.textarea);
|
||||
|
||||
|
||||
this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer);
|
||||
this._instantiationService.setService(ICharSizeService, this._charSizeService);
|
||||
|
||||
this._theme = this.options.theme || this._theme;
|
||||
this._colorManager = new ColorManager(document, this.options.allowTransparency);
|
||||
this.register(this.optionsService.onOptionChange(e => this._colorManager!.onOptionsChange(e, this.optionsService.rawOptions[e])));
|
||||
this._colorManager.setTheme(this._theme);
|
||||
this._themeService = this._instantiationService.createInstance(ThemeService);
|
||||
this._instantiationService.setService(IThemeService, this._themeService);
|
||||
|
||||
this._characterJoinerService = this._instantiationService.createInstance(CharacterJoinerService);
|
||||
this._instantiationService.setService(ICharacterJoinerService, this._characterJoinerService);
|
||||
|
||||
const renderer = this._createRenderer();
|
||||
this._renderService = this.register(this._instantiationService.createInstance(RenderService, renderer, this.rows, this.screenElement));
|
||||
this._renderService = this.register(this._instantiationService.createInstance(RenderService, this.rows, this.screenElement));
|
||||
this._instantiationService.setService(IRenderService, this._renderService);
|
||||
this.register(this._renderService.onRenderedViewportChange(e => this._onRender.fire(e)));
|
||||
this.onResize(e => this._renderService!.resize(e.cols, e.rows));
|
||||
@@ -523,26 +487,32 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// Performance: Add viewport and helper elements from the fragment
|
||||
this.element.appendChild(fragment);
|
||||
|
||||
try {
|
||||
this._onWillOpen.fire(this.element);
|
||||
}
|
||||
catch { /* fails to load addon for some reason */ }
|
||||
if (!this._renderService.hasRenderer()) {
|
||||
this._renderService.setRenderer(this._createRenderer());
|
||||
}
|
||||
|
||||
this._mouseService = this._instantiationService.createInstance(MouseService);
|
||||
this._instantiationService.setService(IMouseService, this._mouseService);
|
||||
|
||||
this.viewport = this._instantiationService.createInstance(Viewport,
|
||||
(amount: number) => this.scrollLines(amount, true, ScrollSource.VIEWPORT),
|
||||
this._viewportElement,
|
||||
this._viewportScrollArea,
|
||||
this.element
|
||||
this._viewportScrollArea
|
||||
);
|
||||
this.viewport.onThemeChange(this._colorManager.colors);
|
||||
this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea()));
|
||||
this.register(this.viewport);
|
||||
|
||||
this.register(this.onCursorMove(() => {
|
||||
this._renderService!.onCursorMove();
|
||||
this._renderService!.handleCursorMove();
|
||||
this._syncTextArea();
|
||||
}));
|
||||
this.register(this.onResize(() => this._renderService!.onResize(this.cols, this.rows)));
|
||||
this.register(this.onBlur(() => this._renderService!.onBlur()));
|
||||
this.register(this.onFocus(() => this._renderService!.onFocus()));
|
||||
this.register(this.onResize(() => this._renderService!.handleResize(this.cols, this.rows)));
|
||||
this.register(this.onBlur(() => this._renderService!.handleBlur()));
|
||||
this.register(this.onFocus(() => this._renderService!.handleFocus()));
|
||||
this.register(this._renderService.onDimensionsChange(() => this.viewport!.syncScrollArea()));
|
||||
|
||||
this._selectionService = this.register(this._instantiationService.createInstance(SelectionService,
|
||||
@@ -553,7 +523,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this._instantiationService.setService(ISelectionService, this._selectionService);
|
||||
this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent)));
|
||||
this.register(this._selectionService.onSelectionChange(() => this._onSelectionChange.fire()));
|
||||
this.register(this._selectionService.onRequestRedraw(e => this._renderService!.onSelectionChanged(e.start, e.end, e.columnSelectMode)));
|
||||
this.register(this._selectionService.onRequestRedraw(e => this._renderService!.handleSelectionChanged(e.start, e.end, e.columnSelectMode)));
|
||||
this.register(this._selectionService.onLinuxMouseSelection(text => {
|
||||
// If there's a new selection, put it into the textarea, focus and select it
|
||||
// in order to register it as a selection on the OS. This event is fired
|
||||
@@ -570,7 +540,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService);
|
||||
this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement));
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e)));
|
||||
this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e)));
|
||||
|
||||
// apply mouse event classes set by escape codes before terminal was attached
|
||||
if (this.coreMouseService.areMouseEventsActive) {
|
||||
@@ -585,12 +555,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
// ensure the correct order of the dprchange event
|
||||
this._accessibilityManager = new AccessibilityManager(this, this._renderService);
|
||||
}
|
||||
this.register(this.optionsService.onSpecificOptionChange('screenReaderMode', e => this._handleScreenReaderModeOptionChange(e)));
|
||||
|
||||
if (this.options.overviewRulerWidth) {
|
||||
this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
|
||||
}
|
||||
this.optionsService.onOptionChange(() => {
|
||||
if (!this._overviewRulerRenderer && this.options.overviewRulerWidth && this._viewportElement && this.screenElement) {
|
||||
this.optionsService.onSpecificOptionChange('overviewRulerWidth', value => {
|
||||
if (!this._overviewRulerRenderer && value && this._viewportElement && this.screenElement) {
|
||||
this._overviewRulerRenderer = this.register(this._instantiationService.createInstance(OverviewRulerRenderer, this._viewportElement, this.screenElement));
|
||||
}
|
||||
});
|
||||
@@ -609,18 +580,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
private _createRenderer(): IRenderer {
|
||||
return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the theme on the renderer. The renderer must have been initialized.
|
||||
* @param theme The theme to set.
|
||||
*/
|
||||
private _setTheme(theme: ITheme): void {
|
||||
this._theme = theme;
|
||||
this._colorManager?.setTheme(theme);
|
||||
this._renderService?.setColors(this._colorManager!.colors);
|
||||
this.viewport?.onThemeChange(this._colorManager!.colors);
|
||||
return this._instantiationService.createInstance(DomRenderer, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -859,20 +819,20 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
// normal viewport scrolling
|
||||
// conditionally stop event, if the viewport still had rows to scroll within
|
||||
if (this.viewport!.onWheel(ev)) {
|
||||
if (this.viewport!.handleWheel(ev)) {
|
||||
return this.cancel(ev);
|
||||
}
|
||||
}, { passive: false }));
|
||||
|
||||
this.register(addDisposableDomListener(el, 'touchstart', (ev: TouchEvent) => {
|
||||
if (this.coreMouseService.areMouseEventsActive) return;
|
||||
this.viewport!.onTouchStart(ev);
|
||||
this.viewport!.handleTouchStart(ev);
|
||||
return this.cancel(ev);
|
||||
}, { passive: true }));
|
||||
|
||||
this.register(addDisposableDomListener(el, 'touchmove', (ev: TouchEvent) => {
|
||||
if (this.coreMouseService.areMouseEventsActive) return;
|
||||
if (!this.viewport!.onTouchMove(ev)) {
|
||||
if (!this.viewport!.handleTouchMove(ev)) {
|
||||
return this.cancel(ev);
|
||||
}
|
||||
}, { passive: false }));
|
||||
@@ -1027,10 +987,9 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a keydown event
|
||||
* Key Resources:
|
||||
* - https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||
* @param ev The keydown event to be handled.
|
||||
* Handle a keydown [KeyboardEvent].
|
||||
*
|
||||
* [KeyboardEvent]: https://developer.mozilla.org/en-US/docs/DOM/KeyboardEvent
|
||||
*/
|
||||
protected _keyDown(event: KeyboardEvent): boolean | undefined {
|
||||
this._keyDownHandled = false;
|
||||
@@ -1044,7 +1003,7 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
const shouldIgnoreComposition = this.browser.isMac && this.options.macOptionIsMeta && event.altKey;
|
||||
|
||||
if (!shouldIgnoreComposition && !this._compositionHelper!.keydown(event)) {
|
||||
if (this.buffer.ybase !== this.buffer.ydisp) {
|
||||
if (this.options.scrollOnUserInput && this.buffer.ybase !== this.buffer.ydisp) {
|
||||
this._bufferService.scrollToBottom();
|
||||
}
|
||||
return false;
|
||||
@@ -1106,10 +1065,10 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
this.coreService.triggerDataEvent(result.key, true);
|
||||
|
||||
// Cancel events when not in screen reader mode so events don't get bubbled up and handled by
|
||||
// other listeners. When screen reader mode is enabled, this could cause issues if the event
|
||||
// is handled at a higher level, this is a compromise in order to echo keys to the screen
|
||||
// reader.
|
||||
if (!this.optionsService.rawOptions.screenReaderMode) {
|
||||
// other listeners. When screen reader mode is enabled, we don't cancel them (unless ctrl or alt
|
||||
// is also depressed) so that the cursor textarea can be updated, which triggers the screen
|
||||
// reader to read it.
|
||||
if (!this.optionsService.rawOptions.screenReaderMode || event.altKey || event.ctrlKey) {
|
||||
return this.cancel(event, true);
|
||||
}
|
||||
|
||||
@@ -1322,13 +1281,13 @@ export class Terminal extends CoreTerminal implements ITerminal {
|
||||
|
||||
switch (type) {
|
||||
case WindowsOptionsReportType.GET_WIN_SIZE_PIXELS:
|
||||
const canvasWidth = this._renderService.dimensions.canvasWidth.toFixed(0);
|
||||
const canvasHeight = this._renderService.dimensions.canvasHeight.toFixed(0);
|
||||
const canvasWidth = this._renderService.dimensions.css.canvas.width.toFixed(0);
|
||||
const canvasHeight = this._renderService.dimensions.css.canvas.height.toFixed(0);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[4;${canvasHeight};${canvasWidth}t`);
|
||||
break;
|
||||
case WindowsOptionsReportType.GET_CELL_SIZE_PIXELS:
|
||||
const cellWidth = this._renderService.dimensions.actualCellWidth.toFixed(0);
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight.toFixed(0);
|
||||
const cellWidth = this._renderService.dimensions.css.cell.width.toFixed(0);
|
||||
const cellHeight = this._renderService.dimensions.css.cell.height.toFixed(0);
|
||||
this.coreService.triggerDataEvent(`${C0.ESC}[6;${cellHeight};${cellWidth}t`);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
|
||||
import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IBufferRange } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICharSizeService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, ICompositionHelper, CharacterJoinerHandler, IBufferRange, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types';
|
||||
import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions, ColorIndex } from 'common/Types';
|
||||
import { Buffer } from 'common/buffer/Buffer';
|
||||
import * as Browser from 'common/Platform';
|
||||
import { Terminal } from 'browser/Terminal';
|
||||
@@ -17,6 +17,8 @@ import { IUnicodeService, IOptionsService, ICoreService, ICoreMouseService } fro
|
||||
import { IFunctionIdentifier, IParams } from 'common/parser/Types';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { ISelectionRedrawRequestEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { css } from 'common/Color';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
export class TestTerminal extends Terminal {
|
||||
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
|
||||
@@ -41,6 +43,7 @@ export class MockTerminal implements ITerminal {
|
||||
public onTitleChange!: IEvent<string>;
|
||||
public onBell!: IEvent<void>;
|
||||
public onScroll!: IEvent<number>;
|
||||
public onWillOpen!: IEvent<HTMLElement>;
|
||||
public onKey!: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
public onRender!: IEvent<{ start: number, end: number }>;
|
||||
public onResize!: IEvent<{ cols: number, rows: number }>;
|
||||
@@ -264,7 +267,6 @@ export class MockRenderer implements IRenderer {
|
||||
public dispose(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public colorManager!: IColorManager;
|
||||
public on(type: string, listener: XtermListener): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -278,20 +280,17 @@ export class MockRenderer implements IRenderer {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public dimensions!: IRenderDimensions;
|
||||
public setColors(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public registerDecoration(decorationOptions: IDecorationOptions): IDecoration {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onResize(cols: number, rows: number): void { }
|
||||
public onCharSizeChanged(): void { }
|
||||
public onBlur(): void { }
|
||||
public onFocus(): void { }
|
||||
public onSelectionChanged(start: [number, number], end: [number, number]): void { }
|
||||
public onCursorMove(): void { }
|
||||
public onOptionsChanged(): void { }
|
||||
public onDevicePixelRatioChange(): void { }
|
||||
public handleResize(cols: number, rows: number): void { }
|
||||
public handleCharSizeChanged(): void { }
|
||||
public handleBlur(): void { }
|
||||
public handleFocus(): void { }
|
||||
public handleSelectionChanged(start: [number, number], end: [number, number]): void { }
|
||||
public handleCursorMove(): void { }
|
||||
public handleOptionsChanged(): void { }
|
||||
public handleDevicePixelRatioChange(): void { }
|
||||
public clear(): void { }
|
||||
public renderRows(start: number, end: number): void { }
|
||||
}
|
||||
@@ -301,16 +300,16 @@ export class MockViewport implements IViewport {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public scrollBarWidth: number = 0;
|
||||
public onThemeChange(colors: IColorSet): void {
|
||||
public handleThemeChange(colors: IColorSet): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onWheel(ev: WheelEvent): boolean {
|
||||
public handleWheel(ev: WheelEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onTouchStart(ev: TouchEvent): void {
|
||||
public handleTouchStart(ev: TouchEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onTouchMove(ev: TouchEvent): boolean {
|
||||
public handleTouchMove(ev: TouchEvent): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public syncScrollArea(): void { }
|
||||
@@ -374,20 +373,7 @@ export class MockRenderService implements IRenderService {
|
||||
public onRenderedViewportChange: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRender: IEvent<{ start: number, end: number }, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public onRefreshRequest: IEvent<{ start: number, end: number}, void> = new EventEmitter<{ start: number, end: number }>().event;
|
||||
public dimensions: IRenderDimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
public dimensions: IRenderDimensions = createRenderDimensions();
|
||||
public refreshRows(start: number, end: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
@@ -400,31 +386,31 @@ export class MockRenderService implements IRenderService {
|
||||
public resize(cols: number, rows: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public hasRenderer(): boolean {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setRenderer(renderer: IRenderer): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public setColors(colors: IColorSet): void {
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onDevicePixelRatioChange(): void {
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onResize(cols: number, rows: number): void {
|
||||
public handleCharSizeChanged(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onCharSizeChanged(): void {
|
||||
public handleBlur(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onBlur(): void {
|
||||
public handleFocus(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onFocus(): void {
|
||||
public handleSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onSelectionChanged(start: [number, number], end: [number, number], columnSelectMode: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onCursorMove(): void {
|
||||
public handleCursorMove(): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public clear(): void {
|
||||
@@ -494,10 +480,45 @@ export class MockSelectionService implements ISelectionService {
|
||||
public refresh(isLinuxMouseSelection?: boolean): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
public handleMouseDown(event: MouseEvent): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public isCellInSelection(x: number, y: number): boolean {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export class MockThemeService implements IThemeService{
|
||||
public serviceBrand: undefined;
|
||||
public onChangeColors = new EventEmitter<ReadonlyColorSet>().event;
|
||||
public restoreColor(slot?: ColorIndex | undefined): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public modifyColors(callback: (colors: IColorSet) => void): void {
|
||||
throw new Error('Method not implemented.');
|
||||
}
|
||||
public colors: ReadonlyColorSet = {
|
||||
background: css.toColor('#010101'),
|
||||
foreground: css.toColor('#020202'),
|
||||
ansi: [
|
||||
// dark:
|
||||
css.toColor('#2e3436'),
|
||||
css.toColor('#cc0000'),
|
||||
css.toColor('#4e9a06'),
|
||||
css.toColor('#c4a000'),
|
||||
css.toColor('#3465a4'),
|
||||
css.toColor('#75507b'),
|
||||
css.toColor('#06989a'),
|
||||
css.toColor('#d3d7cf'),
|
||||
// bright:
|
||||
css.toColor('#555753'),
|
||||
css.toColor('#ef2929'),
|
||||
css.toColor('#8ae234'),
|
||||
css.toColor('#fce94f'),
|
||||
css.toColor('#729fcf'),
|
||||
css.toColor('#ad7fa8'),
|
||||
css.toColor('#34e2e2'),
|
||||
css.toColor('#eeeeec')
|
||||
]
|
||||
} as any;
|
||||
}
|
||||
|
||||
Vendored
+6
-9
@@ -23,6 +23,7 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal {
|
||||
onFocus: IEvent<void>;
|
||||
onA11yChar: IEvent<string>;
|
||||
onA11yTab: IEvent<number>;
|
||||
onWillOpen: IEvent<HTMLElement>;
|
||||
|
||||
cancel(ev: Event, force?: boolean): boolean | void;
|
||||
}
|
||||
@@ -105,11 +106,6 @@ export interface IBrowser {
|
||||
isWindows: boolean;
|
||||
}
|
||||
|
||||
export interface IColorManager {
|
||||
colors: IColorSet;
|
||||
onOptionsChange(key: string, value: any): void;
|
||||
}
|
||||
|
||||
export interface IColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
@@ -125,6 +121,8 @@ export interface IColorSet {
|
||||
contrastCache: IColorContrastCache;
|
||||
}
|
||||
|
||||
export type ReadonlyColorSet = Readonly<Omit<IColorSet, 'ansi'>> & { ansi: Readonly<Pick<IColorSet, 'ansi'>['ansi']> };
|
||||
|
||||
export interface IColorContrastCache {
|
||||
clear(): void;
|
||||
setCss(bg: number, fg: number, value: string | null): void;
|
||||
@@ -146,10 +144,9 @@ export interface IViewport extends IDisposable {
|
||||
scrollBarWidth: number;
|
||||
syncScrollArea(immediate?: boolean): void;
|
||||
getLinesScrolled(ev: WheelEvent): number;
|
||||
onWheel(ev: WheelEvent): boolean;
|
||||
onTouchStart(ev: TouchEvent): void;
|
||||
onTouchMove(ev: TouchEvent): boolean;
|
||||
onThemeChange(colors: IColorSet): void;
|
||||
handleWheel(ev: WheelEvent): boolean;
|
||||
handleTouchStart(ev: TouchEvent): void;
|
||||
handleTouchMove(ev: TouchEvent): boolean;
|
||||
}
|
||||
|
||||
export interface ILinkifierEvent {
|
||||
|
||||
+21
-17
@@ -5,11 +5,11 @@
|
||||
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IViewport } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { IColorSet, IViewport, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { IBufferService, IOptionsService } from 'common/services/Services';
|
||||
import { IBuffer } from 'common/buffer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
const FALLBACK_SCROLL_BAR_WIDTH = 15;
|
||||
|
||||
@@ -26,7 +26,7 @@ interface ISmoothScrollState {
|
||||
export class Viewport extends Disposable implements IViewport {
|
||||
public scrollBarWidth: number = 0;
|
||||
private _currentRowHeight: number = 0;
|
||||
private _currentScaledCellHeight: number = 0;
|
||||
private _currentDeviceCellHeight: number = 0;
|
||||
private _lastRecordedBufferLength: number = 0;
|
||||
private _lastRecordedViewportHeight: number = 0;
|
||||
private _lastRecordedBufferHeight: number = 0;
|
||||
@@ -52,12 +52,12 @@ export class Viewport extends Disposable implements IViewport {
|
||||
private readonly _scrollLines: (amount: number) => void,
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
private readonly _scrollArea: HTMLElement,
|
||||
private readonly _element: HTMLElement,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IRenderService private readonly _renderService: IRenderService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
@@ -65,7 +65,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
// Unfortunately the overlay scrollbar would be hidden underneath the screen element in that case,
|
||||
// therefore we account for a standard amount to make it visible
|
||||
this.scrollBarWidth = (this._viewportElement.offsetWidth - this._scrollArea.offsetWidth) || FALLBACK_SCROLL_BAR_WIDTH;
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._onScroll.bind(this)));
|
||||
this.register(addDisposableDomListener(this._viewportElement, 'scroll', this._handleScroll.bind(this)));
|
||||
|
||||
// Track properties used in performance critical code manually to avoid using slow getters
|
||||
this._activeBuffer = this._bufferService.buffer;
|
||||
@@ -73,11 +73,15 @@ export class Viewport extends Disposable implements IViewport {
|
||||
this._renderDimensions = this._renderService.dimensions;
|
||||
this.register(this._renderService.onDimensionsChange(e => this._renderDimensions = e));
|
||||
|
||||
this._handleThemeChange(themeService.colors);
|
||||
this.register(themeService.onChangeColors(e => this._handleThemeChange(e)));
|
||||
this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
|
||||
|
||||
// Perform this async to ensure the ICharSizeService is ready.
|
||||
setTimeout(() => this.syncScrollArea(), 0);
|
||||
}
|
||||
|
||||
public onThemeChange(colors: IColorSet): void {
|
||||
private _handleThemeChange(colors: ReadonlyColorSet): void {
|
||||
this._viewportElement.style.backgroundColor = colors.background.css;
|
||||
}
|
||||
|
||||
@@ -100,10 +104,10 @@ export class Viewport extends Disposable implements IViewport {
|
||||
|
||||
private _innerRefresh(): void {
|
||||
if (this._charSizeService.height > 0) {
|
||||
this._currentRowHeight = this._renderService.dimensions.scaledCellHeight / this._coreBrowserService.dpr;
|
||||
this._currentScaledCellHeight = this._renderService.dimensions.scaledCellHeight;
|
||||
this._currentRowHeight = this._renderService.dimensions.device.cell.height / this._coreBrowserService.dpr;
|
||||
this._currentDeviceCellHeight = this._renderService.dimensions.device.cell.height;
|
||||
this._lastRecordedViewportHeight = this._viewportElement.offsetHeight;
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.canvasHeight);
|
||||
const newBufferHeight = Math.round(this._currentRowHeight * this._lastRecordedBufferLength) + (this._lastRecordedViewportHeight - this._renderService.dimensions.css.canvas.height);
|
||||
if (this._lastRecordedBufferHeight !== newBufferHeight) {
|
||||
this._lastRecordedBufferHeight = newBufferHeight;
|
||||
this._scrollArea.style.height = this._lastRecordedBufferHeight + 'px';
|
||||
@@ -134,7 +138,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
}
|
||||
|
||||
// If viewport height changed
|
||||
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.canvasHeight) {
|
||||
if (this._lastRecordedViewportHeight !== this._renderService.dimensions.css.canvas.height) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
@@ -146,7 +150,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
}
|
||||
|
||||
// If row height changed
|
||||
if (this._renderDimensions.scaledCellHeight !== this._currentScaledCellHeight) {
|
||||
if (this._renderDimensions.device.cell.height !== this._currentDeviceCellHeight) {
|
||||
this._refresh(immediate);
|
||||
return;
|
||||
}
|
||||
@@ -157,7 +161,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
* terminal to scroll to it.
|
||||
* @param ev The scroll event.
|
||||
*/
|
||||
private _onScroll(ev: Event): void {
|
||||
private _handleScroll(ev: Event): void {
|
||||
// Record current scroll top position
|
||||
this._lastScrollTop = this._viewportElement.scrollTop;
|
||||
|
||||
@@ -234,7 +238,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
* `Viewport`.
|
||||
* @param ev The mouse wheel event.
|
||||
*/
|
||||
public onWheel(ev: WheelEvent): boolean {
|
||||
public handleWheel(ev: WheelEvent): boolean {
|
||||
const amount = this._getPixelsScrolled(ev);
|
||||
if (amount === 0) {
|
||||
return false;
|
||||
@@ -315,7 +319,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
* Handles the touchstart event, recording the touch occurred.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchStart(ev: TouchEvent): void {
|
||||
public handleTouchStart(ev: TouchEvent): void {
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
}
|
||||
|
||||
@@ -323,7 +327,7 @@ export class Viewport extends Disposable implements IViewport {
|
||||
* Handles the touchmove event, scrolling the viewport if the position shifted.
|
||||
* @param ev The touch event.
|
||||
*/
|
||||
public onTouchMove(ev: TouchEvent): boolean {
|
||||
public handleTouchMove(ev: TouchEvent): boolean {
|
||||
const deltaY = this._lastTouchY - ev.touches[0].pageY;
|
||||
this._lastTouchY = ev.touches[0].pageY;
|
||||
if (deltaY === 0) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IRenderService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IInternalDecoration } from 'common/services/Services';
|
||||
|
||||
export class BufferDecorationRenderer extends Disposable {
|
||||
@@ -28,7 +28,7 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
this._container.classList.add('xterm-decoration-container');
|
||||
this._screenElement.appendChild(this._container);
|
||||
|
||||
this.register(this._renderService.onRenderedViewportChange(() => this._queueRefresh()));
|
||||
this.register(this._renderService.onRenderedViewportChange(() => this._doRefreshDecorations()));
|
||||
this.register(this._renderService.onDimensionsChange(() => {
|
||||
this._dimensionsChanged = true;
|
||||
this._queueRefresh();
|
||||
@@ -39,12 +39,10 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
}));
|
||||
this.register(this._decorationService.onDecorationRegistered(() => this._queueRefresh()));
|
||||
this.register(this._decorationService.onDecorationRemoved(decoration => this._removeDecoration(decoration)));
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this._container.remove();
|
||||
this._decorationElements.clear();
|
||||
super.dispose();
|
||||
this.register(toDisposable(() => {
|
||||
this._container.remove();
|
||||
this._decorationElements.clear();
|
||||
}));
|
||||
}
|
||||
|
||||
private _queueRefresh(): void {
|
||||
@@ -52,12 +50,12 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
return;
|
||||
}
|
||||
this._animationFrame = this._renderService.addRefreshCallback(() => {
|
||||
this.refreshDecorations();
|
||||
this._doRefreshDecorations();
|
||||
this._animationFrame = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
public refreshDecorations(): void {
|
||||
private _doRefreshDecorations(): void {
|
||||
for (const decoration of this._decorationService.decorations) {
|
||||
this._renderDecoration(decoration);
|
||||
}
|
||||
@@ -74,10 +72,10 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
private _createElement(decoration: IInternalDecoration): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.classList.add('xterm-decoration');
|
||||
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.actualCellWidth)}px`;
|
||||
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.lineHeight = `${this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.width = `${Math.round((decoration.options.width || 1) * this._renderService.dimensions.css.cell.width)}px`;
|
||||
element.style.height = `${(decoration.options.height || 1) * this._renderService.dimensions.css.cell.height}px`;
|
||||
element.style.top = `${(decoration.marker.line - this._bufferService.buffers.active.ydisp) * this._renderService.dimensions.css.cell.height}px`;
|
||||
element.style.lineHeight = `${this._renderService.dimensions.css.cell.height}px`;
|
||||
|
||||
const x = decoration.options.x ?? 0;
|
||||
if (x && x > this._bufferService.cols) {
|
||||
@@ -100,13 +98,12 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
} else {
|
||||
let element = this._decorationElements.get(decoration);
|
||||
if (!element) {
|
||||
decoration.onDispose(() => this._removeDecoration(decoration));
|
||||
element = this._createElement(decoration);
|
||||
decoration.element = element;
|
||||
this._decorationElements.set(decoration, element);
|
||||
this._container.appendChild(element);
|
||||
}
|
||||
element.style.top = `${line * this._renderService.dimensions.actualCellHeight}px`;
|
||||
element.style.top = `${line * this._renderService.dimensions.css.cell.height}px`;
|
||||
element.style.display = this._altBufferIsActive ? 'none' : 'block';
|
||||
decoration.onRenderEmitter.fire(element);
|
||||
}
|
||||
@@ -118,14 +115,15 @@ export class BufferDecorationRenderer extends Disposable {
|
||||
}
|
||||
const x = decoration.options.x ?? 0;
|
||||
if ((decoration.options.anchor || 'left') === 'right') {
|
||||
element.style.right = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
element.style.right = x ? `${x * this._renderService.dimensions.css.cell.width}px` : '';
|
||||
} else {
|
||||
element.style.left = x ? `${x * this._renderService.dimensions.actualCellWidth}px` : '';
|
||||
element.style.left = x ? `${x * this._renderService.dimensions.css.cell.width}px` : '';
|
||||
}
|
||||
}
|
||||
|
||||
private _removeDecoration(decoration: IInternalDecoration): void {
|
||||
this._decorationElements.get(decoration)?.remove();
|
||||
this._decorationElements.delete(decoration);
|
||||
decoration.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { ColorZoneStore, IColorZone, IColorZoneStore } from 'browser/decorations/ColorZoneStore';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
// Helper objects to avoid excessive calculation and garbage collection during rendering. These are
|
||||
@@ -68,6 +68,9 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
this._registerDecorationListeners();
|
||||
this._registerBufferChangeListeners();
|
||||
this._registerDimensionChangeListeners();
|
||||
this.register(toDisposable(() => {
|
||||
this._canvas?.remove();
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,24 +110,13 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
}));
|
||||
// overview ruler width changed
|
||||
this.register(this._optionsService.onOptionChange(o => {
|
||||
if (o === 'overviewRulerWidth') {
|
||||
this._queueRefresh(true);
|
||||
}
|
||||
}));
|
||||
this.register(this._optionsService.onSpecificOptionChange('overviewRulerWidth', () => this._queueRefresh(true)));
|
||||
// device pixel ratio changed
|
||||
this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => {
|
||||
this._queueRefresh(true);
|
||||
}));
|
||||
this.register(addDisposableDomListener(this._coreBrowseService.window, 'resize', () => this._queueRefresh(true)));
|
||||
// set the canvas dimensions
|
||||
this._queueRefresh(true);
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
this._canvas?.remove();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
private _refreshDrawConstants(): void {
|
||||
// width
|
||||
const outerWidth = Math.floor(this._canvas.width / 3);
|
||||
@@ -198,8 +190,6 @@ export class OverviewRulerRenderer extends Disposable {
|
||||
}
|
||||
|
||||
private _renderColorZone(zone: IColorZone): void {
|
||||
// TODO: Is _decorationElements needed?
|
||||
|
||||
this._ctx.fillStyle = zone.color;
|
||||
this._ctx.fillRect(
|
||||
/* x */ drawX[zone.position || 'full'],
|
||||
|
||||
@@ -89,7 +89,7 @@ export class CompositionHelper {
|
||||
/**
|
||||
* Handles the keydown event, routing any necessary events to the CompositionHelper functions.
|
||||
* @param ev The keydown event.
|
||||
* @return Whether the Terminal should continue processing the keydown event.
|
||||
* @returns Whether the Terminal should continue processing the keydown event.
|
||||
*/
|
||||
public keydown(ev: KeyboardEvent): boolean {
|
||||
if (this._isComposing || this._isSendingComposition) {
|
||||
@@ -218,9 +218,9 @@ export class CompositionHelper {
|
||||
if (this._bufferService.buffer.isCursorInViewport) {
|
||||
const cursorX = Math.min(this._bufferService.buffer.x, this._bufferService.cols - 1);
|
||||
|
||||
const cellHeight = this._renderService.dimensions.actualCellHeight;
|
||||
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.actualCellHeight;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.actualCellWidth;
|
||||
const cellHeight = this._renderService.dimensions.css.cell.height;
|
||||
const cursorTop = this._bufferService.buffer.y * this._renderService.dimensions.css.cell.height;
|
||||
const cursorLeft = cursorX * this._renderService.dimensions.css.cell.width;
|
||||
|
||||
this._compositionView.style.left = cursorLeft + 'px';
|
||||
this._compositionView.style.top = cursorTop + 'px';
|
||||
|
||||
@@ -18,15 +18,19 @@ export function getCoordsRelativeToElement(window: Pick<Window, 'getComputedStyl
|
||||
* Gets coordinates within the terminal for a particular mouse event. The result
|
||||
* is returned as an array in the form [x, y] instead of an object as it's a
|
||||
* little faster and this function is used in some low level code.
|
||||
* @param window The window object the element belongs to.
|
||||
* @param event The mouse event.
|
||||
* @param element The terminal's container element.
|
||||
* @param colCount The number of columns in the terminal.
|
||||
* @param rowCount The number of rows n the terminal.
|
||||
* @param hasValidCharSize Whether there is a valid character size available.
|
||||
* @param cssCellWidth The cell width device pixel render dimensions.
|
||||
* @param cssCellHeight The cell height device pixel render dimensions.
|
||||
* @param isSelection Whether the request is for the selection or not. This will
|
||||
* apply an offset to the x value such that the left half of the cell will
|
||||
* select that cell and the right half will select the next cell.
|
||||
*/
|
||||
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clientX: number, clientY: number}, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, actualCellWidth: number, actualCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: Pick<MouseEvent, 'clientX' | 'clientY'>, element: HTMLElement, colCount: number, rowCount: number, hasValidCharSize: boolean, cssCellWidth: number, cssCellHeight: number, isSelection?: boolean): [number, number] | undefined {
|
||||
// Coordinates cannot be measured if there are no valid
|
||||
if (!hasValidCharSize) {
|
||||
return undefined;
|
||||
@@ -37,8 +41,8 @@ export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: {clie
|
||||
return undefined;
|
||||
}
|
||||
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? actualCellWidth / 2 : 0)) / actualCellWidth);
|
||||
coords[1] = Math.ceil(coords[1] / actualCellHeight);
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? cssCellWidth / 2 : 0)) / cssCellWidth);
|
||||
coords[1] = Math.ceil(coords[1] / cssCellHeight);
|
||||
|
||||
// Ensure coordinates are within the terminal viewport. Note that selections
|
||||
// need an addition point of precision to cover the end point (as characters
|
||||
|
||||
@@ -68,7 +68,7 @@ function resetStartingRow(startX: number, startY: number, targetX: number, targe
|
||||
}
|
||||
return repeat(bufferLine(
|
||||
startX, startY, startX,
|
||||
startY - wrappedRowsForRow(bufferService, startY), false, bufferService
|
||||
startY - wrappedRowsForRow(startY, bufferService), false, bufferService
|
||||
).length, sequence(Direction.LEFT, applicationCursor));
|
||||
}
|
||||
|
||||
@@ -77,8 +77,8 @@ function resetStartingRow(startX: number, startY: number, targetX: number, targe
|
||||
* ignoring wrapped rows
|
||||
*/
|
||||
function moveToRequestedRow(startY: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
const startRow = startY - wrappedRowsForRow(bufferService, startY);
|
||||
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
const startRow = startY - wrappedRowsForRow(startY, bufferService);
|
||||
const endRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
||||
|
||||
const rowsToMove = Math.abs(startRow - endRow) - wrappedRowsCount(startY, targetY, bufferService);
|
||||
|
||||
@@ -91,7 +91,7 @@ function moveToRequestedRow(startY: number, targetY: number, bufferService: IBuf
|
||||
function moveToRequestedCol(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): string {
|
||||
let startRow;
|
||||
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
|
||||
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
||||
} else {
|
||||
startRow = startY;
|
||||
}
|
||||
@@ -115,8 +115,8 @@ function moveToRequestedCol(startX: number, startY: number, targetX: number, tar
|
||||
*/
|
||||
function wrappedRowsCount(startY: number, targetY: number, bufferService: IBufferService): number {
|
||||
let wrappedRows = 0;
|
||||
const startRow = startY - wrappedRowsForRow(bufferService, startY);
|
||||
const endRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
const startRow = startY - wrappedRowsForRow(startY, bufferService);
|
||||
const endRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
||||
|
||||
for (let i = 0; i < Math.abs(startRow - endRow); i++) {
|
||||
const direction = verticalDirection(startY, targetY) === Direction.UP ? -1 : 1;
|
||||
@@ -133,7 +133,7 @@ function wrappedRowsCount(startY: number, targetY: number, bufferService: IBuffe
|
||||
* Calculates the number of wrapped rows that make up a given row.
|
||||
* @param currentRow The row to determine how many wrapped rows make it up
|
||||
*/
|
||||
function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): number {
|
||||
function wrappedRowsForRow(currentRow: number, bufferService: IBufferService): number {
|
||||
let rowCount = 0;
|
||||
let line = bufferService.buffer.lines.get(currentRow);
|
||||
let lineWraps = line?.isWrapped;
|
||||
@@ -157,7 +157,7 @@ function wrappedRowsForRow(bufferService: IBufferService, currentRow: number): n
|
||||
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
|
||||
let startRow;
|
||||
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
|
||||
startRow = targetY - wrappedRowsForRow(bufferService, targetY);
|
||||
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
||||
} else {
|
||||
startRow = startY;
|
||||
}
|
||||
@@ -237,7 +237,7 @@ function sequence(direction: Direction, applicationCursor: boolean): string {
|
||||
* Returns a string repeated a given number of times
|
||||
* Polyfill from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat
|
||||
* @param count The number of times to repeat the string
|
||||
* @param string The string that is to be repeated
|
||||
* @param str The string that is to be repeated
|
||||
*/
|
||||
function repeat(count: number, str: string): string {
|
||||
count = Math.floor(count);
|
||||
|
||||
@@ -78,7 +78,6 @@ export class Terminal implements ITerminalApi {
|
||||
|
||||
public get element(): HTMLElement | undefined { return this._core.element; }
|
||||
public get parser(): IParser {
|
||||
this._checkProposedApi();
|
||||
if (!this._parser) {
|
||||
this._parser = new ParserApi(this._core);
|
||||
}
|
||||
@@ -92,7 +91,6 @@ export class Terminal implements ITerminalApi {
|
||||
public get rows(): number { return this._core.rows; }
|
||||
public get cols(): number { return this._core.cols; }
|
||||
public get buffer(): IBufferNamespaceApi {
|
||||
this._checkProposedApi();
|
||||
if (!this._buffer) {
|
||||
this._buffer = new BufferNamespaceApi(this._core);
|
||||
}
|
||||
@@ -148,7 +146,6 @@ export class Terminal implements ITerminalApi {
|
||||
this._core.attachCustomKeyEventHandler(customKeyEventHandler);
|
||||
}
|
||||
public registerLinkProvider(linkProvider: ILinkProvider): IDisposable {
|
||||
this._checkProposedApi();
|
||||
return this._core.registerLinkProvider(linkProvider);
|
||||
}
|
||||
public registerCharacterJoiner(handler: (text: string) => [number, number][]): number {
|
||||
|
||||
Vendored
-56
@@ -1,56 +0,0 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
export interface IRenderDimensions {
|
||||
scaledCharWidth: number;
|
||||
scaledCharHeight: number;
|
||||
scaledCellWidth: number;
|
||||
scaledCellHeight: number;
|
||||
scaledCharLeft: number;
|
||||
scaledCharTop: number;
|
||||
scaledCanvasWidth: number;
|
||||
scaledCanvasHeight: number;
|
||||
canvasWidth: number;
|
||||
canvasHeight: number;
|
||||
actualCellWidth: number;
|
||||
actualCellHeight: number;
|
||||
}
|
||||
|
||||
export interface IRequestRedrawEvent {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
readonly dimensions: IRenderDimensions;
|
||||
|
||||
/**
|
||||
* Fires when the renderer is requesting to be redrawn on the next animation
|
||||
* frame but is _not_ a result of content changing (eg. selection changes).
|
||||
*/
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
onOptionsChanged(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
clearTextureAtlas?(): void;
|
||||
}
|
||||
@@ -3,16 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types';
|
||||
import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types';
|
||||
import { ICharSizeService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IOptionsService, IBufferService, IInstantiationService } from 'common/services/Services';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { color } from 'common/Color';
|
||||
import { removeElementFromParent } from 'browser/Dom';
|
||||
import { BOLD_CLASS, CURSOR_BLINK_CLASS, CURSOR_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory, ITALIC_CLASS } from 'browser/renderer/dom/DomRendererRowFactory';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types';
|
||||
import { ICharSizeService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { ILinkifier2, ILinkifierEvent, ReadonlyColorSet } from 'browser/Types';
|
||||
import { color } from 'common/Color';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { IBufferService, IInstantiationService, IOptionsService } from 'common/services/Services';
|
||||
|
||||
const TERMINAL_CLASS_PREFIX = 'xterm-dom-renderer-owner-';
|
||||
const ROW_CONTAINER_CLASS = 'xterm-rows';
|
||||
@@ -37,13 +38,13 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
private _rowContainer: HTMLElement;
|
||||
private _rowElements: HTMLElement[] = [];
|
||||
private _selectionContainer: HTMLElement;
|
||||
private _cellToRowElements: Int16Array[] = [];
|
||||
|
||||
public dimensions: IRenderDimensions;
|
||||
|
||||
public get onRequestRedraw(): IEvent<IRequestRedrawEvent> { return new EventEmitter<IRequestRedrawEvent>().event; }
|
||||
public readonly onRequestRedraw = this.register(new EventEmitter<IRequestRedrawEvent>()).event;
|
||||
|
||||
constructor(
|
||||
private _colors: IColorSet,
|
||||
private readonly _element: HTMLElement,
|
||||
private readonly _screenElement: HTMLElement,
|
||||
private readonly _viewportElement: HTMLElement,
|
||||
@@ -52,7 +53,8 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@IBufferService private readonly _bufferService: IBufferService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
this._rowContainer = document.createElement('div');
|
||||
@@ -64,62 +66,50 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
this._selectionContainer.classList.add(SELECTION_CLASS);
|
||||
this._selectionContainer.setAttribute('aria-hidden', 'true');
|
||||
|
||||
this.dimensions = {
|
||||
scaledCharWidth: 0,
|
||||
scaledCharHeight: 0,
|
||||
scaledCellWidth: 0,
|
||||
scaledCellHeight: 0,
|
||||
scaledCharLeft: 0,
|
||||
scaledCharTop: 0,
|
||||
scaledCanvasWidth: 0,
|
||||
scaledCanvasHeight: 0,
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
actualCellWidth: 0,
|
||||
actualCellHeight: 0
|
||||
};
|
||||
this.dimensions = createRenderDimensions();
|
||||
this._updateDimensions();
|
||||
this._injectCss();
|
||||
this.register(this._optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
|
||||
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document, this._colors);
|
||||
this.register(themeService.onChangeColors(e => this._injectCss(e)));
|
||||
this._injectCss(themeService.colors);
|
||||
|
||||
this._rowFactory = instantiationService.createInstance(DomRendererRowFactory, document);
|
||||
|
||||
this._element.classList.add(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this._screenElement.appendChild(this._rowContainer);
|
||||
this._screenElement.appendChild(this._selectionContainer);
|
||||
|
||||
this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e)));
|
||||
this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e)));
|
||||
}
|
||||
this.register(this._linkifier2.onShowLinkUnderline(e => this._handleLinkHover(e)));
|
||||
this.register(this._linkifier2.onHideLinkUnderline(e => this._handleLinkLeave(e)));
|
||||
|
||||
public dispose(): void {
|
||||
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
this.register(toDisposable(() => {
|
||||
this._element.classList.remove(TERMINAL_CLASS_PREFIX + this._terminalClass);
|
||||
|
||||
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
|
||||
// https://github.com/xtermjs/xterm.js/issues/2960
|
||||
removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
|
||||
|
||||
super.dispose();
|
||||
// Outside influences such as React unmounts may manipulate the DOM before our disposal.
|
||||
// https://github.com/xtermjs/xterm.js/issues/2960
|
||||
removeElementFromParent(this._rowContainer, this._selectionContainer, this._themeStyleElement, this._dimensionsStyleElement);
|
||||
}));
|
||||
}
|
||||
|
||||
private _updateDimensions(): void {
|
||||
const dpr = this._coreBrowserService.dpr;
|
||||
this.dimensions.scaledCharWidth = this._charSizeService.width * dpr;
|
||||
this.dimensions.scaledCharHeight = Math.ceil(this._charSizeService.height * dpr);
|
||||
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._optionsService.rawOptions.lineHeight);
|
||||
this.dimensions.scaledCharLeft = 0;
|
||||
this.dimensions.scaledCharTop = 0;
|
||||
this.dimensions.scaledCanvasWidth = this.dimensions.scaledCellWidth * this._bufferService.cols;
|
||||
this.dimensions.scaledCanvasHeight = this.dimensions.scaledCellHeight * this._bufferService.rows;
|
||||
this.dimensions.canvasWidth = Math.round(this.dimensions.scaledCanvasWidth / dpr);
|
||||
this.dimensions.canvasHeight = Math.round(this.dimensions.scaledCanvasHeight / dpr);
|
||||
this.dimensions.actualCellWidth = this.dimensions.canvasWidth / this._bufferService.cols;
|
||||
this.dimensions.actualCellHeight = this.dimensions.canvasHeight / this._bufferService.rows;
|
||||
this.dimensions.device.char.width = this._charSizeService.width * dpr;
|
||||
this.dimensions.device.char.height = Math.ceil(this._charSizeService.height * dpr);
|
||||
this.dimensions.device.cell.width = this.dimensions.device.char.width + Math.round(this._optionsService.rawOptions.letterSpacing);
|
||||
this.dimensions.device.cell.height = Math.floor(this.dimensions.device.char.height * this._optionsService.rawOptions.lineHeight);
|
||||
this.dimensions.device.char.left = 0;
|
||||
this.dimensions.device.char.top = 0;
|
||||
this.dimensions.device.canvas.width = this.dimensions.device.cell.width * this._bufferService.cols;
|
||||
this.dimensions.device.canvas.height = this.dimensions.device.cell.height * this._bufferService.rows;
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / dpr);
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / dpr);
|
||||
this.dimensions.css.cell.width = this.dimensions.css.canvas.width / this._bufferService.cols;
|
||||
this.dimensions.css.cell.height = this.dimensions.css.canvas.height / this._bufferService.rows;
|
||||
|
||||
for (const element of this._rowElements) {
|
||||
element.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
element.style.height = `${this.dimensions.actualCellHeight}px`;
|
||||
element.style.lineHeight = `${this.dimensions.actualCellHeight}px`;
|
||||
element.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
element.style.height = `${this.dimensions.css.cell.height}px`;
|
||||
element.style.lineHeight = `${this.dimensions.css.cell.height}px`;
|
||||
// Make sure rows don't overflow onto following row
|
||||
element.style.overflow = 'hidden';
|
||||
}
|
||||
@@ -134,22 +124,17 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` display: inline-block;` +
|
||||
` height: 100%;` +
|
||||
` vertical-align: top;` +
|
||||
` width: ${this.dimensions.actualCellWidth}px` +
|
||||
` width: ${this.dimensions.css.cell.width}px` +
|
||||
`}`;
|
||||
|
||||
this._dimensionsStyleElement.textContent = styles;
|
||||
|
||||
this._selectionContainer.style.height = this._viewportElement.style.height;
|
||||
this._screenElement.style.width = `${this.dimensions.canvasWidth}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.canvasHeight}px`;
|
||||
this._screenElement.style.width = `${this.dimensions.css.canvas.width}px`;
|
||||
this._screenElement.style.height = `${this.dimensions.css.canvas.height}px`;
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
this._injectCss();
|
||||
}
|
||||
|
||||
private _injectCss(): void {
|
||||
private _injectCss(colors: ReadonlyColorSet): void {
|
||||
if (!this._themeStyleElement) {
|
||||
this._themeStyleElement = document.createElement('style');
|
||||
this._screenElement.appendChild(this._themeStyleElement);
|
||||
@@ -158,7 +143,7 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
// Base CSS
|
||||
let styles =
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
|
||||
` color: ${this._colors.foreground.css};` +
|
||||
` color: ${colors.foreground.css};` +
|
||||
` font-family: ${this._optionsService.rawOptions.fontFamily};` +
|
||||
` font-size: ${this._optionsService.rawOptions.fontSize}px;` +
|
||||
`}`;
|
||||
@@ -183,18 +168,18 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
styles +=
|
||||
`@keyframes blink_block` + `_` + this._terminalClass + ` {` +
|
||||
` 0% {` +
|
||||
` background-color: ${this._colors.cursor.css};` +
|
||||
` color: ${this._colors.cursorAccent.css};` +
|
||||
` background-color: ${colors.cursor.css};` +
|
||||
` color: ${colors.cursorAccent.css};` +
|
||||
` }` +
|
||||
` 50% {` +
|
||||
` background-color: ${this._colors.cursorAccent.css};` +
|
||||
` color: ${this._colors.cursor.css};` +
|
||||
` background-color: ${colors.cursorAccent.css};` +
|
||||
` color: ${colors.cursor.css};` +
|
||||
` }` +
|
||||
`}`;
|
||||
// Cursor
|
||||
styles +=
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}:not(.${FOCUS_CLASS}) .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` outline: 1px solid ${this._colors.cursor.css};` +
|
||||
` outline: 1px solid ${colors.cursor.css};` +
|
||||
` outline-offset: -1px;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_BLINK_CLASS}:not(.${CURSOR_STYLE_BLOCK_CLASS}) {` +
|
||||
@@ -204,14 +189,14 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BLOCK_CLASS} {` +
|
||||
` background-color: ${this._colors.cursor.css};` +
|
||||
` color: ${this._colors.cursorAccent.css};` +
|
||||
` background-color: ${colors.cursor.css};` +
|
||||
` color: ${colors.cursorAccent.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_BAR_CLASS} {` +
|
||||
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${this._colors.cursor.css} inset;` +
|
||||
` box-shadow: ${this._optionsService.rawOptions.cursorWidth}px 0 0 ${colors.cursor.css} inset;` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${CURSOR_CLASS}.${CURSOR_STYLE_UNDERLINE_CLASS} {` +
|
||||
` box-shadow: 0 -1px 0 ${this._colors.cursor.css} inset;` +
|
||||
` box-shadow: 0 -1px 0 ${colors.cursor.css} inset;` +
|
||||
`}`;
|
||||
// Selection
|
||||
styles +=
|
||||
@@ -224,26 +209,26 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
`}` +
|
||||
`${this._terminalSelector}.focus .${SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${this._colors.selectionBackgroundOpaque.css};` +
|
||||
` background-color: ${colors.selectionBackgroundOpaque.css};` +
|
||||
`}` +
|
||||
`${this._terminalSelector} .${SELECTION_CLASS} div {` +
|
||||
` position: absolute;` +
|
||||
` background-color: ${this._colors.selectionInactiveBackgroundOpaque.css};` +
|
||||
` background-color: ${colors.selectionInactiveBackgroundOpaque.css};` +
|
||||
`}`;
|
||||
// Colors
|
||||
this._colors.ansi.forEach((c, i) => {
|
||||
for (const [i, c] of colors.ansi.entries()) {
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${i} { color: ${c.css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${i} { background-color: ${c.css}; }`;
|
||||
});
|
||||
}
|
||||
styles +=
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(this._colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${this._colors.foreground.css}; }`;
|
||||
`${this._terminalSelector} .${FG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { color: ${color.opaque(colors.background).css}; }` +
|
||||
`${this._terminalSelector} .${BG_CLASS_PREFIX}${INVERTED_DEFAULT_COLOR} { background-color: ${colors.foreground.css}; }`;
|
||||
|
||||
this._themeStyleElement.textContent = styles;
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
@@ -260,30 +245,30 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
}
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
this._refreshRowElements(cols, rows);
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
public onCharSizeChanged(): void {
|
||||
public handleCharSizeChanged(): void {
|
||||
this._updateDimensions();
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
public handleBlur(): void {
|
||||
this._rowContainer.classList.remove(FOCUS_CLASS);
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
public handleFocus(): void {
|
||||
this._rowContainer.classList.add(FOCUS_CLASS);
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
// Remove all selections
|
||||
while (this._selectionContainer.children.length) {
|
||||
this._selectionContainer.removeChild(this._selectionContainer.children[0]);
|
||||
}
|
||||
|
||||
this._rowFactory.onSelectionChanged(start, end, columnSelectMode);
|
||||
this._rowFactory.handleSelectionChanged(start, end, columnSelectMode);
|
||||
this.renderRows(0, this._bufferService.rows - 1);
|
||||
|
||||
// Selection does not exist
|
||||
@@ -336,26 +321,32 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
*/
|
||||
private _createSelectionElement(row: number, colStart: number, colEnd: number, rowCount: number = 1): HTMLElement {
|
||||
const element = document.createElement('div');
|
||||
element.style.height = `${rowCount * this.dimensions.actualCellHeight}px`;
|
||||
element.style.top = `${row * this.dimensions.actualCellHeight}px`;
|
||||
element.style.left = `${colStart * this.dimensions.actualCellWidth}px`;
|
||||
element.style.width = `${this.dimensions.actualCellWidth * (colEnd - colStart)}px`;
|
||||
element.style.height = `${rowCount * this.dimensions.css.cell.height}px`;
|
||||
element.style.top = `${row * this.dimensions.css.cell.height}px`;
|
||||
element.style.left = `${colStart * this.dimensions.css.cell.width}px`;
|
||||
element.style.width = `${this.dimensions.css.cell.width * (colEnd - colStart)}px`;
|
||||
return element;
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
public handleCursorMove(): void {
|
||||
// No-op, the cursor is drawn when rows are drawn
|
||||
}
|
||||
|
||||
public onOptionsChanged(): void {
|
||||
private _handleOptionsChanged(): void {
|
||||
// Force a refresh
|
||||
this._updateDimensions();
|
||||
this._injectCss();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
for (const e of this._rowElements) {
|
||||
e.innerText = '';
|
||||
/**
|
||||
* NOTE: This used to be `e.innerText = '';` but that doesn't work when using `jsdom` and `@testing-library/react`
|
||||
*
|
||||
* references:
|
||||
* - https://github.com/testing-library/react-testing-library/issues/1146
|
||||
* - https://github.com/jsdom/jsdom/issues/1245
|
||||
*/
|
||||
e.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,11 +357,13 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
|
||||
for (let y = start; y <= end; y++) {
|
||||
const rowElement = this._rowElements[y];
|
||||
rowElement.innerText = '';
|
||||
const row = y + this._bufferService.buffer.ydisp;
|
||||
const lineData = this._bufferService.buffer.lines.get(row);
|
||||
const cursorStyle = this._optionsService.rawOptions.cursorStyle;
|
||||
rowElement.appendChild(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.actualCellWidth, this._bufferService.cols));
|
||||
if (!this._cellToRowElements[y] || this._cellToRowElements[y].length !== this._bufferService.cols) {
|
||||
this._cellToRowElements[y] = new Int16Array(this._bufferService.cols);
|
||||
}
|
||||
rowElement.replaceChildren(this._rowFactory.createRow(lineData!, row, row === cursorAbsoluteY, cursorStyle, cursorX, cursorBlink, this.dimensions.css.cell.width, this._bufferService.cols, this._cellToRowElements[y]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,15 +371,22 @@ export class DomRenderer extends Disposable implements IRenderer {
|
||||
return `.${TERMINAL_CLASS_PREFIX}${this._terminalClass}`;
|
||||
}
|
||||
|
||||
private _onLinkHover(e: ILinkifierEvent): void {
|
||||
private _handleLinkHover(e: ILinkifierEvent): void {
|
||||
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, true);
|
||||
}
|
||||
|
||||
private _onLinkLeave(e: ILinkifierEvent): void {
|
||||
private _handleLinkLeave(e: ILinkifierEvent): void {
|
||||
this._setCellUnderline(e.x1, e.x2, e.y1, e.y2, e.cols, false);
|
||||
}
|
||||
|
||||
private _setCellUnderline(x: number, x2: number, y: number, y2: number, cols: number, enabled: boolean): void {
|
||||
x = this._cellToRowElements[y][x];
|
||||
x2 = this._cellToRowElements[y2][x2];
|
||||
|
||||
if (x === -1 || x2 === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (x !== x2 || y !== y2) {
|
||||
const row = this._rowElements[y];
|
||||
if (!row) {
|
||||
|
||||
@@ -12,7 +12,9 @@ import { IBufferLine } from 'common/Types';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { MockCoreService, MockDecorationService, MockOptionsService } from 'common/TestUtils.test';
|
||||
import { css } from 'common/Color';
|
||||
import { MockCharacterJoinerService, MockCoreBrowserService } from 'browser/TestUtils.test';
|
||||
import { MockCharacterJoinerService, MockCoreBrowserService, MockThemeService } from 'browser/TestUtils.test';
|
||||
|
||||
const EMPTY_ELEM_MAPPING = new Int16Array(1000);
|
||||
|
||||
describe('DomRendererRowFactory', () => {
|
||||
let dom: jsdom.JSDOM;
|
||||
@@ -23,42 +25,19 @@ describe('DomRendererRowFactory', () => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
rowFactory = new DomRendererRowFactory(
|
||||
dom.window.document,
|
||||
{
|
||||
background: css.toColor('#010101'),
|
||||
foreground: css.toColor('#020202'),
|
||||
ansi: [
|
||||
// dark:
|
||||
css.toColor('#2e3436'),
|
||||
css.toColor('#cc0000'),
|
||||
css.toColor('#4e9a06'),
|
||||
css.toColor('#c4a000'),
|
||||
css.toColor('#3465a4'),
|
||||
css.toColor('#75507b'),
|
||||
css.toColor('#06989a'),
|
||||
css.toColor('#d3d7cf'),
|
||||
// bright:
|
||||
css.toColor('#555753'),
|
||||
css.toColor('#ef2929'),
|
||||
css.toColor('#8ae234'),
|
||||
css.toColor('#fce94f'),
|
||||
css.toColor('#729fcf'),
|
||||
css.toColor('#ad7fa8'),
|
||||
css.toColor('#34e2e2'),
|
||||
css.toColor('#eeeeec')
|
||||
]
|
||||
} as any,
|
||||
new MockCharacterJoinerService(),
|
||||
new MockOptionsService({ drawBoldTextInBrightColors: true }),
|
||||
new MockCoreBrowserService(),
|
||||
new MockCoreService(),
|
||||
new MockDecorationService()
|
||||
new MockDecorationService(),
|
||||
new MockThemeService()
|
||||
);
|
||||
lineData = createEmptyLineData(2);
|
||||
});
|
||||
|
||||
describe('createRow', () => {
|
||||
it('should not create anything for an empty row', () => {
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
''
|
||||
);
|
||||
@@ -68,7 +47,7 @@ describe('DomRendererRowFactory', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, '語', 2, '語'.charCodeAt(0)]));
|
||||
// There should be no element for the following "empty" cell
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, '', 0, 0]));
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="width: 10px;">語</span>'
|
||||
);
|
||||
@@ -76,7 +55,7 @@ describe('DomRendererRowFactory', () => {
|
||||
|
||||
it('should add class for cursor and cursor style', () => {
|
||||
for (const style of ['block', 'bar', 'underline']) {
|
||||
const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, true, style, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-cursor xterm-cursor-${style}"> </span>`
|
||||
);
|
||||
@@ -84,7 +63,7 @@ describe('DomRendererRowFactory', () => {
|
||||
});
|
||||
|
||||
it('should add class for cursor blink', () => {
|
||||
const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, true, 'block', 0, true, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-cursor xterm-cursor-blink xterm-cursor-block"> </span>`
|
||||
);
|
||||
@@ -93,7 +72,7 @@ describe('DomRendererRowFactory', () => {
|
||||
it('should not render cells that go beyond the terminal\'s columns', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 1, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span>'
|
||||
);
|
||||
@@ -104,7 +83,7 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.BOLD;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bold">a</span>'
|
||||
);
|
||||
@@ -114,7 +93,7 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.ITALIC;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-italic">a</span>'
|
||||
);
|
||||
@@ -124,7 +103,7 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.DIM;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-dim">a</span>'
|
||||
);
|
||||
@@ -137,7 +116,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.SINGLE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-1">a</span>'
|
||||
);
|
||||
@@ -148,7 +127,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOUBLE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-2">a</span>'
|
||||
);
|
||||
@@ -159,7 +138,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.CURLY;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-3">a</span>'
|
||||
);
|
||||
@@ -170,7 +149,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DOTTED;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-4">a</span>'
|
||||
);
|
||||
@@ -181,7 +160,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg = DEFAULT_ATTR_DATA.bg | BgFlags.HAS_EXTENDED;
|
||||
cell.extended.underlineStyle = UnderlineStyle.DASHED;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-underline-5">a</span>'
|
||||
);
|
||||
@@ -192,7 +171,7 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg = DEFAULT_ATTR_DATA.fg | FgFlags.STRIKETHROUGH;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-strikethrough">a</span>'
|
||||
);
|
||||
@@ -205,7 +184,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.fg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-fg-${i}">a</span>`
|
||||
);
|
||||
@@ -219,7 +198,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.bg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.bg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-bg-${i}">a</span>`
|
||||
);
|
||||
@@ -231,7 +210,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= Attributes.CM_P16 | 2 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bg-2 xterm-fg-1">a</span>'
|
||||
);
|
||||
@@ -242,7 +221,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_P16 | 1;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bg-257 xterm-fg-1">a</span>'
|
||||
);
|
||||
@@ -252,7 +231,7 @@ describe('DomRendererRowFactory', () => {
|
||||
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
|
||||
cell.fg |= Attributes.CM_P16 | 1 | FgFlags.INVERSE;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-bg-1 xterm-fg-257">a</span>'
|
||||
);
|
||||
@@ -265,7 +244,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg &= ~Attributes.PCOLOR_MASK;
|
||||
cell.fg |= i;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
`<span class="xterm-bold xterm-fg-${i + 8}">a</span>`
|
||||
);
|
||||
@@ -277,7 +256,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="background-color:#040506;color:#010203;">a</span>'
|
||||
);
|
||||
@@ -288,7 +267,7 @@ describe('DomRendererRowFactory', () => {
|
||||
cell.fg |= Attributes.CM_RGB | 1 << 16 | 2 << 8 | 3 | FgFlags.INVERSE;
|
||||
cell.bg |= Attributes.CM_RGB | 4 << 16 | 5 << 8 | 6;
|
||||
lineData.setCell(0, cell);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span style="background-color:#010203;color:#040506;">a</span>'
|
||||
);
|
||||
@@ -299,16 +278,16 @@ describe('DomRendererRowFactory', () => {
|
||||
it('should force selected cells with content to be rendered above the background', () => {
|
||||
lineData.setCell(0, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'b', 1, 'b'.charCodeAt(0)]));
|
||||
rowFactory.onSelectionChanged([1, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
rowFactory.handleSelectionChanged([1, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span>a</span><span class="xterm-decoration-top">b</span>'
|
||||
);
|
||||
});
|
||||
it('should force whitespace cells to be rendered above the background', () => {
|
||||
lineData.setCell(1, CellData.fromCharData([DEFAULT_ATTR, 'a', 1, 'a'.charCodeAt(0)]));
|
||||
rowFactory.onSelectionChanged([0, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20);
|
||||
rowFactory.handleSelectionChanged([0, 0], [2, 0], false);
|
||||
const fragment = rowFactory.createRow(lineData, 0, false, undefined, 0, false, 5, 20, EMPTY_ELEM_MAPPING);
|
||||
assert.equal(getFragmentHtml(fragment),
|
||||
'<span class="xterm-decoration-top"> </span><span class="xterm-decoration-top">a</span>'
|
||||
);
|
||||
|
||||
@@ -4,15 +4,15 @@
|
||||
*/
|
||||
|
||||
import { IBufferLine, ICellData, IColor } from 'common/Types';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants';
|
||||
import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants';
|
||||
import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants';
|
||||
import { CellData } from 'common/buffer/CellData';
|
||||
import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services';
|
||||
import { color, rgba } from 'common/Color';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICoreBrowserService } from 'browser/services/Services';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { JoinedCellData } from 'browser/services/CharacterJoinerService';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils';
|
||||
import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
|
||||
export const BOLD_CLASS = 'xterm-bold';
|
||||
@@ -35,26 +35,25 @@ export class DomRendererRowFactory {
|
||||
|
||||
constructor(
|
||||
private readonly _document: Document,
|
||||
private _colors: IColorSet,
|
||||
@ICharacterJoinerService private readonly _characterJoinerService: ICharacterJoinerService,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService,
|
||||
@ICoreBrowserService private readonly _coreBrowserService: ICoreBrowserService,
|
||||
@ICoreService private readonly _coreService: ICoreService,
|
||||
@IDecorationService private readonly _decorationService: IDecorationService
|
||||
) {
|
||||
}
|
||||
@IDecorationService private readonly _decorationService: IDecorationService,
|
||||
@IThemeService private readonly _themeService: IThemeService
|
||||
) {}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._colors = colors;
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
this._selectionStart = start;
|
||||
this._selectionEnd = end;
|
||||
this._columnSelectMode = columnSelectMode;
|
||||
}
|
||||
|
||||
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number): DocumentFragment {
|
||||
public createRow(lineData: IBufferLine, row: number, isCursorRow: boolean, cursorStyle: string | undefined, cursorX: number, cursorBlink: boolean, cellWidth: number, cols: number, cellMap: Int16Array): DocumentFragment {
|
||||
// NOTE: `cellMap` maps cell positions to a span element index in a row.
|
||||
// All positions should be updated, even skipped ones after wide chars or left overs at the end,
|
||||
// otherwise the mouse hover logic might mark the wrong elements as underlined.
|
||||
|
||||
const fragment = this._document.createDocumentFragment();
|
||||
|
||||
const joinedRanges = this._characterJoinerService.getJoinedCharacters(row);
|
||||
@@ -71,12 +70,18 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
}
|
||||
|
||||
for (let x = 0; x < lineLength; x++) {
|
||||
const colors = this._themeService.colors;
|
||||
let elemIndex = -1;
|
||||
|
||||
let x = 0;
|
||||
for (; x < lineLength; x++) {
|
||||
lineData.loadCell(x, this._workCell);
|
||||
let width = this._workCell.getWidth();
|
||||
|
||||
// The character to the left is a wide character, drawing is owned by the char at x-1
|
||||
// still have to update cellMap with current element index
|
||||
if (width === 0) {
|
||||
cellMap[x] = elemIndex;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -166,7 +171,7 @@ export class DomRendererRowFactory {
|
||||
if (cell.isUnderline()) {
|
||||
charElement.classList.add(`${UNDERLINE_CLASS}-${cell.extended.underlineStyle}`);
|
||||
if (charElement.textContent === ' ') {
|
||||
charElement.innerHTML = ' ';
|
||||
charElement.textContent = '\xa0'; // =
|
||||
}
|
||||
if (!cell.isUnderlineColorDefault()) {
|
||||
if (cell.isUnderlineColorRGB()) {
|
||||
@@ -176,7 +181,7 @@ export class DomRendererRowFactory {
|
||||
if (this._optionsService.rawOptions.drawBoldTextInBrightColors && cell.isBold() && fg < 8) {
|
||||
fg += 8;
|
||||
}
|
||||
charElement.style.textDecorationColor = this._colors.ansi[fg].css;
|
||||
charElement.style.textDecorationColor = colors.ansi[fg].css;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -224,17 +229,17 @@ export class DomRendererRowFactory {
|
||||
// Apply selection foreground if applicable
|
||||
const isInSelection = this._isCellInSelection(x, row);
|
||||
if (!isTop) {
|
||||
if (this._colors.selectionForeground && isInSelection) {
|
||||
if (colors.selectionForeground && isInSelection) {
|
||||
fgColorMode = Attributes.CM_RGB;
|
||||
fg = this._colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
fgOverride = this._colors.selectionForeground;
|
||||
fg = colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
fgOverride = colors.selectionForeground;
|
||||
}
|
||||
}
|
||||
|
||||
// If in the selection, force the element to be above the selection to improve contrast and
|
||||
// support opaque selections
|
||||
if (isInSelection) {
|
||||
bgOverride = this._coreBrowserService.isFocused ? this._colors.selectionBackgroundOpaque : this._colors.selectionInactiveBackgroundOpaque;
|
||||
bgOverride = this._coreBrowserService.isFocused ? colors.selectionBackgroundOpaque : colors.selectionInactiveBackgroundOpaque;
|
||||
isTop = true;
|
||||
}
|
||||
|
||||
@@ -248,7 +253,7 @@ export class DomRendererRowFactory {
|
||||
switch (bgColorMode) {
|
||||
case Attributes.CM_P16:
|
||||
case Attributes.CM_P256:
|
||||
resolvedBg = this._colors.ansi[bg];
|
||||
resolvedBg = colors.ansi[bg];
|
||||
charElement.classList.add(`xterm-bg-${bg}`);
|
||||
break;
|
||||
case Attributes.CM_RGB:
|
||||
@@ -258,10 +263,10 @@ export class DomRendererRowFactory {
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (isInverse) {
|
||||
resolvedBg = this._colors.foreground;
|
||||
resolvedBg = colors.foreground;
|
||||
charElement.classList.add(`xterm-bg-${INVERTED_DEFAULT_COLOR}`);
|
||||
} else {
|
||||
resolvedBg = this._colors.background;
|
||||
resolvedBg = colors.background;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,7 +284,7 @@ export class DomRendererRowFactory {
|
||||
if (cell.isBold() && fg < 8 && this._optionsService.rawOptions.drawBoldTextInBrightColors) {
|
||||
fg += 8;
|
||||
}
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.ansi[fg], cell, bgOverride, undefined)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.ansi[fg], cell, bgOverride, undefined)) {
|
||||
charElement.classList.add(`xterm-fg-${fg}`);
|
||||
}
|
||||
break;
|
||||
@@ -295,7 +300,7 @@ export class DomRendererRowFactory {
|
||||
break;
|
||||
case Attributes.CM_DEFAULT:
|
||||
default:
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, this._colors.foreground, cell, bgOverride, undefined)) {
|
||||
if (!this._applyMinimumContrast(charElement, resolvedBg, colors.foreground, cell, bgOverride, undefined)) {
|
||||
if (isInverse) {
|
||||
charElement.classList.add(`xterm-fg-${INVERTED_DEFAULT_COLOR}`);
|
||||
}
|
||||
@@ -303,9 +308,17 @@ export class DomRendererRowFactory {
|
||||
}
|
||||
|
||||
fragment.appendChild(charElement);
|
||||
cellMap[x] = ++elemIndex;
|
||||
|
||||
x = lastCharX;
|
||||
}
|
||||
|
||||
// since the loop above might exit early not handling all cells,
|
||||
// also set remaining cell positions to last element index
|
||||
if (x < cols - 1) {
|
||||
cellMap.subarray(x).fill(++elemIndex);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@@ -317,13 +330,13 @@ export class DomRendererRowFactory {
|
||||
// Try get from cache first, only use the cache when there are no decoration overrides
|
||||
let adjustedColor: IColor | undefined | null = undefined;
|
||||
if (!bgOverride && !fgOverride) {
|
||||
adjustedColor = this._colors.contrastCache.getColor(bg.rgba, fg.rgba);
|
||||
adjustedColor = this._themeService.colors.contrastCache.getColor(bg.rgba, fg.rgba);
|
||||
}
|
||||
|
||||
// Calculate and store in cache
|
||||
if (adjustedColor === undefined) {
|
||||
adjustedColor = color.ensureContrastRatio(bgOverride || bg, fgOverride || fg, this._optionsService.rawOptions.minimumContrastRatio);
|
||||
this._colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
|
||||
this._themeService.colors.contrastCache.setColor((bgOverride || bg).rgba, (fgOverride || fg).rgba, adjustedColor ?? null);
|
||||
}
|
||||
|
||||
if (adjustedColor) {
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { ICoreBrowserService, IThemeService } from 'browser/services/Services';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { Attributes, BgFlags, FgFlags } from 'common/buffer/Constants';
|
||||
import { IDecorationService } from 'common/services/Services';
|
||||
import { ICellData } from 'common/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
// Work variables to avoid garbage collection
|
||||
let $fg = 0;
|
||||
let $bg = 0;
|
||||
let $hasFg = false;
|
||||
let $hasBg = false;
|
||||
let $isSelected = false;
|
||||
let $colors: ReadonlyColorSet | undefined;
|
||||
|
||||
export class CellColorResolver {
|
||||
/**
|
||||
* The shared result of the {@link resolve} call. This is only safe to use immediately after as
|
||||
* any other calls will share object.
|
||||
*/
|
||||
public readonly result: { fg: number, bg: number, ext: number } = {
|
||||
fg: 0,
|
||||
bg: 0,
|
||||
ext: 0
|
||||
};
|
||||
|
||||
constructor(
|
||||
private readonly _terminal: Terminal,
|
||||
private readonly _selectionRenderModel: ISelectionRenderModel,
|
||||
private readonly _decorationService: IDecorationService,
|
||||
private readonly _coreBrowserService: ICoreBrowserService,
|
||||
private readonly _themeService: IThemeService
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves colors for the cell, putting the result into the shared {@link result}. This resolves
|
||||
* overrides, inverse and selection for the cell which can then be used to feed into the renderer.
|
||||
*/
|
||||
public resolve(cell: ICellData, x: number, y: number): void {
|
||||
this.result.bg = cell.bg;
|
||||
this.result.fg = cell.fg;
|
||||
this.result.ext = cell.bg & BgFlags.HAS_EXTENDED ? cell.extended.ext : 0;
|
||||
// Get any foreground/background overrides, this happens on the model to avoid spreading
|
||||
// override logic throughout the different sub-renderers
|
||||
|
||||
// Reset overrides work variables
|
||||
$bg = 0;
|
||||
$fg = 0;
|
||||
$hasBg = false;
|
||||
$hasFg = false;
|
||||
$isSelected = false;
|
||||
$colors = this._themeService.colors;
|
||||
|
||||
// Apply decorations on the bottom layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasFg = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Apply the selection color if needed
|
||||
$isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y);
|
||||
if ($isSelected) {
|
||||
$bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF;
|
||||
$hasBg = true;
|
||||
if ($colors.selectionForeground) {
|
||||
$fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF;
|
||||
$hasFg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply decorations on the top layer
|
||||
this._decorationService.forEachDecorationAtCell(x, y, 'top', d => {
|
||||
if (d.backgroundColorRGB) {
|
||||
$bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasBg = true;
|
||||
}
|
||||
if (d.foregroundColorRGB) {
|
||||
$fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF;
|
||||
$hasFg = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Convert any overrides from rgba to the fg/bg packed format. This resolves the inverse flag
|
||||
// ahead of time in order to use the correct cache key
|
||||
if ($hasBg) {
|
||||
if ($isSelected) {
|
||||
// Non-RGB attributes from model + force non-dim + override + force RGB color mode
|
||||
$bg = (cell.bg & ~Attributes.RGB_MASK & ~BgFlags.DIM) | $bg | Attributes.CM_RGB;
|
||||
} else {
|
||||
// Non-RGB attributes from model + override + force RGB color mode
|
||||
$bg = (cell.bg & ~Attributes.RGB_MASK) | $bg | Attributes.CM_RGB;
|
||||
}
|
||||
}
|
||||
if ($hasFg) {
|
||||
// Non-RGB attributes from model + force disable inverse + override + force RGB color mode
|
||||
$fg = (cell.fg & ~Attributes.RGB_MASK & ~FgFlags.INVERSE) | $fg | Attributes.CM_RGB;
|
||||
}
|
||||
|
||||
// Handle case where inverse was specified by only one of bg override or fg override was set,
|
||||
// resolving the other inverse color and setting the inverse flag if needed.
|
||||
if (this.result.fg & FgFlags.INVERSE) {
|
||||
if ($hasBg && !$hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
$fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
$hasFg = true;
|
||||
}
|
||||
if (!$hasBg && $hasFg) {
|
||||
// Resolve bg color type (default color has a different meaning in fg vs bg)
|
||||
if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) {
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB;
|
||||
} else {
|
||||
$bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK);
|
||||
}
|
||||
$hasBg = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Release object
|
||||
$colors = undefined;
|
||||
|
||||
// Use the override if it exists
|
||||
this.result.bg = $hasBg ? $bg : this.result.bg;
|
||||
this.result.fg = $hasFg ? $fg : this.result.fg;
|
||||
}
|
||||
}
|
||||
+19
-20
@@ -3,38 +3,37 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { generateConfig, configEquals } from './CharAtlasUtils';
|
||||
import { WebglCharAtlas } from './WebglCharAtlas';
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Terminal } from 'xterm';
|
||||
import { IColorSet, ITerminal } from 'browser/Types';
|
||||
import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
|
||||
import { ITerminalOptions, Terminal } from 'xterm';
|
||||
import { ITerminal, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ICharAtlasConfig, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { generateConfig, configEquals } from 'browser/renderer/shared/CharAtlasUtils';
|
||||
|
||||
interface ICharAtlasCacheEntry {
|
||||
atlas: WebglCharAtlas;
|
||||
interface ITextureAtlasCacheEntry {
|
||||
atlas: ITextureAtlas;
|
||||
config: ICharAtlasConfig;
|
||||
// N.B. This implementation potentially holds onto copies of the terminal forever, so
|
||||
// this may cause memory leaks.
|
||||
ownedBy: Terminal[];
|
||||
}
|
||||
|
||||
const charAtlasCache: ICharAtlasCacheEntry[] = [];
|
||||
const charAtlasCache: ITextureAtlasCacheEntry[] = [];
|
||||
|
||||
/**
|
||||
* Acquires a char atlas, either generating a new one or returning an existing
|
||||
* one that is in use by another terminal.
|
||||
* @param terminal The terminal.
|
||||
* @param colors The colors to use.
|
||||
*/
|
||||
export function acquireCharAtlas(
|
||||
export function acquireTextureAtlas(
|
||||
terminal: Terminal,
|
||||
colors: IColorSet,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number,
|
||||
scaledCharWidth: number,
|
||||
scaledCharHeight: number,
|
||||
options: Required<ITerminalOptions>,
|
||||
colors: ReadonlyColorSet,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
deviceCharWidth: number,
|
||||
deviceCharHeight: number,
|
||||
devicePixelRatio: number
|
||||
): WebglCharAtlas {
|
||||
const newConfig = generateConfig(scaledCellWidth, scaledCellHeight, scaledCharWidth, scaledCharHeight, terminal, colors, devicePixelRatio);
|
||||
): ITextureAtlas {
|
||||
const newConfig = generateConfig(deviceCellWidth, deviceCellHeight, deviceCharWidth, deviceCharHeight, options, colors, devicePixelRatio);
|
||||
|
||||
// Check to see if the terminal already owns this config
|
||||
for (let i = 0; i < charAtlasCache.length; i++) {
|
||||
@@ -66,8 +65,8 @@ export function acquireCharAtlas(
|
||||
}
|
||||
|
||||
const core: ITerminal = (terminal as any)._core;
|
||||
const newEntry: ICharAtlasCacheEntry = {
|
||||
atlas: new WebglCharAtlas(document, newConfig, core.unicodeService),
|
||||
const newEntry: ITextureAtlasCacheEntry = {
|
||||
atlas: new TextureAtlas(document, newConfig, core.unicodeService),
|
||||
config: newConfig,
|
||||
ownedBy: [terminal]
|
||||
};
|
||||
+22
-27
@@ -5,16 +5,11 @@
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { Attributes } from 'common/buffer/Constants';
|
||||
import { Terminal, FontWeight } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IColor } from 'common/Types';
|
||||
import { ITerminalOptions } from 'xterm';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { NULL_COLOR } from 'common/Color';
|
||||
|
||||
const NULL_COLOR: IColor = {
|
||||
css: '',
|
||||
rgba: 0
|
||||
};
|
||||
|
||||
export function generateConfig(scaledCellWidth: number, scaledCellHeight: number, scaledCharWidth: number, scaledCharHeight: number, terminal: Terminal, colors: IColorSet, devicePixelRatio: number): ICharAtlasConfig {
|
||||
export function generateConfig(deviceCellWidth: number, deviceCellHeight: number, deviceCharWidth: number, deviceCharHeight: number, options: Required<ITerminalOptions>, colors: ReadonlyColorSet, devicePixelRatio: number): ICharAtlasConfig {
|
||||
// null out some fields that don't matter
|
||||
const clonedColors: IColorSet = {
|
||||
foreground: colors.foreground,
|
||||
@@ -32,21 +27,21 @@ export function generateConfig(scaledCellWidth: number, scaledCellHeight: number
|
||||
contrastCache: colors.contrastCache
|
||||
};
|
||||
return {
|
||||
customGlyphs: terminal.options.customGlyphs,
|
||||
customGlyphs: options.customGlyphs,
|
||||
devicePixelRatio,
|
||||
letterSpacing: terminal.options.letterSpacing,
|
||||
lineHeight: terminal.options.lineHeight,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
scaledCharWidth,
|
||||
scaledCharHeight,
|
||||
fontFamily: terminal.options.fontFamily,
|
||||
fontSize: terminal.options.fontSize,
|
||||
fontWeight: terminal.options.fontWeight,
|
||||
fontWeightBold: terminal.options.fontWeightBold,
|
||||
allowTransparency: terminal.options.allowTransparency,
|
||||
drawBoldTextInBrightColors: terminal.options.drawBoldTextInBrightColors,
|
||||
minimumContrastRatio: terminal.options.minimumContrastRatio,
|
||||
letterSpacing: options.letterSpacing,
|
||||
lineHeight: options.lineHeight,
|
||||
deviceCellWidth: deviceCellWidth,
|
||||
deviceCellHeight: deviceCellHeight,
|
||||
deviceCharWidth: deviceCharWidth,
|
||||
deviceCharHeight: deviceCharHeight,
|
||||
fontFamily: options.fontFamily,
|
||||
fontSize: options.fontSize,
|
||||
fontWeight: options.fontWeight,
|
||||
fontWeightBold: options.fontWeightBold,
|
||||
allowTransparency: options.allowTransparency,
|
||||
drawBoldTextInBrightColors: options.drawBoldTextInBrightColors,
|
||||
minimumContrastRatio: options.minimumContrastRatio,
|
||||
colors: clonedColors
|
||||
};
|
||||
}
|
||||
@@ -66,12 +61,12 @@ export function configEquals(a: ICharAtlasConfig, b: ICharAtlasConfig): boolean
|
||||
a.fontWeight === b.fontWeight &&
|
||||
a.fontWeightBold === b.fontWeightBold &&
|
||||
a.allowTransparency === b.allowTransparency &&
|
||||
a.scaledCharWidth === b.scaledCharWidth &&
|
||||
a.scaledCharHeight === b.scaledCharHeight &&
|
||||
a.deviceCharWidth === b.deviceCharWidth &&
|
||||
a.deviceCharHeight === b.deviceCharHeight &&
|
||||
a.drawBoldTextInBrightColors === b.drawBoldTextInBrightColors &&
|
||||
a.minimumContrastRatio === b.minimumContrastRatio &&
|
||||
a.colors.foreground === b.colors.foreground &&
|
||||
a.colors.background === b.colors.background;
|
||||
a.colors.foreground.rgba === b.colors.foreground.rgba &&
|
||||
a.colors.background.rgba === b.colors.background.rgba;
|
||||
}
|
||||
|
||||
export function is256Color(colorCode: number): boolean {
|
||||
+50
-28
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { throwIfFalsy } from 'browser/renderer/RendererUtils';
|
||||
import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
|
||||
interface IBlockVector {
|
||||
x: number;
|
||||
@@ -33,7 +33,7 @@ export const blockElementDefinitions: { [index: string]: IBlockVector[] | undefi
|
||||
'▐': [{ x: 4, y: 0, w: 4, h: 8 }], // RIGHT HALF BLOCK
|
||||
|
||||
// Block elements (0x2594-0x2595)
|
||||
'▔': [{ x: 0, y: 0, w: 9, h: 1 }], // UPPER ONE EIGHTH BLOCK
|
||||
'▔': [{ x: 0, y: 0, w: 8, h: 1 }], // UPPER ONE EIGHTH BLOCK
|
||||
'▕': [{ x: 7, y: 0, w: 1, h: 8 }], // RIGHT ONE EIGHTH BLOCK
|
||||
|
||||
// Terminal graphic characters (0x2596-0x259F)
|
||||
@@ -362,15 +362,31 @@ export const powerlineDefinitions: { [index: string]: IVectorShape } = {
|
||||
'\u{E0B2}': { d: 'M1,0 L0,.5 L1,1', type: VectorType.FILL, leftPadding: 2 },
|
||||
// Left triangle line
|
||||
'\u{E0B3}': { d: 'M2,-.5 L0,.5 L2,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Right semi-circle solid,
|
||||
// Right semi-circle solid
|
||||
'\u{E0B4}': { d: 'M0,0 L0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.FILL, rightPadding: 1 },
|
||||
// Right semi-circle line,
|
||||
// Right semi-circle line
|
||||
'\u{E0B5}': { d: 'M0,1 C0.552,1,1,0.776,1,.5 C1,0.224,0.552,0,0,0', type: VectorType.STROKE, rightPadding: 1 },
|
||||
// Left semi-circle solid,
|
||||
// Left semi-circle solid
|
||||
'\u{E0B6}': { d: 'M1,0 L1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.FILL, leftPadding: 1 },
|
||||
// Left semi-circle line,
|
||||
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: 1 }
|
||||
// Left semi-circle line
|
||||
'\u{E0B7}': { d: 'M1,1 C0.448,1,0,0.776,0,.5 C0,0.224,0.448,0,1,0', type: VectorType.STROKE, leftPadding: 1 },
|
||||
// Lower left triangle
|
||||
'\u{E0B8}': { d: 'M-.5,-.5 L1.5,1.5 L-.5,1.5', type: VectorType.FILL },
|
||||
// Backslash separator
|
||||
'\u{E0B9}': { d: 'M-.5,-.5 L1.5,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Lower right triangle
|
||||
'\u{E0BA}': { d: 'M1.5,-.5 L-.5,1.5 L1.5,1.5', type: VectorType.FILL },
|
||||
// Upper left triangle
|
||||
'\u{E0BC}': { d: 'M1.5,-.5 L-.5,1.5 L-.5,-.5', type: VectorType.FILL },
|
||||
// Forward slash separator
|
||||
'\u{E0BD}': { d: 'M1.5,-.5 L-.5,1.5', type: VectorType.STROKE, leftPadding: 1, rightPadding: 1 },
|
||||
// Upper right triangle
|
||||
'\u{E0BE}': { d: 'M-.5,-.5 L1.5,1.5 L1.5,-.5', type: VectorType.FILL }
|
||||
};
|
||||
// Backslash separator redundant
|
||||
powerlineDefinitions['\u{E0BB}'] = powerlineDefinitions['\u{E0B9}'];
|
||||
// Forward slash separator redundant
|
||||
powerlineDefinitions['\u{E0BF}'] = powerlineDefinitions['\u{E0BD}'];
|
||||
|
||||
/**
|
||||
* Try drawing a custom block element or box drawing character, returning whether it was
|
||||
@@ -381,32 +397,32 @@ export function tryDrawCustomChar(
|
||||
c: string,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number
|
||||
): boolean {
|
||||
const blockElementDefinition = blockElementDefinitions[c];
|
||||
if (blockElementDefinition) {
|
||||
drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
drawBlockElementChar(ctx, blockElementDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
const patternDefinition = patternCharacterDefinitions[c];
|
||||
if (patternDefinition) {
|
||||
drawPatternChar(ctx, patternDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
drawPatternChar(ctx, patternDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
return true;
|
||||
}
|
||||
|
||||
const boxDrawingDefinition = boxDrawingDefinitions[c];
|
||||
if (boxDrawingDefinition) {
|
||||
drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, devicePixelRatio);
|
||||
drawBoxDrawingChar(ctx, boxDrawingDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight, devicePixelRatio);
|
||||
return true;
|
||||
}
|
||||
|
||||
const powerlineDefinition = powerlineDefinitions[c];
|
||||
if (powerlineDefinition) {
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, scaledCellWidth, scaledCellHeight, fontSize, devicePixelRatio);
|
||||
drawPowerlineChar(ctx, powerlineDefinition, xOffset, yOffset, deviceCellWidth, deviceCellHeight, fontSize, devicePixelRatio);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -418,13 +434,13 @@ function drawBlockElementChar(
|
||||
charDefinition: IBlockVector[],
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
for (let i = 0; i < charDefinition.length; i++) {
|
||||
const box = charDefinition[i];
|
||||
const xEighth = scaledCellWidth / 8;
|
||||
const yEighth = scaledCellHeight / 8;
|
||||
const xEighth = deviceCellWidth / 8;
|
||||
const yEighth = deviceCellHeight / 8;
|
||||
ctx.fillRect(
|
||||
xOffset + box.x * xEighth,
|
||||
yOffset + box.y * yEighth,
|
||||
@@ -441,8 +457,8 @@ function drawPatternChar(
|
||||
charDefinition: number[][],
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number
|
||||
): void {
|
||||
let patternSet = cachedPatterns.get(charDefinition);
|
||||
if (!patternSet) {
|
||||
@@ -492,7 +508,7 @@ function drawPatternChar(
|
||||
patternSet.set(fillStyle, pattern);
|
||||
}
|
||||
ctx.fillStyle = pattern;
|
||||
ctx.fillRect(xOffset, yOffset, scaledCellWidth, scaledCellHeight);
|
||||
ctx.fillRect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -540,8 +556,8 @@ function drawBoxDrawingChar(
|
||||
charDefinition: { [fontWeight: number]: string | ((xp: number, yp: number) => string) },
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
devicePixelRatio: number
|
||||
): void {
|
||||
ctx.strokeStyle = ctx.fillStyle;
|
||||
@@ -551,7 +567,7 @@ function drawBoxDrawingChar(
|
||||
let actualInstructions: string;
|
||||
if (typeof instructions === 'function') {
|
||||
const xp = .15;
|
||||
const yp = .15 / scaledCellHeight * scaledCellWidth;
|
||||
const yp = .15 / deviceCellHeight * deviceCellWidth;
|
||||
actualInstructions = instructions(xp, yp);
|
||||
} else {
|
||||
actualInstructions = instructions;
|
||||
@@ -567,7 +583,7 @@ function drawBoxDrawingChar(
|
||||
if (!args[0] || !args[1]) {
|
||||
continue;
|
||||
}
|
||||
f(ctx, translateArgs(args, scaledCellWidth, scaledCellHeight, xOffset, yOffset, true, devicePixelRatio));
|
||||
f(ctx, translateArgs(args, deviceCellWidth, deviceCellHeight, xOffset, yOffset, true, devicePixelRatio));
|
||||
}
|
||||
ctx.stroke();
|
||||
ctx.closePath();
|
||||
@@ -579,11 +595,16 @@ function drawPowerlineChar(
|
||||
charDefinition: IVectorShape,
|
||||
xOffset: number,
|
||||
yOffset: number,
|
||||
scaledCellWidth: number,
|
||||
scaledCellHeight: number,
|
||||
deviceCellWidth: number,
|
||||
deviceCellHeight: number,
|
||||
fontSize: number,
|
||||
devicePixelRatio: number
|
||||
): void {
|
||||
// Clip the cell to make sure drawing doesn't occur beyond bounds
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xOffset, yOffset, deviceCellWidth, deviceCellHeight);
|
||||
ctx.clip(clipRegion);
|
||||
|
||||
ctx.beginPath();
|
||||
// Scale the stroke with DPR and font size
|
||||
const cssLineWidth = fontSize / 12;
|
||||
@@ -601,11 +622,12 @@ function drawPowerlineChar(
|
||||
}
|
||||
f(ctx, translateArgs(
|
||||
args,
|
||||
scaledCellWidth,
|
||||
scaledCellHeight,
|
||||
deviceCellWidth,
|
||||
deviceCellHeight,
|
||||
xOffset,
|
||||
yOffset,
|
||||
false,
|
||||
devicePixelRatio,
|
||||
(charDefinition.leftPadding ?? 0) * (cssLineWidth / 2),
|
||||
(charDefinition.rightPadding ?? 0) * (cssLineWidth / 2)
|
||||
));
|
||||
@@ -0,0 +1 @@
|
||||
This folder contains files that are shared between the renderer addons, but not necessarily bundled into the `xterm` module.
|
||||
@@ -3,6 +3,8 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IDimensions, IOffset, IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
|
||||
export function throwIfFalsy<T>(value: T | undefined | null): T {
|
||||
if (!value) {
|
||||
throw new Error('value must not be falsy');
|
||||
@@ -28,3 +30,29 @@ function isBoxOrBlockGlyph(codepoint: number): boolean {
|
||||
export function excludeFromContrastRatioDemands(codepoint: number): boolean {
|
||||
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
|
||||
}
|
||||
|
||||
export function createRenderDimensions(): IRenderDimensions {
|
||||
return {
|
||||
css: {
|
||||
canvas: createDimension(),
|
||||
cell: createDimension()
|
||||
},
|
||||
device: {
|
||||
canvas: createDimension(),
|
||||
cell: createDimension(),
|
||||
char: {
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
top: 0
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createDimension(): IDimensions {
|
||||
return {
|
||||
width: 0,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ISelectionRenderModel } from 'browser/renderer/shared/Types';
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
class SelectionRenderModel implements ISelectionRenderModel {
|
||||
public hasSelection!: boolean;
|
||||
public columnSelectMode!: boolean;
|
||||
public viewportStartRow!: number;
|
||||
public viewportEndRow!: number;
|
||||
public viewportCappedStartRow!: number;
|
||||
public viewportCappedEndRow!: number;
|
||||
public startCol!: number;
|
||||
public endCol!: number;
|
||||
public selectionStart: [number, number] | undefined;
|
||||
public selectionEnd: [number, number] | undefined;
|
||||
|
||||
constructor() {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.hasSelection = false;
|
||||
this.columnSelectMode = false;
|
||||
this.viewportStartRow = 0;
|
||||
this.viewportEndRow = 0;
|
||||
this.viewportCappedStartRow = 0;
|
||||
this.viewportCappedEndRow = 0;
|
||||
this.startCol = 0;
|
||||
this.endCol = 0;
|
||||
this.selectionStart = undefined;
|
||||
this.selectionEnd = undefined;
|
||||
}
|
||||
|
||||
public update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean = false): void {
|
||||
this.selectionStart = start;
|
||||
this.selectionEnd = end;
|
||||
// Selection does not exist
|
||||
if (!start || !end || (start[0] === end[0] && start[1] === end[1])) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
// Translate from buffer position to viewport position
|
||||
const viewportStartRow = start[1] - terminal.buffer.active.viewportY;
|
||||
const viewportEndRow = end[1] - terminal.buffer.active.viewportY;
|
||||
const viewportCappedStartRow = Math.max(viewportStartRow, 0);
|
||||
const viewportCappedEndRow = Math.min(viewportEndRow, terminal.rows - 1);
|
||||
|
||||
// No need to draw the selection
|
||||
if (viewportCappedStartRow >= terminal.rows || viewportCappedEndRow < 0) {
|
||||
this.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
this.hasSelection = true;
|
||||
this.columnSelectMode = columnSelectMode;
|
||||
this.viewportStartRow = viewportStartRow;
|
||||
this.viewportEndRow = viewportEndRow;
|
||||
this.viewportCappedStartRow = viewportCappedStartRow;
|
||||
this.viewportCappedEndRow = viewportCappedEndRow;
|
||||
this.startCol = start[0];
|
||||
this.endCol = end[0];
|
||||
}
|
||||
|
||||
public isCellSelected(terminal: Terminal, x: number, y: number): boolean {
|
||||
if (!this.hasSelection) {
|
||||
return false;
|
||||
}
|
||||
y -= terminal.buffer.active.viewportY;
|
||||
if (this.columnSelectMode) {
|
||||
if (this.startCol <= this.endCol) {
|
||||
return x >= this.startCol && y >= this.viewportCappedStartRow &&
|
||||
x < this.endCol && y <= this.viewportCappedEndRow;
|
||||
}
|
||||
return x < this.startCol && y >= this.viewportCappedStartRow &&
|
||||
x >= this.endCol && y <= this.viewportCappedEndRow;
|
||||
}
|
||||
return (y > this.viewportStartRow && y < this.viewportEndRow) ||
|
||||
(this.viewportStartRow === this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol && x < this.endCol) ||
|
||||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportEndRow && x < this.endCol) ||
|
||||
(this.viewportStartRow < this.viewportEndRow && y === this.viewportStartRow && x >= this.startCol);
|
||||
}
|
||||
}
|
||||
|
||||
export function createSelectionRenderModel(): ISelectionRenderModel {
|
||||
return new SelectionRenderModel();
|
||||
}
|
||||
+382
-175
@@ -3,120 +3,108 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ICharAtlasConfig } from './Types';
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants';
|
||||
import { IRasterizedGlyph, IBoundingBox } from '../Types';
|
||||
import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants';
|
||||
import { DEFAULT_COLOR, Attributes, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants';
|
||||
import { throwIfFalsy } from '../WebglUtils';
|
||||
import { IColor } from 'common/Types';
|
||||
import { IDisposable } from 'xterm';
|
||||
import { AttributeData } from 'common/buffer/AttributeData';
|
||||
import { color, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph } from 'browser/renderer/RendererUtils';
|
||||
import { color, NULL_COLOR, rgba } from 'common/Color';
|
||||
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs';
|
||||
import { excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils';
|
||||
import { IUnicodeService } from 'common/services/Services';
|
||||
import { FourKeyMap } from 'common/MultiKeyMap';
|
||||
|
||||
// For debugging purposes, it can be useful to set this to a really tiny value,
|
||||
// to verify that LRU eviction works.
|
||||
const TEXTURE_WIDTH = 1024;
|
||||
const TEXTURE_HEIGHT = 1024;
|
||||
|
||||
/**
|
||||
* The amount of the texture to be filled before throwing it away and starting
|
||||
* again. Since the throw away and individual glyph draws don't cost too much,
|
||||
* this prevent juggling multiple textures in the GL context.
|
||||
*/
|
||||
const TEXTURE_CAPACITY = Math.floor(TEXTURE_HEIGHT * 0.8);
|
||||
|
||||
const TRANSPARENT_COLOR = {
|
||||
css: 'rgba(0, 0, 0, 0)',
|
||||
rgba: 0
|
||||
};
|
||||
import { IdleTaskQueue } from 'common/TaskQueue';
|
||||
import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, IRequestRedrawEvent, ITextureAtlas } from 'browser/renderer/shared/Types';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
|
||||
/**
|
||||
* A shared object which is used to draw nothing for a particular cell.
|
||||
*/
|
||||
const NULL_RASTERIZED_GLYPH: IRasterizedGlyph = {
|
||||
offset: { x: 0, y: 0 },
|
||||
texturePage: 0,
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
offset: { x: 0, y: 0 },
|
||||
size: { x: 0, y: 0 },
|
||||
sizeClipSpace: { x: 0, y: 0 }
|
||||
};
|
||||
|
||||
const TMP_CANVAS_GLYPH_PADDING = 2;
|
||||
|
||||
const enum Constants {
|
||||
/**
|
||||
* The amount of pixel padding to allow in each row. Setting this to zero would make the atlas
|
||||
* page pack as tightly as possible, but more pages would end up being created as a result.
|
||||
*/
|
||||
ROW_PIXEL_THRESHOLD = 2,
|
||||
/**
|
||||
* The maximum texture size regardless of what the actual hardware maximum turns out to be. This
|
||||
* is enforced to ensure uploading the texture still finishes in a reasonable amount of time. A
|
||||
* 4096 squared image takes up 16MB of GPU memory.
|
||||
*/
|
||||
FORCED_MAX_TEXTURE_SIZE = 4096
|
||||
}
|
||||
|
||||
interface ICharAtlasActiveRow {
|
||||
x: number;
|
||||
y: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
/** Work variables to avoid garbage collection. */
|
||||
const w: { glyph: IRasterizedGlyph | undefined } = {
|
||||
glyph: undefined
|
||||
};
|
||||
// Work variables to avoid garbage collection
|
||||
let $glyph = undefined;
|
||||
|
||||
export class WebglCharAtlas implements IDisposable {
|
||||
export class TextureAtlas implements ITextureAtlas {
|
||||
private _didWarmUp: boolean = false;
|
||||
|
||||
private _cacheMap: FourKeyMap<number, number, number, number, IRasterizedGlyph> = new FourKeyMap();
|
||||
private _cacheMapCombined: FourKeyMap<string, number, number, number, IRasterizedGlyph> = new FourKeyMap();
|
||||
|
||||
// The texture that the atlas is drawn to
|
||||
public cacheCanvas: HTMLCanvasElement;
|
||||
private _cacheCtx: CanvasRenderingContext2D;
|
||||
private _pages: AtlasPage[] = [];
|
||||
public get pages(): { canvas: HTMLCanvasElement, version: number }[] { return this._pages; }
|
||||
|
||||
// The set of atlas pages that can be written to
|
||||
private _activePages: AtlasPage[] = [];
|
||||
|
||||
private _tmpCanvas: HTMLCanvasElement;
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
// Texture atlas current positioning data. The texture packing strategy used is to fill from
|
||||
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
|
||||
// row's height, the following happens:
|
||||
//
|
||||
// - The current row becomes the fixed height row A
|
||||
// - A new fixed height row B the exact size of the glyph is created below the current row
|
||||
// - A new dynamic height current row is created below B
|
||||
//
|
||||
// This strategy does a good job preventing space being wasted for very short glyphs such as
|
||||
// underscores, hyphens etc. or those with underlines rendered.
|
||||
private _currentRow: ICharAtlasActiveRow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 0
|
||||
};
|
||||
private readonly _fixedRows: ICharAtlasActiveRow[] = [];
|
||||
|
||||
public hasCanvasChanged = false;
|
||||
|
||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||
private _workAttributeData: AttributeData = new AttributeData();
|
||||
|
||||
private _textureSize: number = 512;
|
||||
|
||||
public static maxAtlasPages: number | undefined;
|
||||
public static maxTextureSize: number | undefined;
|
||||
|
||||
private readonly _onAddTextureAtlasCanvas = new EventEmitter<HTMLCanvasElement>();
|
||||
public readonly onAddTextureAtlasCanvas = this._onAddTextureAtlasCanvas.event;
|
||||
private readonly _onRemoveTextureAtlasCanvas = new EventEmitter<HTMLCanvasElement>();
|
||||
public readonly onRemoveTextureAtlasCanvas = this._onRemoveTextureAtlasCanvas.event;
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
private readonly _document: Document,
|
||||
private readonly _config: ICharAtlasConfig,
|
||||
private readonly _unicodeService: IUnicodeService
|
||||
) {
|
||||
this.cacheCanvas = document.createElement('canvas');
|
||||
this.cacheCanvas.width = TEXTURE_WIDTH;
|
||||
this.cacheCanvas.height = TEXTURE_HEIGHT;
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this._cacheCtx = throwIfFalsy(this.cacheCanvas.getContext('2d', { alpha: true }));
|
||||
|
||||
this._tmpCanvas = document.createElement('canvas');
|
||||
this._tmpCanvas.width = this._config.scaledCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCanvas.height = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', { alpha: this._config.allowTransparency }));
|
||||
this._createNewPage();
|
||||
this._tmpCanvas = createCanvas(
|
||||
_document,
|
||||
this._config.deviceCellWidth * 4 + TMP_CANVAS_GLYPH_PADDING * 2,
|
||||
this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 2
|
||||
);
|
||||
this._tmpCtx = throwIfFalsy(this._tmpCanvas.getContext('2d', {
|
||||
alpha: this._config.allowTransparency,
|
||||
willReadFrequently: true
|
||||
}));
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this.cacheCanvas.parentElement) {
|
||||
this.cacheCanvas.parentElement.removeChild(this.cacheCanvas);
|
||||
for (const page of this.pages) {
|
||||
page.canvas.remove();
|
||||
}
|
||||
this._onAddTextureAtlasCanvas.dispose();
|
||||
}
|
||||
|
||||
public warmUp(): void {
|
||||
@@ -127,36 +115,133 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
|
||||
private _doWarmUp(): void {
|
||||
// Pre-fill with ASCII 33-126
|
||||
// Pre-fill with ASCII 33-126, this is not urgent and done in idle callbacks
|
||||
const queue = new IdleTaskQueue();
|
||||
for (let i = 33; i < 126; i++) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
|
||||
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
|
||||
queue.enqueue(() => {
|
||||
if (!this._cacheMap.get(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT)) {
|
||||
const rasterizedGlyph = this._drawToCache(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT);
|
||||
this._cacheMap.set(i, DEFAULT_COLOR, DEFAULT_COLOR, DEFAULT_EXT, rasterizedGlyph);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _requestClearModel = false;
|
||||
public beginFrame(): boolean {
|
||||
if (this._currentRow.y > TEXTURE_CAPACITY) {
|
||||
this.clearTexture();
|
||||
this.warmUp();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return this._requestClearModel;
|
||||
}
|
||||
|
||||
public clearTexture(): void {
|
||||
if (this._currentRow.x === 0 && this._currentRow.y === 0) {
|
||||
if (this._pages[0].currentRow.x === 0 && this._pages[0].currentRow.y === 0) {
|
||||
return;
|
||||
}
|
||||
this._cacheCtx.clearRect(0, 0, TEXTURE_WIDTH, TEXTURE_HEIGHT);
|
||||
for (const page of this._pages) {
|
||||
page.clear();
|
||||
}
|
||||
this._cacheMap.clear();
|
||||
this._cacheMapCombined.clear();
|
||||
this._currentRow.x = 0;
|
||||
this._currentRow.y = 0;
|
||||
this._currentRow.height = 0;
|
||||
this._fixedRows.length = 0;
|
||||
this._didWarmUp = false;
|
||||
}
|
||||
|
||||
private _createNewPage(): AtlasPage {
|
||||
// Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
|
||||
// microtask to ensure it does not interrupt textures that will be rendered in the current
|
||||
// animation frame which would result in blank rendered areas. This is actually not that
|
||||
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
|
||||
if (TextureAtlas.maxAtlasPages && this._pages.length >= Math.max(4, TextureAtlas.maxAtlasPages / 2)) {
|
||||
queueMicrotask(() => {
|
||||
// Find the set of the largest 4 images, below the maximum size, with the highest
|
||||
// percentages used
|
||||
const pagesBySize = this._pages.filter(e => {
|
||||
return e.canvas.width * 2 <= (TextureAtlas.maxTextureSize || Constants.FORCED_MAX_TEXTURE_SIZE);
|
||||
}).sort((a, b) => {
|
||||
if (b.canvas.width !== a.canvas.width) {
|
||||
return b.canvas.width - a.canvas.width;
|
||||
}
|
||||
return b.percentageUsed - a.percentageUsed;
|
||||
});
|
||||
let sameSizeI = -1;
|
||||
let size = 0;
|
||||
for (let i = 0; i < pagesBySize.length; i++) {
|
||||
if (pagesBySize[i].canvas.width !== size) {
|
||||
sameSizeI = i;
|
||||
size = pagesBySize[i].canvas.width;
|
||||
} else if (i - sameSizeI === 3) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Gather details of the merge
|
||||
const mergingPages = pagesBySize.slice(sameSizeI, sameSizeI + 4);
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
||||
const mergedPageIndex = sortedMergingPagesIndexes[0];
|
||||
|
||||
// Merge into the new page
|
||||
const mergedPage = this._mergePages(mergingPages, mergedPageIndex);
|
||||
mergedPage.version++;
|
||||
|
||||
// Replace the first _merging_ page with the _merged_ page
|
||||
this._pages[mergedPageIndex] = mergedPage;
|
||||
|
||||
// Delete the other 3 pages, shifting glyph texture pages as needed
|
||||
for (let i = sortedMergingPagesIndexes.length - 1; i >= 1; i--) {
|
||||
this._deletePage(sortedMergingPagesIndexes[i]);
|
||||
}
|
||||
|
||||
// Request the model to be cleared to refresh all texture pages.
|
||||
this._requestClearModel = true;
|
||||
this._onAddTextureAtlasCanvas.fire(mergedPage.canvas);
|
||||
});
|
||||
}
|
||||
|
||||
// All new atlas pages are created small as they are highly dynamic
|
||||
const newPage = new AtlasPage(this._document, this._textureSize);
|
||||
this._pages.push(newPage);
|
||||
this._activePages.push(newPage);
|
||||
this._onAddTextureAtlasCanvas.fire(newPage.canvas);
|
||||
return newPage;
|
||||
}
|
||||
|
||||
private _mergePages(mergingPages: AtlasPage[], mergedPageIndex: number): AtlasPage {
|
||||
const mergedSize = mergingPages[0].canvas.width * 2;
|
||||
const mergedPage = new AtlasPage(this._document, mergedSize, mergingPages);
|
||||
for (const [i, p] of mergingPages.entries()) {
|
||||
const xOffset = i * p.canvas.width % mergedSize;
|
||||
const yOffset = Math.floor(i / 2) * p.canvas.height;
|
||||
mergedPage.ctx.drawImage(p.canvas, xOffset, yOffset);
|
||||
for (const g of p.glyphs) {
|
||||
g.texturePage = mergedPageIndex;
|
||||
g.sizeClipSpace.x = g.size.x / mergedSize;
|
||||
g.sizeClipSpace.y = g.size.y / mergedSize;
|
||||
g.texturePosition.x += xOffset;
|
||||
g.texturePosition.y += yOffset;
|
||||
g.texturePositionClipSpace.x = g.texturePosition.x / mergedSize;
|
||||
g.texturePositionClipSpace.y = g.texturePosition.y / mergedSize;
|
||||
}
|
||||
|
||||
this._onRemoveTextureAtlasCanvas.fire(p.canvas);
|
||||
|
||||
// Remove the merging page from active pages if it was there
|
||||
const index = this._activePages.indexOf(p);
|
||||
if (index !== -1) {
|
||||
this._activePages.splice(index, 1);
|
||||
}
|
||||
}
|
||||
return mergedPage;
|
||||
}
|
||||
|
||||
private _deletePage(pageIndex: number): void {
|
||||
this._pages.splice(pageIndex, 1);
|
||||
for (let j = pageIndex; j < this._pages.length; j++) {
|
||||
const adjustingPage = this._pages[j];
|
||||
for (const g of adjustingPage.glyphs) {
|
||||
g.texturePage--;
|
||||
}
|
||||
adjustingPage.version++;
|
||||
}
|
||||
}
|
||||
|
||||
public getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
return this._getFromCacheMap(this._cacheMapCombined, chars, bg, fg, ext);
|
||||
}
|
||||
@@ -175,12 +260,12 @@ export class WebglCharAtlas implements IDisposable {
|
||||
fg: number,
|
||||
ext: number
|
||||
): IRasterizedGlyph {
|
||||
w.glyph = cacheMap.get(key, bg, fg, ext);
|
||||
if (!w.glyph) {
|
||||
w.glyph = this._drawToCache(key, bg, fg, ext);
|
||||
cacheMap.set(key, bg, fg, ext, w.glyph);
|
||||
$glyph = cacheMap.get(key, bg, fg, ext);
|
||||
if (!$glyph) {
|
||||
$glyph = this._drawToCache(key, bg, fg, ext);
|
||||
cacheMap.set(key, bg, fg, ext, $glyph);
|
||||
}
|
||||
return w.glyph;
|
||||
return $glyph;
|
||||
}
|
||||
|
||||
private _getColorFromAnsiIndex(idx: number): IColor {
|
||||
@@ -195,7 +280,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// The background color might have some transparency, so we need to render it as fully
|
||||
// transparent in the atlas. Otherwise we'd end up drawing the transparent background twice
|
||||
// around the anti-aliased edges of the glyph, and it would look too dark.
|
||||
return TRANSPARENT_COLOR;
|
||||
return NULL_COLOR;
|
||||
}
|
||||
|
||||
let result: IColor;
|
||||
@@ -338,17 +423,18 @@ export class WebglCharAtlas implements IDisposable {
|
||||
private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number): IRasterizedGlyph {
|
||||
const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars;
|
||||
|
||||
this.hasCanvasChanged = true;
|
||||
// Uncomment for debugging
|
||||
// console.log(`draw to cache "${chars}"`, bg, fg, ext);
|
||||
|
||||
// Allow 1 cell width per character, with a minimum of 2 (CJK), plus some padding. This is used
|
||||
// to draw the glyph to the canvas as well as to restrict the bounding box search to ensure
|
||||
// giant ligatures (eg. =====>) don't impact overall performance.
|
||||
const allowedWidth = this._config.scaledCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
const allowedWidth = this._config.deviceCellWidth * Math.max(chars.length, 2) + TMP_CANVAS_GLYPH_PADDING * 2;
|
||||
if (this._tmpCanvas.width < allowedWidth) {
|
||||
this._tmpCanvas.width = allowedWidth;
|
||||
}
|
||||
// Include line height when drawing glyphs
|
||||
const allowedHeight = this._config.scaledCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
|
||||
const allowedHeight = this._config.deviceCellHeight + TMP_CANVAS_GLYPH_PADDING * 4;
|
||||
if (this._tmpCanvas.height < allowedHeight) {
|
||||
this._tmpCanvas.height = allowedHeight;
|
||||
}
|
||||
@@ -409,7 +495,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Draw custom characters if applicable
|
||||
let customGlyph = false;
|
||||
if (this._config.customGlyphs !== false) {
|
||||
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight, this._config.fontSize, this._config.devicePixelRatio);
|
||||
customGlyph = tryDrawCustomChar(this._tmpCtx, chars, padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight, this._config.fontSize, this._config.devicePixelRatio);
|
||||
}
|
||||
|
||||
// Whether to clear pixels based on a threshold difference between the glyph color and the
|
||||
@@ -450,15 +536,15 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// Underline style/stroke
|
||||
this._tmpCtx.beginPath();
|
||||
const xLeft = padding;
|
||||
const yTop = Math.ceil(padding + this._config.scaledCharHeight) - yOffset;
|
||||
const yMid = padding + this._config.scaledCharHeight + lineWidth - yOffset;
|
||||
const yBot = Math.ceil(padding + this._config.scaledCharHeight + lineWidth * 2) - yOffset;
|
||||
const yTop = Math.ceil(padding + this._config.deviceCharHeight) - yOffset;
|
||||
const yMid = padding + this._config.deviceCharHeight + lineWidth - yOffset;
|
||||
const yBot = Math.ceil(padding + this._config.deviceCharHeight + lineWidth * 2) - yOffset;
|
||||
|
||||
for (let i = 0; i < chWidth; i++) {
|
||||
this._tmpCtx.save();
|
||||
const xChLeft = xLeft + i * this._config.scaledCellWidth;
|
||||
const xChRight = xLeft + (i + 1) * this._config.scaledCellWidth;
|
||||
const xChMid = xChLeft + this._config.scaledCellWidth / 2;
|
||||
const xChLeft = xLeft + i * this._config.deviceCellWidth;
|
||||
const xChRight = xLeft + (i + 1) * this._config.deviceCellWidth;
|
||||
const xChMid = xChLeft + this._config.deviceCellWidth / 2;
|
||||
switch (this._workAttributeData.extended.underlineStyle) {
|
||||
case UnderlineStyle.DOUBLE:
|
||||
this._tmpCtx.moveTo(xChLeft, yTop);
|
||||
@@ -469,18 +555,18 @@ export class WebglCharAtlas implements IDisposable {
|
||||
case UnderlineStyle.CURLY:
|
||||
// Choose the bezier top and bottom based on the device pixel ratio, the curly line is
|
||||
// made taller when the line width is as otherwise it's not very clear otherwise.
|
||||
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.scaledCharHeight - lineWidth / 2) - yOffset;
|
||||
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.scaledCharHeight + lineWidth / 2) - yOffset;
|
||||
const yCurlyBot = lineWidth <= 1 ? yBot : Math.ceil(padding + this._config.deviceCharHeight - lineWidth / 2) - yOffset;
|
||||
const yCurlyTop = lineWidth <= 1 ? yTop : Math.ceil(padding + this._config.deviceCharHeight + lineWidth / 2) - yOffset;
|
||||
// Clip the left and right edges of the underline such that it can be drawn just outside
|
||||
// the edge of the cell to ensure a continuous stroke when there are multiple underlined
|
||||
// glyphs adjacent to one another.
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xChLeft, yTop, this._config.scaledCellWidth, yBot - yTop);
|
||||
clipRegion.rect(xChLeft, yTop, this._config.deviceCellWidth, yBot - yTop);
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
// Start 1/2 cell before and end 1/2 cells after to ensure a smooth curve with other cells
|
||||
this._tmpCtx.moveTo(xChLeft - this._config.scaledCellWidth / 2, yMid);
|
||||
this._tmpCtx.moveTo(xChLeft - this._config.deviceCellWidth / 2, yMid);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChLeft - this._config.scaledCellWidth / 2, yCurlyTop,
|
||||
xChLeft - this._config.deviceCellWidth / 2, yCurlyTop,
|
||||
xChLeft, yCurlyTop,
|
||||
xChLeft, yMid
|
||||
);
|
||||
@@ -496,8 +582,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
);
|
||||
this._tmpCtx.bezierCurveTo(
|
||||
xChRight, yCurlyBot,
|
||||
xChRight + this._config.scaledCellWidth / 2, yCurlyBot,
|
||||
xChRight + this._config.scaledCellWidth / 2, yMid
|
||||
xChRight + this._config.deviceCellWidth / 2, yCurlyBot,
|
||||
xChRight + this._config.deviceCellWidth / 2, yMid
|
||||
);
|
||||
break;
|
||||
case UnderlineStyle.DOTTED:
|
||||
@@ -541,11 +627,11 @@ export class WebglCharAtlas implements IDisposable {
|
||||
// outline around the whole glyph, as well as additional pixels in the glyph at the top
|
||||
// which would increase GPU memory demands
|
||||
const clipRegion = new Path2D();
|
||||
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.scaledCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
|
||||
clipRegion.rect(xLeft, yTop - Math.ceil(lineWidth / 2), this._config.deviceCellWidth, yBot - yTop + Math.ceil(lineWidth / 2));
|
||||
this._tmpCtx.clip(clipRegion);
|
||||
this._tmpCtx.lineWidth = this._config.devicePixelRatio * 3;
|
||||
this._tmpCtx.strokeStyle = backgroundColor.css;
|
||||
this._tmpCtx.strokeText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
this._tmpCtx.strokeText(chars, padding, padding + this._config.deviceCharHeight);
|
||||
this._tmpCtx.restore();
|
||||
}
|
||||
}
|
||||
@@ -554,21 +640,21 @@ export class WebglCharAtlas implements IDisposable {
|
||||
|
||||
// Draw the character
|
||||
if (!customGlyph) {
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight);
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight);
|
||||
}
|
||||
|
||||
// If this charcater is underscore and beyond the cell bounds, shift it up until it is visible
|
||||
// even on the bottom row, try for a maximum of 5 pixels.
|
||||
if (chars === '_' && !this._config.allowTransparency) {
|
||||
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
let isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
if (isBeyondCellBounds) {
|
||||
for (let offset = 1; offset <= 5; offset++) {
|
||||
this._tmpCtx.save();
|
||||
this._tmpCtx.fillStyle = backgroundColor.css;
|
||||
this._tmpCtx.fillRect(0, 0, this._tmpCanvas.width, this._tmpCanvas.height);
|
||||
this._tmpCtx.restore();
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight - offset);
|
||||
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.scaledCellWidth, this._config.scaledCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
this._tmpCtx.fillText(chars, padding, padding + this._config.deviceCharHeight - offset);
|
||||
isBeyondCellBounds = clearColor(this._tmpCtx.getImageData(padding, padding, this._config.deviceCellWidth, this._config.deviceCellHeight), backgroundColor, foregroundColor, enableClearThresholdCheck);
|
||||
if (!isBeyondCellBounds) {
|
||||
break;
|
||||
}
|
||||
@@ -583,8 +669,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
this._tmpCtx.lineWidth = lineWidth;
|
||||
this._tmpCtx.strokeStyle = this._tmpCtx.fillStyle;
|
||||
this._tmpCtx.beginPath();
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.scaledCharWidth * chWidth, padding + Math.floor(this._config.scaledCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.moveTo(padding, padding + Math.floor(this._config.deviceCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.lineTo(padding + this._config.deviceCharWidth * chWidth, padding + Math.floor(this._config.deviceCharHeight / 2) - yOffset);
|
||||
this._tmpCtx.stroke();
|
||||
}
|
||||
|
||||
@@ -610,63 +696,119 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
|
||||
const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, allowedWidth, restrictedPowerlineGlyph, customGlyph, padding);
|
||||
const clippedImageData = this._clipImageData(imageData, this._workBoundingBox);
|
||||
|
||||
// Find the best atlas row to use
|
||||
let activePage: AtlasPage;
|
||||
let activeRow: ICharAtlasActiveRow;
|
||||
while (true) {
|
||||
// Select the ideal existing row, preferring fixed rows over the current row
|
||||
activeRow = this._currentRow;
|
||||
for (const row of this._fixedRows) {
|
||||
if ((activeRow === this._currentRow || row.height < activeRow.height) && rasterizedGlyph.size.y <= row.height) {
|
||||
activeRow = row;
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new one if vertical space would be wasted, fixing the previously active row in the
|
||||
// process as it now has a fixed height
|
||||
if (activeRow.height > rasterizedGlyph.size.y * 2) {
|
||||
// Fix the current row as the new row is being added below
|
||||
if (this._currentRow.height > 0) {
|
||||
this._fixedRows.push(this._currentRow);
|
||||
}
|
||||
|
||||
// Create the new fixed height row
|
||||
activeRow = {
|
||||
x: 0,
|
||||
y: this._currentRow.y + this._currentRow.height,
|
||||
height: rasterizedGlyph.size.y
|
||||
};
|
||||
this._fixedRows.push(activeRow);
|
||||
|
||||
// Create the new current row below the new fixed height row
|
||||
this._currentRow = {
|
||||
x: 0,
|
||||
y: activeRow.y + activeRow.height,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
|
||||
// Exit the loop if there is enough room in the row
|
||||
if (activeRow.x + rasterizedGlyph.size.x <= TEXTURE_WIDTH) {
|
||||
// If there are no active pages (the last smallest 4 were merged), create a new one
|
||||
if (this._activePages.length === 0) {
|
||||
const newPage = this._createNewPage();
|
||||
activePage = newPage;
|
||||
activeRow = newPage.currentRow;
|
||||
activeRow.height = rasterizedGlyph.size.y;
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is enough room in the current row, finish it and try again
|
||||
if (activeRow === this._currentRow) {
|
||||
// Get the best current row from all active pages
|
||||
activePage = this._activePages[this._activePages.length - 1];
|
||||
activeRow = activePage.currentRow;
|
||||
for (const p of this._activePages) {
|
||||
if (rasterizedGlyph.size.y <= p.currentRow.height) {
|
||||
activePage = p;
|
||||
activeRow = p.currentRow;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This algorithm could be simplified:
|
||||
// - Search for the page with ROW_PIXEL_THRESHOLD in mind
|
||||
// - Keep track of current/fixed rows in a Map
|
||||
|
||||
// Replace the best current row with a fixed row if there is one at least as good as the
|
||||
// current row. Search in reverse to prioritize filling in older pages.
|
||||
for (let i = this._activePages.length - 1; i >= 0; i--) {
|
||||
for (const row of this._activePages[i].fixedRows) {
|
||||
if (row.height <= activeRow.height && rasterizedGlyph.size.y <= row.height) {
|
||||
activePage = this._activePages[i];
|
||||
activeRow = row;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create a new one if too much vertical space would be wasted or there is not enough room
|
||||
// left in the page. The previous active row will become fixed in the process as it now has a
|
||||
// fixed height
|
||||
if (activeRow.y + rasterizedGlyph.size.y >= activePage.canvas.height || activeRow.height > rasterizedGlyph.size.y + Constants.ROW_PIXEL_THRESHOLD) {
|
||||
// Create the new fixed height row, creating a new page if there isn't enough room on the
|
||||
// current page
|
||||
let wasNewPageCreated = false;
|
||||
if (activePage.currentRow.y + activePage.currentRow.height + rasterizedGlyph.size.y >= activePage.canvas.height) {
|
||||
// Find the first page with room to create the new row on
|
||||
let candidatePage: AtlasPage | undefined;
|
||||
for (const p of this._activePages) {
|
||||
if (p.currentRow.y + p.currentRow.height + rasterizedGlyph.size.y < p.canvas.height) {
|
||||
candidatePage = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (candidatePage) {
|
||||
activePage = candidatePage;
|
||||
} else {
|
||||
// Create a new page if there is no room
|
||||
const newPage = this._createNewPage();
|
||||
activePage = newPage;
|
||||
activeRow = newPage.currentRow;
|
||||
activeRow.height = rasterizedGlyph.size.y;
|
||||
wasNewPageCreated = true;
|
||||
}
|
||||
}
|
||||
if (!wasNewPageCreated) {
|
||||
// Fix the current row as the new row is being added below
|
||||
if (activePage.currentRow.height > 0) {
|
||||
activePage.fixedRows.push(activePage.currentRow);
|
||||
}
|
||||
activeRow = {
|
||||
x: 0,
|
||||
y: activePage.currentRow.y + activePage.currentRow.height,
|
||||
height: rasterizedGlyph.size.y
|
||||
};
|
||||
activePage.fixedRows.push(activeRow);
|
||||
|
||||
// Create the new current row below the new fixed height row
|
||||
activePage.currentRow = {
|
||||
x: 0,
|
||||
y: activeRow.y + activeRow.height,
|
||||
height: 0
|
||||
};
|
||||
}
|
||||
// TODO: Remove pages from _activePages when all rows are filled
|
||||
}
|
||||
|
||||
// Exit the loop if there is enough room in the row
|
||||
if (activeRow.x + rasterizedGlyph.size.x <= activePage.canvas.width) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If there is not enough room in the current row, finish it and try again
|
||||
if (activeRow === activePage.currentRow) {
|
||||
activeRow.x = 0;
|
||||
activeRow.y += activeRow.height;
|
||||
activeRow.height = 0;
|
||||
} else {
|
||||
this._fixedRows.splice(this._fixedRows.indexOf(activeRow), 1);
|
||||
activePage.fixedRows.splice(activePage.fixedRows.indexOf(activeRow), 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Record texture position
|
||||
rasterizedGlyph.texturePage = this._pages.indexOf(activePage);
|
||||
rasterizedGlyph.texturePosition.x = activeRow.x;
|
||||
rasterizedGlyph.texturePosition.y = activeRow.y;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / TEXTURE_WIDTH;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / TEXTURE_HEIGHT;
|
||||
rasterizedGlyph.texturePositionClipSpace.x = activeRow.x / activePage.canvas.width;
|
||||
rasterizedGlyph.texturePositionClipSpace.y = activeRow.y / activePage.canvas.height;
|
||||
|
||||
// Fix the clipspace position as pages may be of differing size
|
||||
rasterizedGlyph.sizeClipSpace.x /= activePage.canvas.width;
|
||||
rasterizedGlyph.sizeClipSpace.y /= activePage.canvas.height;
|
||||
|
||||
// Update atlas current row, for fixed rows the glyph height will never be larger than the row
|
||||
// height
|
||||
@@ -674,7 +816,17 @@ export class WebglCharAtlas implements IDisposable {
|
||||
activeRow.x += rasterizedGlyph.size.x;
|
||||
|
||||
// putImageData doesn't do any blending, so it will overwrite any existing cache entry for us
|
||||
this._cacheCtx.putImageData(clippedImageData, rasterizedGlyph.texturePosition.x, rasterizedGlyph.texturePosition.y);
|
||||
activePage.ctx.putImageData(
|
||||
imageData,
|
||||
rasterizedGlyph.texturePosition.x - this._workBoundingBox.left,
|
||||
rasterizedGlyph.texturePosition.y - this._workBoundingBox.top,
|
||||
this._workBoundingBox.left,
|
||||
this._workBoundingBox.top,
|
||||
rasterizedGlyph.size.x,
|
||||
rasterizedGlyph.size.y
|
||||
);
|
||||
activePage.addGlyph(rasterizedGlyph);
|
||||
activePage.version++;
|
||||
|
||||
return rasterizedGlyph;
|
||||
}
|
||||
@@ -688,8 +840,8 @@ export class WebglCharAtlas implements IDisposable {
|
||||
*/
|
||||
private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, allowedWidth: number, restrictedGlyph: boolean, customGlyph: boolean, padding: number): IRasterizedGlyph {
|
||||
boundingBox.top = 0;
|
||||
const height = restrictedGlyph ? this._config.scaledCellHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.scaledCellWidth : allowedWidth;
|
||||
const height = restrictedGlyph ? this._config.deviceCellHeight : this._tmpCanvas.height;
|
||||
const width = restrictedGlyph ? this._config.deviceCellWidth : allowedWidth;
|
||||
let found = false;
|
||||
for (let y = 0; y < height; y++) {
|
||||
for (let x = 0; x < width; x++) {
|
||||
@@ -706,7 +858,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
boundingBox.left = 0;
|
||||
found = false;
|
||||
for (let x = 0; x < width; x++) {
|
||||
for (let x = 0; x < padding + width; x++) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
@@ -721,7 +873,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
boundingBox.right = width;
|
||||
found = false;
|
||||
for (let x = width - 1; x >= 0; x--) {
|
||||
for (let x = padding + width - 1; x >= padding; x--) {
|
||||
for (let y = 0; y < height; y++) {
|
||||
const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3;
|
||||
if (imageData.data[alphaOffset] !== 0) {
|
||||
@@ -750,6 +902,7 @@ export class WebglCharAtlas implements IDisposable {
|
||||
}
|
||||
}
|
||||
return {
|
||||
texturePage: 0,
|
||||
texturePosition: { x: 0, y: 0 },
|
||||
texturePositionClipSpace: { x: 0, y: 0 },
|
||||
size: {
|
||||
@@ -757,31 +910,78 @@ export class WebglCharAtlas implements IDisposable {
|
||||
y: boundingBox.bottom - boundingBox.top + 1
|
||||
},
|
||||
sizeClipSpace: {
|
||||
x: (boundingBox.right - boundingBox.left + 1) / TEXTURE_WIDTH,
|
||||
y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT
|
||||
x: (boundingBox.right - boundingBox.left + 1),
|
||||
y: (boundingBox.bottom - boundingBox.top + 1)
|
||||
},
|
||||
offset: {
|
||||
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.scaledCellWidth - this._config.scaledCharWidth) / 2) : 0),
|
||||
y: -boundingBox.top + padding + ((restrictedGlyph || customGlyph) ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.scaledCellHeight - this._config.scaledCharHeight) / 2) : 0)
|
||||
x: -boundingBox.left + padding + ((restrictedGlyph || customGlyph) ? Math.floor((this._config.deviceCellWidth - this._config.deviceCharWidth) / 2) : 0),
|
||||
y: -boundingBox.top + padding + ((restrictedGlyph || customGlyph) ? this._config.lineHeight === 1 ? 0 : Math.round((this._config.deviceCellHeight - this._config.deviceCharHeight) / 2) : 0)
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private _clipImageData(imageData: ImageData, boundingBox: IBoundingBox): ImageData {
|
||||
const width = boundingBox.right - boundingBox.left + 1;
|
||||
const height = boundingBox.bottom - boundingBox.top + 1;
|
||||
const clippedData = new Uint8ClampedArray(width * height * 4);
|
||||
for (let y = boundingBox.top; y <= boundingBox.bottom; y++) {
|
||||
for (let x = boundingBox.left; x <= boundingBox.right; x++) {
|
||||
const oldOffset = y * this._tmpCanvas.width * 4 + x * 4;
|
||||
const newOffset = (y - boundingBox.top) * width * 4 + (x - boundingBox.left) * 4;
|
||||
clippedData[newOffset] = imageData.data[oldOffset];
|
||||
clippedData[newOffset + 1] = imageData.data[oldOffset + 1];
|
||||
clippedData[newOffset + 2] = imageData.data[oldOffset + 2];
|
||||
clippedData[newOffset + 3] = imageData.data[oldOffset + 3];
|
||||
class AtlasPage {
|
||||
public readonly canvas: HTMLCanvasElement;
|
||||
public readonly ctx: CanvasRenderingContext2D;
|
||||
|
||||
private _usedPixels: number = 0;
|
||||
public get percentageUsed(): number { return this._usedPixels / (this.canvas.width * this.canvas.height); }
|
||||
|
||||
private readonly _glyphs: IRasterizedGlyph[] = [];
|
||||
public get glyphs(): ReadonlyArray<IRasterizedGlyph> { return this._glyphs; }
|
||||
public addGlyph(glyph: IRasterizedGlyph): void {
|
||||
this._glyphs.push(glyph);
|
||||
this._usedPixels += glyph.size.x * glyph.size.y;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to check whether the canvas of the atlas page has changed.
|
||||
*/
|
||||
public version = 0;
|
||||
|
||||
// Texture atlas current positioning data. The texture packing strategy used is to fill from
|
||||
// left-to-right and top-to-bottom. When the glyph being written is less than half of the current
|
||||
// row's height, the following happens:
|
||||
//
|
||||
// - The current row becomes the fixed height row A
|
||||
// - A new fixed height row B the exact size of the glyph is created below the current row
|
||||
// - A new dynamic height current row is created below B
|
||||
//
|
||||
// This strategy does a good job preventing space being wasted for very short glyphs such as
|
||||
// underscores, hyphens etc. or those with underlines rendered.
|
||||
public currentRow: ICharAtlasActiveRow = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
height: 0
|
||||
};
|
||||
public readonly fixedRows: ICharAtlasActiveRow[] = [];
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
size: number,
|
||||
sourcePages?: AtlasPage[]
|
||||
) {
|
||||
if (sourcePages) {
|
||||
for (const p of sourcePages) {
|
||||
this._glyphs.push(...p.glyphs);
|
||||
this._usedPixels += p._usedPixels;
|
||||
}
|
||||
}
|
||||
return new ImageData(clippedData, width, height);
|
||||
this.canvas = createCanvas(document, size, size);
|
||||
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
|
||||
// It might also contain some characters with transparent backgrounds if allowTransparency is
|
||||
// set.
|
||||
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||
this.currentRow.x = 0;
|
||||
this.currentRow.y = 0;
|
||||
this.currentRow.height = 0;
|
||||
this.fixedRows.length = 0;
|
||||
this.version++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -839,3 +1039,10 @@ function checkCompletelyTransparent(imageData: ImageData): boolean {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function createCanvas(document: Document, width: number, height: number): HTMLCanvasElement {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
return canvas;
|
||||
}
|
||||
+173
@@ -0,0 +1,173 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { FontWeight, Terminal } from 'xterm';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
|
||||
export interface ICharAtlasConfig {
|
||||
customGlyphs: boolean;
|
||||
devicePixelRatio: number;
|
||||
letterSpacing: number;
|
||||
lineHeight: number;
|
||||
fontSize: number;
|
||||
fontFamily: string;
|
||||
fontWeight: FontWeight;
|
||||
fontWeightBold: FontWeight;
|
||||
deviceCellWidth: number;
|
||||
deviceCellHeight: number;
|
||||
deviceCharWidth: number;
|
||||
deviceCharHeight: number;
|
||||
allowTransparency: boolean;
|
||||
drawBoldTextInBrightColors: boolean;
|
||||
minimumContrastRatio: number;
|
||||
colors: IColorSet;
|
||||
}
|
||||
|
||||
export interface IDimensions {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface IOffset {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
export interface IRenderDimensions {
|
||||
/**
|
||||
* Dimensions measured in CSS pixels (ie. device pixels / device pixel ratio).
|
||||
*/
|
||||
css: {
|
||||
canvas: IDimensions;
|
||||
cell: IDimensions;
|
||||
};
|
||||
/**
|
||||
* Dimensions measured in actual pixels as rendered to the device.
|
||||
*/
|
||||
device: {
|
||||
canvas: IDimensions;
|
||||
cell: IDimensions;
|
||||
char: IDimensions & IOffset;
|
||||
};
|
||||
}
|
||||
|
||||
export interface IRequestRedrawEvent {
|
||||
start: number;
|
||||
end: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Note that IRenderer implementations should emit the refresh event after
|
||||
* rendering rows to the screen.
|
||||
*/
|
||||
export interface IRenderer extends IDisposable {
|
||||
readonly dimensions: IRenderDimensions;
|
||||
|
||||
/**
|
||||
* Fires when the renderer is requesting to be redrawn on the next animation
|
||||
* frame but is _not_ a result of content changing (eg. selection changes).
|
||||
*/
|
||||
readonly onRequestRedraw: IEvent<IRequestRedrawEvent>;
|
||||
|
||||
dispose(): void;
|
||||
handleDevicePixelRatioChange(): void;
|
||||
handleResize(cols: number, rows: number): void;
|
||||
handleCharSizeChanged(): void;
|
||||
handleBlur(): void;
|
||||
handleFocus(): void;
|
||||
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleCursorMove(): void;
|
||||
clear(): void;
|
||||
renderRows(start: number, end: number): void;
|
||||
clearTextureAtlas?(): void;
|
||||
}
|
||||
|
||||
export interface ITextureAtlas extends IDisposable {
|
||||
readonly pages: { canvas: HTMLCanvasElement, version: number }[];
|
||||
|
||||
onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* Warm up the texture atlas, adding common glyphs to avoid slowing early frame.
|
||||
*/
|
||||
warmUp(): void;
|
||||
|
||||
/**
|
||||
* Call when a frame is being drawn, this will return true if the atlas was cleared to make room
|
||||
* for a new set of glyphs.
|
||||
*/
|
||||
beginFrame(): boolean;
|
||||
|
||||
/**
|
||||
* Clear all glyphs from the texture atlas.
|
||||
*/
|
||||
clearTexture(): void;
|
||||
getRasterizedGlyph(code: number, bg: number, fg: number, ext: number): IRasterizedGlyph;
|
||||
getRasterizedGlyphCombinedChar(chars: string, bg: number, fg: number, ext: number): IRasterizedGlyph;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a rasterized glyph within a texture atlas. Some numbers are
|
||||
* tracked in CSS pixels as well in order to reduce calculations during the
|
||||
* render loop.
|
||||
*/
|
||||
export interface IRasterizedGlyph {
|
||||
/**
|
||||
* The x and y offset between the glyph's top/left and the top/left of a cell
|
||||
* in pixels.
|
||||
*/
|
||||
offset: IVector;
|
||||
/**
|
||||
* The index of the texture page that the glyph is on.
|
||||
*/
|
||||
texturePage: number;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in pixels.
|
||||
*/
|
||||
texturePosition: IVector;
|
||||
/**
|
||||
* the x and y position of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
texturePositionClipSpace: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in pixels.
|
||||
*/
|
||||
size: IVector;
|
||||
/**
|
||||
* The width and height of the glyph in the texture in clip space coordinates.
|
||||
*/
|
||||
sizeClipSpace: IVector;
|
||||
}
|
||||
|
||||
export interface IVector {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface IBoundingBox {
|
||||
top: number;
|
||||
left: number;
|
||||
right: number;
|
||||
bottom: number;
|
||||
}
|
||||
|
||||
export interface ISelectionRenderModel {
|
||||
readonly hasSelection: boolean;
|
||||
readonly columnSelectMode: boolean;
|
||||
readonly viewportStartRow: number;
|
||||
readonly viewportEndRow: number;
|
||||
readonly viewportCappedStartRow: number;
|
||||
readonly viewportCappedEndRow: number;
|
||||
readonly startCol: number;
|
||||
readonly endCol: number;
|
||||
readonly selectionStart: [number, number] | undefined;
|
||||
readonly selectionEnd: [number, number] | undefined;
|
||||
clear(): void;
|
||||
update(terminal: Terminal, start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode?: boolean): void;
|
||||
isCellSelected(terminal: Terminal, x: number, y: number): boolean;
|
||||
}
|
||||
@@ -50,17 +50,17 @@ describe('SelectionModel', () => {
|
||||
it('should trim a portion of the selection when a part of it is trimmed', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 2];
|
||||
model.onTrim(1);
|
||||
model.handleTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
|
||||
model.onTrim(1);
|
||||
model.handleTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, [0, 0]);
|
||||
assert.deepEqual(model.finalSelectionEnd, [10, 0]);
|
||||
});
|
||||
it('should clear selection when it is trimmed in its entirety', () => {
|
||||
model.selectionStart = [0, 0];
|
||||
model.selectionEnd = [10, 0];
|
||||
model.onTrim(1);
|
||||
model.handleTrim(1);
|
||||
assert.deepEqual(model.finalSelectionStart, undefined);
|
||||
assert.deepEqual(model.finalSelectionEnd, undefined);
|
||||
});
|
||||
|
||||
@@ -118,9 +118,9 @@ export class SelectionModel {
|
||||
/**
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
* @return Whether a refresh is necessary.
|
||||
* @returns Whether a refresh is necessary.
|
||||
*/
|
||||
public onTrim(amount: number): boolean {
|
||||
public handleTrim(amount: number): boolean {
|
||||
// Adjust the selection position based on the trimmed amount.
|
||||
if (this.selectionStart) {
|
||||
this.selectionStart[1] -= amount;
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
|
||||
import { IOptionsService } from 'common/services/Services';
|
||||
import { IEvent, EventEmitter } from 'common/EventEmitter';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { ICharSizeService } from 'browser/services/Services';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ITerminalOptions } from 'common/Types';
|
||||
|
||||
export class CharSizeService implements ICharSizeService {
|
||||
export class CharSizeService extends Disposable implements ICharSizeService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
public width: number = 0;
|
||||
@@ -16,15 +18,17 @@ export class CharSizeService implements ICharSizeService {
|
||||
|
||||
public get hasValidSize(): boolean { return this.width > 0 && this.height > 0; }
|
||||
|
||||
private _onCharSizeChange = new EventEmitter<void>();
|
||||
public get onCharSizeChange(): IEvent<void> { return this._onCharSizeChange.event; }
|
||||
private readonly _onCharSizeChange = this.register(new EventEmitter<void>());
|
||||
public readonly onCharSizeChange = this._onCharSizeChange.event;
|
||||
|
||||
constructor(
|
||||
document: Document,
|
||||
parentElement: HTMLElement,
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService);
|
||||
this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure()));
|
||||
}
|
||||
|
||||
public measure(): void {
|
||||
|
||||
@@ -8,10 +8,15 @@ import { ICoreBrowserService } from './Services';
|
||||
export class CoreBrowserService implements ICoreBrowserService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _isFocused = false;
|
||||
private _cachedIsFocused: boolean | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
private _textarea: HTMLTextAreaElement,
|
||||
public readonly window: Window & typeof globalThis
|
||||
) {
|
||||
this._textarea.addEventListener('focus', () => this._isFocused = true);
|
||||
this._textarea.addEventListener('blur', () => this._isFocused = false);
|
||||
}
|
||||
|
||||
public get dpr(): number {
|
||||
@@ -19,7 +24,10 @@ export class CoreBrowserService implements ICoreBrowserService {
|
||||
}
|
||||
|
||||
public get isFocused(): boolean {
|
||||
const docOrShadowRoot = this._textarea.getRootNode ? this._textarea.getRootNode() as Document | ShadowRoot : this._textarea.ownerDocument;
|
||||
return docOrShadowRoot.activeElement === this._textarea && this._textarea.ownerDocument.hasFocus();
|
||||
if (this._cachedIsFocused === undefined) {
|
||||
this._cachedIsFocused = this._isFocused && this._textarea.ownerDocument.hasFocus();
|
||||
queueMicrotask(() => this._cachedIsFocused = undefined);
|
||||
}
|
||||
return this._cachedIsFocused;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,8 +23,8 @@ export class MouseService implements IMouseService {
|
||||
colCount,
|
||||
rowCount,
|
||||
this._charSizeService.hasValidSize,
|
||||
this._renderService.dimensions.actualCellWidth,
|
||||
this._renderService.dimensions.actualCellHeight,
|
||||
this._renderService.dimensions.css.cell.width,
|
||||
this._renderService.dimensions.css.cell.height,
|
||||
isSelection
|
||||
);
|
||||
}
|
||||
@@ -37,14 +37,14 @@ export class MouseService implements IMouseService {
|
||||
if (!this._charSizeService.hasValidSize
|
||||
|| coords[0] < 0
|
||||
|| coords[1] < 0
|
||||
|| coords[0] >= this._renderService.dimensions.canvasWidth
|
||||
|| coords[1] >= this._renderService.dimensions.canvasHeight) {
|
||||
|| coords[0] >= this._renderService.dimensions.css.canvas.width
|
||||
|| coords[1] >= this._renderService.dimensions.css.canvas.height) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.actualCellWidth),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.actualCellHeight),
|
||||
col: Math.floor(coords[0] / this._renderService.dimensions.css.cell.width),
|
||||
row: Math.floor(coords[1] / this._renderService.dimensions.css.cell.height),
|
||||
x: Math.floor(coords[0]),
|
||||
y: Math.floor(coords[1])
|
||||
};
|
||||
|
||||
@@ -3,15 +3,17 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { IRenderer, IRenderDimensions } from 'browser/renderer/Types';
|
||||
import { IRenderer, IRenderDimensions } from 'browser/renderer/shared/Types';
|
||||
import { RenderDebouncer } from 'browser/RenderDebouncer';
|
||||
import { EventEmitter, IEvent } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { ScreenDprMonitor } from 'browser/ScreenDprMonitor';
|
||||
import { addDisposableDomListener } from 'browser/Lifecycle';
|
||||
import { IColorSet, IRenderDebouncerWithCallback } from 'browser/Types';
|
||||
import { IColorSet, IRenderDebouncerWithCallback, ReadonlyColorSet } from 'browser/Types';
|
||||
import { IOptionsService, IBufferService, IDecorationService } from 'common/services/Services';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService } from 'browser/services/Services';
|
||||
import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services';
|
||||
import { DebouncedIdleTask } from 'common/TaskQueue';
|
||||
import { ThemeService } from 'browser/services/ThemeService';
|
||||
|
||||
interface ISelectionState {
|
||||
start: [number, number] | undefined;
|
||||
@@ -22,8 +24,10 @@ interface ISelectionState {
|
||||
export class RenderService extends Disposable implements IRenderService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _renderer: IRenderer | undefined;
|
||||
private _renderDebouncer: IRenderDebouncerWithCallback;
|
||||
private _screenDprMonitor: ScreenDprMonitor;
|
||||
private _pausedResizeTask = new DebouncedIdleTask();
|
||||
|
||||
private _isPaused: boolean = false;
|
||||
private _needsFullRefresh: boolean = false;
|
||||
@@ -37,42 +41,42 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
columnSelectMode: false
|
||||
};
|
||||
|
||||
private _onDimensionsChange = new EventEmitter<IRenderDimensions>();
|
||||
public get onDimensionsChange(): IEvent<IRenderDimensions> { return this._onDimensionsChange.event; }
|
||||
private _onRenderedViewportChange = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRenderedViewportChange(): IEvent<{ start: number, end: number }> { return this._onRenderedViewportChange.event; }
|
||||
private _onRender = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRender(): IEvent<{ start: number, end: number }> { return this._onRender.event; }
|
||||
private _onRefreshRequest = new EventEmitter<{ start: number, end: number }>();
|
||||
public get onRefreshRequest(): IEvent<{ start: number, end: number }> { return this._onRefreshRequest.event; }
|
||||
private readonly _onDimensionsChange = this.register(new EventEmitter<IRenderDimensions>());
|
||||
public readonly onDimensionsChange = this._onDimensionsChange.event;
|
||||
private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>());
|
||||
public readonly onRenderedViewportChange = this._onRenderedViewportChange.event;
|
||||
private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>());
|
||||
public readonly onRender = this._onRender.event;
|
||||
private readonly _onRefreshRequest = this.register(new EventEmitter<{ start: number, end: number }>());
|
||||
public readonly onRefreshRequest = this._onRefreshRequest.event;
|
||||
|
||||
public get dimensions(): IRenderDimensions { return this._renderer.dimensions; }
|
||||
public get dimensions(): IRenderDimensions { return this._renderer!.dimensions; }
|
||||
|
||||
constructor(
|
||||
private _renderer: IRenderer,
|
||||
private _rowCount: number,
|
||||
screenElement: HTMLElement,
|
||||
@IOptionsService optionsService: IOptionsService,
|
||||
@ICharSizeService private readonly _charSizeService: ICharSizeService,
|
||||
@IDecorationService decorationService: IDecorationService,
|
||||
@IBufferService bufferService: IBufferService,
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService
|
||||
@ICoreBrowserService coreBrowserService: ICoreBrowserService,
|
||||
@IThemeService themeService: IThemeService
|
||||
) {
|
||||
super();
|
||||
|
||||
this.register({ dispose: () => this._renderer.dispose() });
|
||||
this.register({ dispose: () => this._renderer?.dispose() });
|
||||
|
||||
this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end));
|
||||
this.register(this._renderDebouncer);
|
||||
|
||||
this._screenDprMonitor = new ScreenDprMonitor(coreBrowserService.window);
|
||||
this._screenDprMonitor.setListener(() => this.onDevicePixelRatioChange());
|
||||
this._screenDprMonitor.setListener(() => this.handleDevicePixelRatioChange());
|
||||
this.register(this._screenDprMonitor);
|
||||
|
||||
this.register(bufferService.onResize(() => this._fullRefresh()));
|
||||
this.register(bufferService.buffers.onBufferActivate(() => this._renderer?.clear()));
|
||||
this.register(optionsService.onOptionChange(() => this._handleOptionsChanged()));
|
||||
this.register(this._charSizeService.onCharSizeChange(() => this.onCharSizeChanged()));
|
||||
this.register(this._charSizeService.onCharSizeChange(() => this.handleCharSizeChanged()));
|
||||
|
||||
// Do a full refresh whenever any decoration is added or removed. This may not actually result
|
||||
// in changes but since decorations should be used sparingly or added/removed all in the same
|
||||
@@ -80,23 +84,45 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
this.register(decorationService.onDecorationRegistered(() => this._fullRefresh()));
|
||||
this.register(decorationService.onDecorationRemoved(() => this._fullRefresh()));
|
||||
|
||||
// No need to register this as renderer is explicitly disposed in RenderService.dispose
|
||||
this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
|
||||
// Clear the renderer when the a change that could affect glyphs occurs
|
||||
this.register(optionsService.onMultipleOptionChange([
|
||||
'customGlyphs',
|
||||
'drawBoldTextInBrightColors',
|
||||
'letterSpacing',
|
||||
'lineHeight',
|
||||
'fontFamily',
|
||||
'fontSize',
|
||||
'fontWeight',
|
||||
'fontWeightBold',
|
||||
'minimumContrastRatio'
|
||||
], () => {
|
||||
this.clear();
|
||||
this.handleResize(bufferService.cols, bufferService.rows);
|
||||
this._fullRefresh();
|
||||
}));
|
||||
|
||||
// Refresh the cursor line when the cursor changes
|
||||
this.register(optionsService.onMultipleOptionChange([
|
||||
'cursorBlink',
|
||||
'cursorStyle'
|
||||
], () => this.refreshRows(bufferService.buffer.y, bufferService.buffer.y, true)));
|
||||
|
||||
// dprchange should handle this case, we need this as well for browsers that don't support the
|
||||
// matchMedia query.
|
||||
this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.onDevicePixelRatioChange()));
|
||||
this.register(addDisposableDomListener(coreBrowserService.window, 'resize', () => this.handleDevicePixelRatioChange()));
|
||||
|
||||
this.register(themeService.onChangeColors(() => this._fullRefresh()));
|
||||
|
||||
// Detect whether IntersectionObserver is detected and enable renderer pause
|
||||
// and resume based on terminal visibility if so
|
||||
if ('IntersectionObserver' in coreBrowserService.window) {
|
||||
const observer = new coreBrowserService.window.IntersectionObserver(e => this._onIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 });
|
||||
observer.observe(screenElement);
|
||||
this.register({ dispose: () => observer.disconnect() });
|
||||
}
|
||||
}
|
||||
|
||||
private _onIntersectionChange(entry: IntersectionObserverEntry): void {
|
||||
private _handleIntersectionChange(entry: IntersectionObserverEntry): void {
|
||||
this._isPaused = entry.isIntersecting === undefined ? (entry.intersectionRatio === 0) : !entry.isIntersecting;
|
||||
|
||||
// Terminal was hidden on open
|
||||
@@ -105,6 +131,7 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
}
|
||||
|
||||
if (!this._isPaused && this._needsFullRefresh) {
|
||||
this._pausedResizeTask.flush();
|
||||
this.refreshRows(0, this._rowCount - 1);
|
||||
this._needsFullRefresh = false;
|
||||
}
|
||||
@@ -122,11 +149,14 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
}
|
||||
|
||||
private _renderRows(start: number, end: number): void {
|
||||
if (!this._renderer) {
|
||||
return;
|
||||
}
|
||||
this._renderer.renderRows(start, end);
|
||||
|
||||
// Update selection if needed
|
||||
if (this._needsSelectionRefresh) {
|
||||
this._renderer.onSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode);
|
||||
this._renderer.handleSelectionChanged(this._selectionState.start, this._selectionState.end, this._selectionState.columnSelectMode);
|
||||
this._needsSelectionRefresh = false;
|
||||
}
|
||||
|
||||
@@ -144,26 +174,31 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
}
|
||||
|
||||
private _handleOptionsChanged(): void {
|
||||
this._renderer.onOptionsChanged();
|
||||
if (!this._renderer) {
|
||||
return;
|
||||
}
|
||||
this.refreshRows(0, this._rowCount - 1);
|
||||
this._fireOnCanvasResize();
|
||||
}
|
||||
|
||||
private _fireOnCanvasResize(): void {
|
||||
if (!this._renderer) {
|
||||
return;
|
||||
}
|
||||
// Don't fire the event if the dimensions haven't changed
|
||||
if (this._renderer.dimensions.canvasWidth === this._canvasWidth && this._renderer.dimensions.canvasHeight === this._canvasHeight) {
|
||||
if (this._renderer.dimensions.css.canvas.width === this._canvasWidth && this._renderer.dimensions.css.canvas.height === this._canvasHeight) {
|
||||
return;
|
||||
}
|
||||
this._onDimensionsChange.fire(this._renderer.dimensions);
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
super.dispose();
|
||||
public hasRenderer(): boolean {
|
||||
return !!this._renderer;
|
||||
}
|
||||
|
||||
public setRenderer(renderer: IRenderer): void {
|
||||
// TODO: RenderService should be the only one to dispose the renderer
|
||||
this._renderer.dispose();
|
||||
this._renderer?.dispose();
|
||||
this._renderer = renderer;
|
||||
this._renderer.onRequestRedraw(e => this.refreshRows(e.start, e.end, true));
|
||||
|
||||
@@ -185,54 +220,62 @@ export class RenderService extends Disposable implements IRenderService {
|
||||
}
|
||||
|
||||
public clearTextureAtlas(): void {
|
||||
this._renderer?.clearTextureAtlas?.();
|
||||
if (!this._renderer) {
|
||||
return;
|
||||
}
|
||||
this._renderer.clearTextureAtlas?.();
|
||||
this._fullRefresh();
|
||||
}
|
||||
|
||||
public setColors(colors: IColorSet): void {
|
||||
this._renderer.setColors(colors);
|
||||
this._fullRefresh();
|
||||
}
|
||||
|
||||
public onDevicePixelRatioChange(): void {
|
||||
public handleDevicePixelRatioChange(): void {
|
||||
// Force char size measurement as DomMeasureStrategy(getBoundingClientRect) is not stable
|
||||
// when devicePixelRatio changes
|
||||
this._charSizeService.measure();
|
||||
|
||||
this._renderer.onDevicePixelRatioChange();
|
||||
if (!this._renderer) {
|
||||
return;
|
||||
}
|
||||
this._renderer.handleDevicePixelRatioChange();
|
||||
this.refreshRows(0, this._rowCount - 1);
|
||||
}
|
||||
|
||||
public onResize(cols: number, rows: number): void {
|
||||
this._renderer.onResize(cols, rows);
|
||||
public handleResize(cols: number, rows: number): void {
|
||||
if (!this._renderer) {
|
||||
return;
|
||||
}
|
||||
if (this._isPaused) {
|
||||
this._pausedResizeTask.set(() => this._renderer!.handleResize(cols, rows));
|
||||
} else {
|
||||
this._renderer.handleResize(cols, rows);
|
||||
}
|
||||
this._fullRefresh();
|
||||
}
|
||||
|
||||
// TODO: Is this useful when we have onResize?
|
||||
public onCharSizeChanged(): void {
|
||||
this._renderer.onCharSizeChanged();
|
||||
public handleCharSizeChanged(): void {
|
||||
this._renderer?.handleCharSizeChanged();
|
||||
}
|
||||
|
||||
public onBlur(): void {
|
||||
this._renderer.onBlur();
|
||||
public handleBlur(): void {
|
||||
this._renderer?.handleBlur();
|
||||
}
|
||||
|
||||
public onFocus(): void {
|
||||
this._renderer.onFocus();
|
||||
public handleFocus(): void {
|
||||
this._renderer?.handleFocus();
|
||||
}
|
||||
|
||||
public onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
public handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void {
|
||||
this._selectionState.start = start;
|
||||
this._selectionState.end = end;
|
||||
this._selectionState.columnSelectMode = columnSelectMode;
|
||||
this._renderer.onSelectionChanged(start, end, columnSelectMode);
|
||||
this._renderer?.handleSelectionChanged(start, end, columnSelectMode);
|
||||
}
|
||||
|
||||
public onCursorMove(): void {
|
||||
this._renderer.onCursorMove();
|
||||
public handleCursorMove(): void {
|
||||
this._renderer?.handleCursorMove();
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._renderer.clear();
|
||||
this._renderer?.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,8 +49,8 @@ describe('SelectionService', () => {
|
||||
bufferService = new MockBufferService(20, 20, optionsService);
|
||||
buffer = bufferService.buffer;
|
||||
const renderService = new MockRenderService();
|
||||
renderService.dimensions.canvasHeight = 10 * 20;
|
||||
renderService.dimensions.canvasWidth = 10 * 20;
|
||||
renderService.dimensions.css.canvas.height = 10 * 20;
|
||||
renderService.dimensions.css.canvas.width = 10 * 20;
|
||||
selectionService = new TestSelectionService(bufferService, optionsService, renderService);
|
||||
});
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import { IBufferRange, ILinkifier2 } from 'browser/Types';
|
||||
import { IBufferService, IOptionsService, ICoreService } from 'common/services/Services';
|
||||
import { getCoordsRelativeToElement } from 'browser/input/Mouse';
|
||||
import { moveToCellSequence } from 'browser/input/MoveToCell';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { Disposable, toDisposable } from 'common/Lifecycle';
|
||||
import { getRangeLength } from 'common/buffer/BufferRange';
|
||||
|
||||
/**
|
||||
@@ -111,14 +111,14 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
private _oldSelectionStart: [number, number] | undefined = undefined;
|
||||
private _oldSelectionEnd: [number, number] | undefined = undefined;
|
||||
|
||||
private _onLinuxMouseSelection = this.register(new EventEmitter<string>());
|
||||
public get onLinuxMouseSelection(): IEvent<string> { return this._onLinuxMouseSelection.event; }
|
||||
private _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
|
||||
public get onRequestRedraw(): IEvent<ISelectionRedrawRequestEvent> { return this._onRedrawRequest.event; }
|
||||
private _onSelectionChange = this.register(new EventEmitter<void>());
|
||||
public get onSelectionChange(): IEvent<void> { return this._onSelectionChange.event; }
|
||||
private _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
|
||||
public get onRequestScrollLines(): IEvent<ISelectionRequestScrollLinesEvent> { return this._onRequestScrollLines.event; }
|
||||
private readonly _onLinuxMouseSelection = this.register(new EventEmitter<string>());
|
||||
public readonly onLinuxMouseSelection = this._onLinuxMouseSelection.event;
|
||||
private readonly _onRedrawRequest = this.register(new EventEmitter<ISelectionRedrawRequestEvent>());
|
||||
public readonly onRequestRedraw = this._onRedrawRequest.event;
|
||||
private readonly _onSelectionChange = this.register(new EventEmitter<void>());
|
||||
public readonly onSelectionChange = this._onSelectionChange.event;
|
||||
private readonly _onRequestScrollLines = this.register(new EventEmitter<ISelectionRequestScrollLinesEvent>());
|
||||
public readonly onRequestScrollLines = this._onRequestScrollLines.event;
|
||||
|
||||
constructor(
|
||||
private readonly _element: HTMLElement,
|
||||
@@ -134,24 +134,24 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
super();
|
||||
|
||||
// Init listeners
|
||||
this._mouseMoveListener = event => this._onMouseMove(event as MouseEvent);
|
||||
this._mouseUpListener = event => this._onMouseUp(event as MouseEvent);
|
||||
this._mouseMoveListener = event => this._handleMouseMove(event as MouseEvent);
|
||||
this._mouseUpListener = event => this._handleMouseUp(event as MouseEvent);
|
||||
this._coreService.onUserInput(() => {
|
||||
if (this.hasSelection) {
|
||||
this.clearSelection();
|
||||
}
|
||||
});
|
||||
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
this.register(this._bufferService.buffers.onBufferActivate(e => this._onBufferActivate(e)));
|
||||
this._trimListener = this._bufferService.buffer.lines.onTrim(amount => this._handleTrim(amount));
|
||||
this.register(this._bufferService.buffers.onBufferActivate(e => this._handleBufferActivate(e)));
|
||||
|
||||
this.enable();
|
||||
|
||||
this._model = new SelectionModel(this._bufferService);
|
||||
this._activeSelectionMode = SelectionMode.NORMAL;
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
this._removeMouseDownListeners();
|
||||
this.register(toDisposable(() => {
|
||||
this._removeMouseDownListeners();
|
||||
}));
|
||||
}
|
||||
|
||||
public reset(): void {
|
||||
@@ -375,8 +375,8 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* Handle the buffer being trimmed, adjust the selection position.
|
||||
* @param amount The amount the buffer is being trimmed.
|
||||
*/
|
||||
private _onTrim(amount: number): void {
|
||||
const needsRefresh = this._model.onTrim(amount);
|
||||
private _handleTrim(amount: number): void {
|
||||
const needsRefresh = this._model.handleTrim(amount);
|
||||
if (needsRefresh) {
|
||||
this.refresh();
|
||||
}
|
||||
@@ -408,7 +408,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
*/
|
||||
private _getMouseEventScrollAmount(event: MouseEvent): number {
|
||||
let offset = getCoordsRelativeToElement(this._coreBrowserService.window, event, this._screenElement)[1];
|
||||
const terminalHeight = this._renderService.dimensions.canvasHeight;
|
||||
const terminalHeight = this._renderService.dimensions.css.canvas.height;
|
||||
if (offset >= 0 && offset <= terminalHeight) {
|
||||
return 0;
|
||||
}
|
||||
@@ -438,7 +438,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* Handles te mousedown event, setting up for a new selection.
|
||||
* @param event The mousedown event.
|
||||
*/
|
||||
public onMouseDown(event: MouseEvent): void {
|
||||
public handleMouseDown(event: MouseEvent): void {
|
||||
this._mouseDownTimeStamp = event.timeStamp;
|
||||
// If we have selection, we want the context menu on right click even if the
|
||||
// terminal is in mouse mode.
|
||||
@@ -468,14 +468,14 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._dragScrollAmount = 0;
|
||||
|
||||
if (this._enabled && event.shiftKey) {
|
||||
this._onIncrementalClick(event);
|
||||
this._handleIncrementalClick(event);
|
||||
} else {
|
||||
if (event.detail === 1) {
|
||||
this._onSingleClick(event);
|
||||
this._handleSingleClick(event);
|
||||
} else if (event.detail === 2) {
|
||||
this._onDoubleClick(event);
|
||||
this._handleDoubleClick(event);
|
||||
} else if (event.detail === 3) {
|
||||
this._onTripleClick(event);
|
||||
this._handleTripleClick(event);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,7 +512,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* position.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onIncrementalClick(event: MouseEvent): void {
|
||||
private _handleIncrementalClick(event: MouseEvent): void {
|
||||
if (this._model.selectionStart) {
|
||||
this._model.selectionEnd = this._getMouseBufferCoords(event);
|
||||
}
|
||||
@@ -523,7 +523,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* start position.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onSingleClick(event: MouseEvent): void {
|
||||
private _handleSingleClick(event: MouseEvent): void {
|
||||
this._model.selectionStartLength = 0;
|
||||
this._model.isSelectAllActive = false;
|
||||
this._activeSelectionMode = this.shouldColumnSelect(event) ? SelectionMode.COLUMN : SelectionMode.NORMAL;
|
||||
@@ -557,7 +557,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* Performs a double click, selecting the current word.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onDoubleClick(event: MouseEvent): void {
|
||||
private _handleDoubleClick(event: MouseEvent): void {
|
||||
if (this._selectWordAtCursor(event, true)) {
|
||||
this._activeSelectionMode = SelectionMode.WORD;
|
||||
}
|
||||
@@ -568,7 +568,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* select mode.
|
||||
* @param event The mouse event.
|
||||
*/
|
||||
private _onTripleClick(event: MouseEvent): void {
|
||||
private _handleTripleClick(event: MouseEvent): void {
|
||||
const coords = this._getMouseBufferCoords(event);
|
||||
if (coords) {
|
||||
this._activeSelectionMode = SelectionMode.LINE;
|
||||
@@ -589,7 +589,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* end of the selection and refreshing the selection.
|
||||
* @param event The mousemove event.
|
||||
*/
|
||||
private _onMouseMove(event: MouseEvent): void {
|
||||
private _handleMouseMove(event: MouseEvent): void {
|
||||
// If the mousemove listener is active it means that a selection is
|
||||
// currently being made, we should stop propagation to prevent mouse events
|
||||
// to be sent to the pty.
|
||||
@@ -690,7 +690,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
* Handles the mouseup event, removing the mousedown listeners.
|
||||
* @param event The mouseup event.
|
||||
*/
|
||||
private _onMouseUp(event: MouseEvent): void {
|
||||
private _handleMouseUp(event: MouseEvent): void {
|
||||
const timeElapsed = event.timeStamp - this._mouseDownTimeStamp;
|
||||
|
||||
this._removeMouseDownListeners();
|
||||
@@ -746,30 +746,31 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
this._onSelectionChange.fire();
|
||||
}
|
||||
|
||||
private _onBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
|
||||
private _handleBufferActivate(e: {activeBuffer: IBuffer, inactiveBuffer: IBuffer}): void {
|
||||
this.clearSelection();
|
||||
// Only adjust the selection on trim, shiftElements is rarely used (only in
|
||||
// reverseIndex) and delete in a splice is only ever used when the same
|
||||
// number of elements was just added. Given this is could actually be
|
||||
// beneficial to leave the selection as is for these cases.
|
||||
this._trimListener.dispose();
|
||||
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._onTrim(amount));
|
||||
this._trimListener = e.activeBuffer.lines.onTrim(amount => this._handleTrim(amount));
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a viewport column to the character index on the buffer line, the
|
||||
* latter takes into account wide characters.
|
||||
* @param coords The coordinates to find the 2 index for.
|
||||
* Converts a viewport column (0 to cols - 1) to the character index on the
|
||||
* buffer line, the latter takes into account wide and null characters.
|
||||
* @param bufferLine The buffer line to use.
|
||||
* @param x The x index in the buffer line to convert.
|
||||
*/
|
||||
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, coords: [number, number]): number {
|
||||
let charIndex = coords[0];
|
||||
for (let i = 0; coords[0] >= i; i++) {
|
||||
private _convertViewportColToCharacterIndex(bufferLine: IBufferLine, x: number): number {
|
||||
let charIndex = x;
|
||||
for (let i = 0; x >= i; i++) {
|
||||
const length = bufferLine.loadCell(i, this._workCell).getChars().length;
|
||||
if (this._workCell.getWidth() === 0) {
|
||||
// Wide characters aren't included in the line string so decrement the
|
||||
// index so the index is back on the wide character.
|
||||
charIndex--;
|
||||
} else if (length > 1 && coords[0] !== i) {
|
||||
} else if (length > 1 && x !== i) {
|
||||
// Emojis take up multiple characters, so adjust accordingly. For these
|
||||
// we don't want ot include the character at the column as we're
|
||||
// returning the start index in the string, not the end index.
|
||||
@@ -816,7 +817,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
const line = buffer.translateBufferLineToString(coords[1], false);
|
||||
|
||||
// Get actual index, taking into consideration wide characters
|
||||
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords);
|
||||
let startIndex = this._convertViewportColToCharacterIndex(bufferLine, coords[0]);
|
||||
let endIndex = startIndex;
|
||||
|
||||
// Record offset to be used later
|
||||
@@ -1000,7 +1001,7 @@ export class SelectionService extends Disposable implements ISelectionService {
|
||||
/**
|
||||
* Gets whether the character is considered a word separator by the select
|
||||
* word logic.
|
||||
* @param char The character to check.
|
||||
* @param cell The cell to check.
|
||||
*/
|
||||
private _isCharWordSeparator(cell: CellData): boolean {
|
||||
// Zero width characters are never separators as they are always to the
|
||||
|
||||
@@ -4,11 +4,12 @@
|
||||
*/
|
||||
|
||||
import { IEvent } from 'common/EventEmitter';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/Types';
|
||||
import { IColorSet } from 'browser/Types';
|
||||
import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types';
|
||||
import { IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types';
|
||||
import { createDecorator } from 'common/services/ServiceRegistry';
|
||||
import { IDisposable } from 'common/Types';
|
||||
import { ColorIndex, IDisposable } from 'common/Types';
|
||||
import { ITheme } from 'common/services/Services';
|
||||
|
||||
export const ICharSizeService = createDecorator<ICharSizeService>('CharSizeService');
|
||||
export interface ICharSizeService {
|
||||
@@ -71,16 +72,15 @@ export interface IRenderService extends IDisposable {
|
||||
refreshRows(start: number, end: number): void;
|
||||
clearTextureAtlas(): void;
|
||||
resize(cols: number, rows: number): void;
|
||||
hasRenderer(): boolean;
|
||||
setRenderer(renderer: IRenderer): void;
|
||||
setColors(colors: IColorSet): void;
|
||||
onDevicePixelRatioChange(): void;
|
||||
onResize(cols: number, rows: number): void;
|
||||
// TODO: Is this useful when we have onResize?
|
||||
onCharSizeChanged(): void;
|
||||
onBlur(): void;
|
||||
onFocus(): void;
|
||||
onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
onCursorMove(): void;
|
||||
handleDevicePixelRatioChange(): void;
|
||||
handleResize(cols: number, rows: number): void;
|
||||
handleCharSizeChanged(): void;
|
||||
handleBlur(): void;
|
||||
handleFocus(): void;
|
||||
handleSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void;
|
||||
handleCursorMove(): void;
|
||||
clear(): void;
|
||||
}
|
||||
|
||||
@@ -109,7 +109,7 @@ export interface ISelectionService {
|
||||
shouldColumnSelect(event: KeyboardEvent | MouseEvent): boolean;
|
||||
shouldForceSelection(event: MouseEvent): boolean;
|
||||
refresh(isLinuxMouseSelection?: boolean): void;
|
||||
onMouseDown(event: MouseEvent): void;
|
||||
handleMouseDown(event: MouseEvent): void;
|
||||
isCellInSelection(x: number, y: number): boolean;
|
||||
}
|
||||
|
||||
@@ -121,3 +121,19 @@ export interface ICharacterJoinerService {
|
||||
deregister(joinerId: number): boolean;
|
||||
getJoinedCharacters(row: number): [number, number][];
|
||||
}
|
||||
|
||||
export const IThemeService = createDecorator<IThemeService>('ThemeService');
|
||||
export interface IThemeService {
|
||||
serviceBrand: undefined;
|
||||
|
||||
readonly colors: ReadonlyColorSet;
|
||||
|
||||
readonly onChangeColors: IEvent<ReadonlyColorSet>;
|
||||
|
||||
restoreColor(slot?: ColorIndex): void;
|
||||
/**
|
||||
* Allows external modifying of colors in the theme, this is used instead of {@link colors} to
|
||||
* prevent accidental writes.
|
||||
*/
|
||||
modifyColors(callback: (colors: IColorSet) => void): void;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,367 @@
|
||||
/**
|
||||
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import jsdom = require('jsdom');
|
||||
import { assert } from 'chai';
|
||||
import { ThemeService, DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService';
|
||||
import { OptionsService } from 'common/services/OptionsService';
|
||||
|
||||
describe('ThemeService', () => {
|
||||
let themeService: ThemeService;
|
||||
let dom: jsdom.JSDOM;
|
||||
let window: jsdom.DOMWindow;
|
||||
let optionsService: OptionsService;
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new jsdom.JSDOM('');
|
||||
window = dom.window;
|
||||
(window as any).HTMLCanvasElement.prototype.getContext = () => ({
|
||||
createLinearGradient(): any {
|
||||
return null;
|
||||
},
|
||||
|
||||
fillRect(): void { },
|
||||
|
||||
getImageData(): any {
|
||||
return {data: [0, 0, 0, 0xFF]};
|
||||
}
|
||||
});
|
||||
optionsService = new OptionsService({});
|
||||
themeService = new ThemeService(optionsService);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should fill all colors with values', () => {
|
||||
for (const key of Object.keys(themeService.colors)) {
|
||||
if (key !== 'ansi' && key !== 'contrastCache' && key !== 'selectionForeground') {
|
||||
// A #rrggbb or rgba(...)
|
||||
assert.ok((themeService.colors as any)[key].css.length >= 7);
|
||||
}
|
||||
}
|
||||
assert.equal(themeService.colors.ansi.length, 256);
|
||||
});
|
||||
|
||||
it('should fill 240 colors with expected values', () => {
|
||||
assert.equal(themeService.colors.ansi[16].css, '#000000');
|
||||
assert.equal(themeService.colors.ansi[17].css, '#00005f');
|
||||
assert.equal(themeService.colors.ansi[18].css, '#000087');
|
||||
assert.equal(themeService.colors.ansi[19].css, '#0000af');
|
||||
assert.equal(themeService.colors.ansi[20].css, '#0000d7');
|
||||
assert.equal(themeService.colors.ansi[21].css, '#0000ff');
|
||||
assert.equal(themeService.colors.ansi[22].css, '#005f00');
|
||||
assert.equal(themeService.colors.ansi[23].css, '#005f5f');
|
||||
assert.equal(themeService.colors.ansi[24].css, '#005f87');
|
||||
assert.equal(themeService.colors.ansi[25].css, '#005faf');
|
||||
assert.equal(themeService.colors.ansi[26].css, '#005fd7');
|
||||
assert.equal(themeService.colors.ansi[27].css, '#005fff');
|
||||
assert.equal(themeService.colors.ansi[28].css, '#008700');
|
||||
assert.equal(themeService.colors.ansi[29].css, '#00875f');
|
||||
assert.equal(themeService.colors.ansi[30].css, '#008787');
|
||||
assert.equal(themeService.colors.ansi[31].css, '#0087af');
|
||||
assert.equal(themeService.colors.ansi[32].css, '#0087d7');
|
||||
assert.equal(themeService.colors.ansi[33].css, '#0087ff');
|
||||
assert.equal(themeService.colors.ansi[34].css, '#00af00');
|
||||
assert.equal(themeService.colors.ansi[35].css, '#00af5f');
|
||||
assert.equal(themeService.colors.ansi[36].css, '#00af87');
|
||||
assert.equal(themeService.colors.ansi[37].css, '#00afaf');
|
||||
assert.equal(themeService.colors.ansi[38].css, '#00afd7');
|
||||
assert.equal(themeService.colors.ansi[39].css, '#00afff');
|
||||
assert.equal(themeService.colors.ansi[40].css, '#00d700');
|
||||
assert.equal(themeService.colors.ansi[41].css, '#00d75f');
|
||||
assert.equal(themeService.colors.ansi[42].css, '#00d787');
|
||||
assert.equal(themeService.colors.ansi[43].css, '#00d7af');
|
||||
assert.equal(themeService.colors.ansi[44].css, '#00d7d7');
|
||||
assert.equal(themeService.colors.ansi[45].css, '#00d7ff');
|
||||
assert.equal(themeService.colors.ansi[46].css, '#00ff00');
|
||||
assert.equal(themeService.colors.ansi[47].css, '#00ff5f');
|
||||
assert.equal(themeService.colors.ansi[48].css, '#00ff87');
|
||||
assert.equal(themeService.colors.ansi[49].css, '#00ffaf');
|
||||
assert.equal(themeService.colors.ansi[50].css, '#00ffd7');
|
||||
assert.equal(themeService.colors.ansi[51].css, '#00ffff');
|
||||
assert.equal(themeService.colors.ansi[52].css, '#5f0000');
|
||||
assert.equal(themeService.colors.ansi[53].css, '#5f005f');
|
||||
assert.equal(themeService.colors.ansi[54].css, '#5f0087');
|
||||
assert.equal(themeService.colors.ansi[55].css, '#5f00af');
|
||||
assert.equal(themeService.colors.ansi[56].css, '#5f00d7');
|
||||
assert.equal(themeService.colors.ansi[57].css, '#5f00ff');
|
||||
assert.equal(themeService.colors.ansi[58].css, '#5f5f00');
|
||||
assert.equal(themeService.colors.ansi[59].css, '#5f5f5f');
|
||||
assert.equal(themeService.colors.ansi[60].css, '#5f5f87');
|
||||
assert.equal(themeService.colors.ansi[61].css, '#5f5faf');
|
||||
assert.equal(themeService.colors.ansi[62].css, '#5f5fd7');
|
||||
assert.equal(themeService.colors.ansi[63].css, '#5f5fff');
|
||||
assert.equal(themeService.colors.ansi[64].css, '#5f8700');
|
||||
assert.equal(themeService.colors.ansi[65].css, '#5f875f');
|
||||
assert.equal(themeService.colors.ansi[66].css, '#5f8787');
|
||||
assert.equal(themeService.colors.ansi[67].css, '#5f87af');
|
||||
assert.equal(themeService.colors.ansi[68].css, '#5f87d7');
|
||||
assert.equal(themeService.colors.ansi[69].css, '#5f87ff');
|
||||
assert.equal(themeService.colors.ansi[70].css, '#5faf00');
|
||||
assert.equal(themeService.colors.ansi[71].css, '#5faf5f');
|
||||
assert.equal(themeService.colors.ansi[72].css, '#5faf87');
|
||||
assert.equal(themeService.colors.ansi[73].css, '#5fafaf');
|
||||
assert.equal(themeService.colors.ansi[74].css, '#5fafd7');
|
||||
assert.equal(themeService.colors.ansi[75].css, '#5fafff');
|
||||
assert.equal(themeService.colors.ansi[76].css, '#5fd700');
|
||||
assert.equal(themeService.colors.ansi[77].css, '#5fd75f');
|
||||
assert.equal(themeService.colors.ansi[78].css, '#5fd787');
|
||||
assert.equal(themeService.colors.ansi[79].css, '#5fd7af');
|
||||
assert.equal(themeService.colors.ansi[80].css, '#5fd7d7');
|
||||
assert.equal(themeService.colors.ansi[81].css, '#5fd7ff');
|
||||
assert.equal(themeService.colors.ansi[82].css, '#5fff00');
|
||||
assert.equal(themeService.colors.ansi[83].css, '#5fff5f');
|
||||
assert.equal(themeService.colors.ansi[84].css, '#5fff87');
|
||||
assert.equal(themeService.colors.ansi[85].css, '#5fffaf');
|
||||
assert.equal(themeService.colors.ansi[86].css, '#5fffd7');
|
||||
assert.equal(themeService.colors.ansi[87].css, '#5fffff');
|
||||
assert.equal(themeService.colors.ansi[88].css, '#870000');
|
||||
assert.equal(themeService.colors.ansi[89].css, '#87005f');
|
||||
assert.equal(themeService.colors.ansi[90].css, '#870087');
|
||||
assert.equal(themeService.colors.ansi[91].css, '#8700af');
|
||||
assert.equal(themeService.colors.ansi[92].css, '#8700d7');
|
||||
assert.equal(themeService.colors.ansi[93].css, '#8700ff');
|
||||
assert.equal(themeService.colors.ansi[94].css, '#875f00');
|
||||
assert.equal(themeService.colors.ansi[95].css, '#875f5f');
|
||||
assert.equal(themeService.colors.ansi[96].css, '#875f87');
|
||||
assert.equal(themeService.colors.ansi[97].css, '#875faf');
|
||||
assert.equal(themeService.colors.ansi[98].css, '#875fd7');
|
||||
assert.equal(themeService.colors.ansi[99].css, '#875fff');
|
||||
assert.equal(themeService.colors.ansi[100].css, '#878700');
|
||||
assert.equal(themeService.colors.ansi[101].css, '#87875f');
|
||||
assert.equal(themeService.colors.ansi[102].css, '#878787');
|
||||
assert.equal(themeService.colors.ansi[103].css, '#8787af');
|
||||
assert.equal(themeService.colors.ansi[104].css, '#8787d7');
|
||||
assert.equal(themeService.colors.ansi[105].css, '#8787ff');
|
||||
assert.equal(themeService.colors.ansi[106].css, '#87af00');
|
||||
assert.equal(themeService.colors.ansi[107].css, '#87af5f');
|
||||
assert.equal(themeService.colors.ansi[108].css, '#87af87');
|
||||
assert.equal(themeService.colors.ansi[109].css, '#87afaf');
|
||||
assert.equal(themeService.colors.ansi[110].css, '#87afd7');
|
||||
assert.equal(themeService.colors.ansi[111].css, '#87afff');
|
||||
assert.equal(themeService.colors.ansi[112].css, '#87d700');
|
||||
assert.equal(themeService.colors.ansi[113].css, '#87d75f');
|
||||
assert.equal(themeService.colors.ansi[114].css, '#87d787');
|
||||
assert.equal(themeService.colors.ansi[115].css, '#87d7af');
|
||||
assert.equal(themeService.colors.ansi[116].css, '#87d7d7');
|
||||
assert.equal(themeService.colors.ansi[117].css, '#87d7ff');
|
||||
assert.equal(themeService.colors.ansi[118].css, '#87ff00');
|
||||
assert.equal(themeService.colors.ansi[119].css, '#87ff5f');
|
||||
assert.equal(themeService.colors.ansi[120].css, '#87ff87');
|
||||
assert.equal(themeService.colors.ansi[121].css, '#87ffaf');
|
||||
assert.equal(themeService.colors.ansi[122].css, '#87ffd7');
|
||||
assert.equal(themeService.colors.ansi[123].css, '#87ffff');
|
||||
assert.equal(themeService.colors.ansi[124].css, '#af0000');
|
||||
assert.equal(themeService.colors.ansi[125].css, '#af005f');
|
||||
assert.equal(themeService.colors.ansi[126].css, '#af0087');
|
||||
assert.equal(themeService.colors.ansi[127].css, '#af00af');
|
||||
assert.equal(themeService.colors.ansi[128].css, '#af00d7');
|
||||
assert.equal(themeService.colors.ansi[129].css, '#af00ff');
|
||||
assert.equal(themeService.colors.ansi[130].css, '#af5f00');
|
||||
assert.equal(themeService.colors.ansi[131].css, '#af5f5f');
|
||||
assert.equal(themeService.colors.ansi[132].css, '#af5f87');
|
||||
assert.equal(themeService.colors.ansi[133].css, '#af5faf');
|
||||
assert.equal(themeService.colors.ansi[134].css, '#af5fd7');
|
||||
assert.equal(themeService.colors.ansi[135].css, '#af5fff');
|
||||
assert.equal(themeService.colors.ansi[136].css, '#af8700');
|
||||
assert.equal(themeService.colors.ansi[137].css, '#af875f');
|
||||
assert.equal(themeService.colors.ansi[138].css, '#af8787');
|
||||
assert.equal(themeService.colors.ansi[139].css, '#af87af');
|
||||
assert.equal(themeService.colors.ansi[140].css, '#af87d7');
|
||||
assert.equal(themeService.colors.ansi[141].css, '#af87ff');
|
||||
assert.equal(themeService.colors.ansi[142].css, '#afaf00');
|
||||
assert.equal(themeService.colors.ansi[143].css, '#afaf5f');
|
||||
assert.equal(themeService.colors.ansi[144].css, '#afaf87');
|
||||
assert.equal(themeService.colors.ansi[145].css, '#afafaf');
|
||||
assert.equal(themeService.colors.ansi[146].css, '#afafd7');
|
||||
assert.equal(themeService.colors.ansi[147].css, '#afafff');
|
||||
assert.equal(themeService.colors.ansi[148].css, '#afd700');
|
||||
assert.equal(themeService.colors.ansi[149].css, '#afd75f');
|
||||
assert.equal(themeService.colors.ansi[150].css, '#afd787');
|
||||
assert.equal(themeService.colors.ansi[151].css, '#afd7af');
|
||||
assert.equal(themeService.colors.ansi[152].css, '#afd7d7');
|
||||
assert.equal(themeService.colors.ansi[153].css, '#afd7ff');
|
||||
assert.equal(themeService.colors.ansi[154].css, '#afff00');
|
||||
assert.equal(themeService.colors.ansi[155].css, '#afff5f');
|
||||
assert.equal(themeService.colors.ansi[156].css, '#afff87');
|
||||
assert.equal(themeService.colors.ansi[157].css, '#afffaf');
|
||||
assert.equal(themeService.colors.ansi[158].css, '#afffd7');
|
||||
assert.equal(themeService.colors.ansi[159].css, '#afffff');
|
||||
assert.equal(themeService.colors.ansi[160].css, '#d70000');
|
||||
assert.equal(themeService.colors.ansi[161].css, '#d7005f');
|
||||
assert.equal(themeService.colors.ansi[162].css, '#d70087');
|
||||
assert.equal(themeService.colors.ansi[163].css, '#d700af');
|
||||
assert.equal(themeService.colors.ansi[164].css, '#d700d7');
|
||||
assert.equal(themeService.colors.ansi[165].css, '#d700ff');
|
||||
assert.equal(themeService.colors.ansi[166].css, '#d75f00');
|
||||
assert.equal(themeService.colors.ansi[167].css, '#d75f5f');
|
||||
assert.equal(themeService.colors.ansi[168].css, '#d75f87');
|
||||
assert.equal(themeService.colors.ansi[169].css, '#d75faf');
|
||||
assert.equal(themeService.colors.ansi[170].css, '#d75fd7');
|
||||
assert.equal(themeService.colors.ansi[171].css, '#d75fff');
|
||||
assert.equal(themeService.colors.ansi[172].css, '#d78700');
|
||||
assert.equal(themeService.colors.ansi[173].css, '#d7875f');
|
||||
assert.equal(themeService.colors.ansi[174].css, '#d78787');
|
||||
assert.equal(themeService.colors.ansi[175].css, '#d787af');
|
||||
assert.equal(themeService.colors.ansi[176].css, '#d787d7');
|
||||
assert.equal(themeService.colors.ansi[177].css, '#d787ff');
|
||||
assert.equal(themeService.colors.ansi[178].css, '#d7af00');
|
||||
assert.equal(themeService.colors.ansi[179].css, '#d7af5f');
|
||||
assert.equal(themeService.colors.ansi[180].css, '#d7af87');
|
||||
assert.equal(themeService.colors.ansi[181].css, '#d7afaf');
|
||||
assert.equal(themeService.colors.ansi[182].css, '#d7afd7');
|
||||
assert.equal(themeService.colors.ansi[183].css, '#d7afff');
|
||||
assert.equal(themeService.colors.ansi[184].css, '#d7d700');
|
||||
assert.equal(themeService.colors.ansi[185].css, '#d7d75f');
|
||||
assert.equal(themeService.colors.ansi[186].css, '#d7d787');
|
||||
assert.equal(themeService.colors.ansi[187].css, '#d7d7af');
|
||||
assert.equal(themeService.colors.ansi[188].css, '#d7d7d7');
|
||||
assert.equal(themeService.colors.ansi[189].css, '#d7d7ff');
|
||||
assert.equal(themeService.colors.ansi[190].css, '#d7ff00');
|
||||
assert.equal(themeService.colors.ansi[191].css, '#d7ff5f');
|
||||
assert.equal(themeService.colors.ansi[192].css, '#d7ff87');
|
||||
assert.equal(themeService.colors.ansi[193].css, '#d7ffaf');
|
||||
assert.equal(themeService.colors.ansi[194].css, '#d7ffd7');
|
||||
assert.equal(themeService.colors.ansi[195].css, '#d7ffff');
|
||||
assert.equal(themeService.colors.ansi[196].css, '#ff0000');
|
||||
assert.equal(themeService.colors.ansi[197].css, '#ff005f');
|
||||
assert.equal(themeService.colors.ansi[198].css, '#ff0087');
|
||||
assert.equal(themeService.colors.ansi[199].css, '#ff00af');
|
||||
assert.equal(themeService.colors.ansi[200].css, '#ff00d7');
|
||||
assert.equal(themeService.colors.ansi[201].css, '#ff00ff');
|
||||
assert.equal(themeService.colors.ansi[202].css, '#ff5f00');
|
||||
assert.equal(themeService.colors.ansi[203].css, '#ff5f5f');
|
||||
assert.equal(themeService.colors.ansi[204].css, '#ff5f87');
|
||||
assert.equal(themeService.colors.ansi[205].css, '#ff5faf');
|
||||
assert.equal(themeService.colors.ansi[206].css, '#ff5fd7');
|
||||
assert.equal(themeService.colors.ansi[207].css, '#ff5fff');
|
||||
assert.equal(themeService.colors.ansi[208].css, '#ff8700');
|
||||
assert.equal(themeService.colors.ansi[209].css, '#ff875f');
|
||||
assert.equal(themeService.colors.ansi[210].css, '#ff8787');
|
||||
assert.equal(themeService.colors.ansi[211].css, '#ff87af');
|
||||
assert.equal(themeService.colors.ansi[212].css, '#ff87d7');
|
||||
assert.equal(themeService.colors.ansi[213].css, '#ff87ff');
|
||||
assert.equal(themeService.colors.ansi[214].css, '#ffaf00');
|
||||
assert.equal(themeService.colors.ansi[215].css, '#ffaf5f');
|
||||
assert.equal(themeService.colors.ansi[216].css, '#ffaf87');
|
||||
assert.equal(themeService.colors.ansi[217].css, '#ffafaf');
|
||||
assert.equal(themeService.colors.ansi[218].css, '#ffafd7');
|
||||
assert.equal(themeService.colors.ansi[219].css, '#ffafff');
|
||||
assert.equal(themeService.colors.ansi[220].css, '#ffd700');
|
||||
assert.equal(themeService.colors.ansi[221].css, '#ffd75f');
|
||||
assert.equal(themeService.colors.ansi[222].css, '#ffd787');
|
||||
assert.equal(themeService.colors.ansi[223].css, '#ffd7af');
|
||||
assert.equal(themeService.colors.ansi[224].css, '#ffd7d7');
|
||||
assert.equal(themeService.colors.ansi[225].css, '#ffd7ff');
|
||||
assert.equal(themeService.colors.ansi[226].css, '#ffff00');
|
||||
assert.equal(themeService.colors.ansi[227].css, '#ffff5f');
|
||||
assert.equal(themeService.colors.ansi[228].css, '#ffff87');
|
||||
assert.equal(themeService.colors.ansi[229].css, '#ffffaf');
|
||||
assert.equal(themeService.colors.ansi[230].css, '#ffffd7');
|
||||
assert.equal(themeService.colors.ansi[231].css, '#ffffff');
|
||||
assert.equal(themeService.colors.ansi[232].css, '#080808');
|
||||
assert.equal(themeService.colors.ansi[233].css, '#121212');
|
||||
assert.equal(themeService.colors.ansi[234].css, '#1c1c1c');
|
||||
assert.equal(themeService.colors.ansi[235].css, '#262626');
|
||||
assert.equal(themeService.colors.ansi[236].css, '#303030');
|
||||
assert.equal(themeService.colors.ansi[237].css, '#3a3a3a');
|
||||
assert.equal(themeService.colors.ansi[238].css, '#444444');
|
||||
assert.equal(themeService.colors.ansi[239].css, '#4e4e4e');
|
||||
assert.equal(themeService.colors.ansi[240].css, '#585858');
|
||||
assert.equal(themeService.colors.ansi[241].css, '#626262');
|
||||
assert.equal(themeService.colors.ansi[242].css, '#6c6c6c');
|
||||
assert.equal(themeService.colors.ansi[243].css, '#767676');
|
||||
assert.equal(themeService.colors.ansi[244].css, '#808080');
|
||||
assert.equal(themeService.colors.ansi[245].css, '#8a8a8a');
|
||||
assert.equal(themeService.colors.ansi[246].css, '#949494');
|
||||
assert.equal(themeService.colors.ansi[247].css, '#9e9e9e');
|
||||
assert.equal(themeService.colors.ansi[248].css, '#a8a8a8');
|
||||
assert.equal(themeService.colors.ansi[249].css, '#b2b2b2');
|
||||
assert.equal(themeService.colors.ansi[250].css, '#bcbcbc');
|
||||
assert.equal(themeService.colors.ansi[251].css, '#c6c6c6');
|
||||
assert.equal(themeService.colors.ansi[252].css, '#d0d0d0');
|
||||
assert.equal(themeService.colors.ansi[253].css, '#dadada');
|
||||
assert.equal(themeService.colors.ansi[254].css, '#e4e4e4');
|
||||
assert.equal(themeService.colors.ansi[255].css, '#eeeeee');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setTheme', () => {
|
||||
it('should not throw when not setting all colors', () => {
|
||||
assert.doesNotThrow(() => {
|
||||
optionsService.options.theme = {};
|
||||
});
|
||||
});
|
||||
|
||||
it('should set a partial set of colors, using the default if not present', () => {
|
||||
assert.equal(themeService.colors.background.css, '#000000');
|
||||
assert.equal(themeService.colors.foreground.css, '#ffffff');
|
||||
optionsService.options.theme = {
|
||||
background: '#FF0000',
|
||||
foreground: '#00FF00'
|
||||
};
|
||||
assert.equal(themeService.colors.background.css, '#FF0000');
|
||||
assert.equal(themeService.colors.foreground.css, '#00FF00');
|
||||
optionsService.options.theme = {
|
||||
background: '#0000FF'
|
||||
};
|
||||
assert.equal(themeService.colors.background.css, '#0000FF');
|
||||
// FG reverts back to default
|
||||
assert.equal(themeService.colors.foreground.css, '#ffffff');
|
||||
});
|
||||
|
||||
it('should set all extended ansi colors in reverse order', () => {
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: DEFAULT_ANSI_COLORS.map(a => a.css).slice().reverse()
|
||||
};
|
||||
|
||||
for (let ansiColor = 16; ansiColor <= 255; ansiColor++) {
|
||||
assert.equal(themeService.colors.ansi[ansiColor].css, DEFAULT_ANSI_COLORS[255 + 16 - ansiColor].css);
|
||||
}
|
||||
});
|
||||
|
||||
it('should set one extended ansi color and keep the other default', () => {
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: ['#ffffff']
|
||||
};
|
||||
|
||||
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
|
||||
assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
|
||||
});
|
||||
|
||||
it('should set extended ansi colors to the default when they are unset', () => {
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: ['#ffffff']
|
||||
};
|
||||
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
|
||||
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: []
|
||||
};
|
||||
assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
|
||||
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: ['#ffffff']
|
||||
};
|
||||
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
|
||||
|
||||
optionsService.options.theme = {};
|
||||
assert.equal(themeService.colors.ansi[16].css, DEFAULT_ANSI_COLORS[16].css);
|
||||
});
|
||||
|
||||
it('should set extended ansi colors to the default when they are partially unset', () => {
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: ['#ffffff', '#000000']
|
||||
};
|
||||
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
|
||||
assert.equal(themeService.colors.ansi[17].css, '#000000');
|
||||
|
||||
optionsService.options.theme = {
|
||||
extendedAnsi: ['#ffffff']
|
||||
};
|
||||
assert.equal(themeService.colors.ansi[16].css, '#ffffff');
|
||||
assert.equal(themeService.colors.ansi[17].css, DEFAULT_ANSI_COLORS[17].css);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,235 @@
|
||||
/**
|
||||
* Copyright (c) 2022 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ColorContrastCache } from 'browser/ColorContrastCache';
|
||||
import { IThemeService } from 'browser/services/Services';
|
||||
import { IColorContrastCache, IColorSet, ReadonlyColorSet } from 'browser/Types';
|
||||
import { channels, color, css, NULL_COLOR } from 'common/Color';
|
||||
import { EventEmitter } from 'common/EventEmitter';
|
||||
import { Disposable } from 'common/Lifecycle';
|
||||
import { IOptionsService, ITheme } from 'common/services/Services';
|
||||
import { ColorIndex, IColor } from 'common/Types';
|
||||
|
||||
interface IRestoreColorSet {
|
||||
foreground: IColor;
|
||||
background: IColor;
|
||||
cursor: IColor;
|
||||
ansi: IColor[];
|
||||
}
|
||||
|
||||
|
||||
const DEFAULT_FOREGROUND = css.toColor('#ffffff');
|
||||
const DEFAULT_BACKGROUND = css.toColor('#000000');
|
||||
const DEFAULT_CURSOR = css.toColor('#ffffff');
|
||||
const DEFAULT_CURSOR_ACCENT = css.toColor('#000000');
|
||||
const DEFAULT_SELECTION = {
|
||||
css: 'rgba(255, 255, 255, 0.3)',
|
||||
rgba: 0xFFFFFF4D
|
||||
};
|
||||
|
||||
// An IIFE to generate DEFAULT_ANSI_COLORS.
|
||||
export const DEFAULT_ANSI_COLORS = Object.freeze((() => {
|
||||
const colors = [
|
||||
// dark:
|
||||
css.toColor('#2e3436'),
|
||||
css.toColor('#cc0000'),
|
||||
css.toColor('#4e9a06'),
|
||||
css.toColor('#c4a000'),
|
||||
css.toColor('#3465a4'),
|
||||
css.toColor('#75507b'),
|
||||
css.toColor('#06989a'),
|
||||
css.toColor('#d3d7cf'),
|
||||
// bright:
|
||||
css.toColor('#555753'),
|
||||
css.toColor('#ef2929'),
|
||||
css.toColor('#8ae234'),
|
||||
css.toColor('#fce94f'),
|
||||
css.toColor('#729fcf'),
|
||||
css.toColor('#ad7fa8'),
|
||||
css.toColor('#34e2e2'),
|
||||
css.toColor('#eeeeec')
|
||||
];
|
||||
|
||||
// Fill in the remaining 240 ANSI colors.
|
||||
// Generate colors (16-231)
|
||||
const v = [0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff];
|
||||
for (let i = 0; i < 216; i++) {
|
||||
const r = v[(i / 36) % 6 | 0];
|
||||
const g = v[(i / 6) % 6 | 0];
|
||||
const b = v[i % 6];
|
||||
colors.push({
|
||||
css: channels.toCss(r, g, b),
|
||||
rgba: channels.toRgba(r, g, b)
|
||||
});
|
||||
}
|
||||
|
||||
// Generate greys (232-255)
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const c = 8 + i * 10;
|
||||
colors.push({
|
||||
css: channels.toCss(c, c, c),
|
||||
rgba: channels.toRgba(c, c, c)
|
||||
});
|
||||
}
|
||||
|
||||
return colors;
|
||||
})());
|
||||
|
||||
export class ThemeService extends Disposable implements IThemeService {
|
||||
public serviceBrand: undefined;
|
||||
|
||||
private _colors: IColorSet;
|
||||
private _contrastCache: IColorContrastCache;
|
||||
private _restoreColors!: IRestoreColorSet;
|
||||
|
||||
public get colors(): ReadonlyColorSet { return this._colors; }
|
||||
|
||||
private readonly _onChangeColors = this.register(new EventEmitter<ReadonlyColorSet>());
|
||||
public readonly onChangeColors = this._onChangeColors.event;
|
||||
|
||||
constructor(
|
||||
@IOptionsService private readonly _optionsService: IOptionsService
|
||||
) {
|
||||
super();
|
||||
|
||||
this._contrastCache = new ColorContrastCache();
|
||||
this._colors = {
|
||||
foreground: DEFAULT_FOREGROUND,
|
||||
background: DEFAULT_BACKGROUND,
|
||||
cursor: DEFAULT_CURSOR,
|
||||
cursorAccent: DEFAULT_CURSOR_ACCENT,
|
||||
selectionForeground: undefined,
|
||||
selectionBackgroundTransparent: DEFAULT_SELECTION,
|
||||
selectionBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
selectionInactiveBackgroundTransparent: DEFAULT_SELECTION,
|
||||
selectionInactiveBackgroundOpaque: color.blend(DEFAULT_BACKGROUND, DEFAULT_SELECTION),
|
||||
ansi: DEFAULT_ANSI_COLORS.slice(),
|
||||
contrastCache: this._contrastCache
|
||||
};
|
||||
this._updateRestoreColors();
|
||||
this._setTheme(this._optionsService.rawOptions.theme);
|
||||
|
||||
this.register(this._optionsService.onSpecificOptionChange('minimumContrastRatio', () => this._contrastCache.clear()));
|
||||
this.register(this._optionsService.onSpecificOptionChange('theme', () => this._setTheme(this._optionsService.rawOptions.theme)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the terminal's theme.
|
||||
* @param theme The theme to use. If a partial theme is provided then default
|
||||
* colors will be used where colors are not defined.
|
||||
*/
|
||||
private _setTheme(theme: ITheme = {}): void {
|
||||
const colors = this._colors;
|
||||
colors.foreground = parseColor(theme.foreground, DEFAULT_FOREGROUND);
|
||||
colors.background = parseColor(theme.background, DEFAULT_BACKGROUND);
|
||||
colors.cursor = parseColor(theme.cursor, DEFAULT_CURSOR);
|
||||
colors.cursorAccent = parseColor(theme.cursorAccent, DEFAULT_CURSOR_ACCENT);
|
||||
colors.selectionBackgroundTransparent = parseColor(theme.selectionBackground, DEFAULT_SELECTION);
|
||||
colors.selectionBackgroundOpaque = color.blend(colors.background, colors.selectionBackgroundTransparent);
|
||||
colors.selectionInactiveBackgroundTransparent = parseColor(theme.selectionInactiveBackground, colors.selectionBackgroundTransparent);
|
||||
colors.selectionInactiveBackgroundOpaque = color.blend(colors.background, colors.selectionInactiveBackgroundTransparent);
|
||||
colors.selectionForeground = theme.selectionForeground ? parseColor(theme.selectionForeground, NULL_COLOR) : undefined;
|
||||
if (colors.selectionForeground === NULL_COLOR) {
|
||||
colors.selectionForeground = undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* If selection color is opaque, blend it with background with 0.3 opacity
|
||||
* Issue #2737
|
||||
*/
|
||||
if (color.isOpaque(colors.selectionBackgroundTransparent)) {
|
||||
const opacity = 0.3;
|
||||
colors.selectionBackgroundTransparent = color.opacity(colors.selectionBackgroundTransparent, opacity);
|
||||
}
|
||||
if (color.isOpaque(colors.selectionInactiveBackgroundTransparent)) {
|
||||
const opacity = 0.3;
|
||||
colors.selectionInactiveBackgroundTransparent = color.opacity(colors.selectionInactiveBackgroundTransparent, opacity);
|
||||
}
|
||||
colors.ansi = DEFAULT_ANSI_COLORS.slice();
|
||||
colors.ansi[0] = parseColor(theme.black, DEFAULT_ANSI_COLORS[0]);
|
||||
colors.ansi[1] = parseColor(theme.red, DEFAULT_ANSI_COLORS[1]);
|
||||
colors.ansi[2] = parseColor(theme.green, DEFAULT_ANSI_COLORS[2]);
|
||||
colors.ansi[3] = parseColor(theme.yellow, DEFAULT_ANSI_COLORS[3]);
|
||||
colors.ansi[4] = parseColor(theme.blue, DEFAULT_ANSI_COLORS[4]);
|
||||
colors.ansi[5] = parseColor(theme.magenta, DEFAULT_ANSI_COLORS[5]);
|
||||
colors.ansi[6] = parseColor(theme.cyan, DEFAULT_ANSI_COLORS[6]);
|
||||
colors.ansi[7] = parseColor(theme.white, DEFAULT_ANSI_COLORS[7]);
|
||||
colors.ansi[8] = parseColor(theme.brightBlack, DEFAULT_ANSI_COLORS[8]);
|
||||
colors.ansi[9] = parseColor(theme.brightRed, DEFAULT_ANSI_COLORS[9]);
|
||||
colors.ansi[10] = parseColor(theme.brightGreen, DEFAULT_ANSI_COLORS[10]);
|
||||
colors.ansi[11] = parseColor(theme.brightYellow, DEFAULT_ANSI_COLORS[11]);
|
||||
colors.ansi[12] = parseColor(theme.brightBlue, DEFAULT_ANSI_COLORS[12]);
|
||||
colors.ansi[13] = parseColor(theme.brightMagenta, DEFAULT_ANSI_COLORS[13]);
|
||||
colors.ansi[14] = parseColor(theme.brightCyan, DEFAULT_ANSI_COLORS[14]);
|
||||
colors.ansi[15] = parseColor(theme.brightWhite, DEFAULT_ANSI_COLORS[15]);
|
||||
if (theme.extendedAnsi) {
|
||||
const colorCount = Math.min(colors.ansi.length - 16, theme.extendedAnsi.length);
|
||||
for (let i = 0; i < colorCount; i++) {
|
||||
colors.ansi[i + 16] = parseColor(theme.extendedAnsi[i], DEFAULT_ANSI_COLORS[i + 16]);
|
||||
}
|
||||
}
|
||||
// Clear our the cache
|
||||
this._contrastCache.clear();
|
||||
this._updateRestoreColors();
|
||||
this._onChangeColors.fire(this.colors);
|
||||
}
|
||||
|
||||
public restoreColor(slot?: ColorIndex): void {
|
||||
this._restoreColor(slot);
|
||||
this._onChangeColors.fire(this.colors);
|
||||
}
|
||||
|
||||
private _restoreColor(slot: ColorIndex | undefined): void {
|
||||
// unset slot restores all ansi colors
|
||||
if (slot === undefined) {
|
||||
for (let i = 0; i < this._restoreColors.ansi.length; ++i) {
|
||||
this._colors.ansi[i] = this._restoreColors.ansi[i];
|
||||
}
|
||||
return;
|
||||
}
|
||||
switch (slot) {
|
||||
case ColorIndex.FOREGROUND:
|
||||
this._colors.foreground = this._restoreColors.foreground;
|
||||
break;
|
||||
case ColorIndex.BACKGROUND:
|
||||
this._colors.background = this._restoreColors.background;
|
||||
break;
|
||||
case ColorIndex.CURSOR:
|
||||
this._colors.cursor = this._restoreColors.cursor;
|
||||
break;
|
||||
default:
|
||||
this._colors.ansi[slot] = this._restoreColors.ansi[slot];
|
||||
}
|
||||
}
|
||||
|
||||
public modifyColors(callback: (colors: IColorSet) => void): void {
|
||||
callback(this._colors);
|
||||
// Assume the change happened
|
||||
this._onChangeColors.fire(this.colors);
|
||||
}
|
||||
|
||||
private _updateRestoreColors(): void {
|
||||
this._restoreColors = {
|
||||
foreground: this._colors.foreground,
|
||||
background: this._colors.background,
|
||||
cursor: this._colors.cursor,
|
||||
ansi: this._colors.ansi.slice()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function parseColor(
|
||||
cssString: string | undefined,
|
||||
fallback: IColor
|
||||
): IColor {
|
||||
if (cssString !== undefined) {
|
||||
try {
|
||||
return css.toColor(cssString);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user