Compare commits

...
391 Commits
Author SHA1 Message Date
Daniel Imms 19c760a204 Merge pull request #4333 from xtermjs/revert-4196-ansiheadless
Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal"
2022-12-19 10:20:24 -08:00
Daniel Imms 3a81ef54ca Revert "SerializeAsHTML(): Fallback to default ansi colors when running headless terminal" 2022-12-19 10:10:59 -08:00
Daniel Imms 6cfdcb1b0d Merge pull request #4332 from Tyriar/r5_1_0
v5.1.0
2022-12-19 09:56:50 -08:00
Daniel Imms 30ef1fcaa5 v5.1.0 2022-12-19 09:56:20 -08:00
Daniel Imms 84b8121105 Merge pull request #4288 from jerch/fix_weblinks
Fix weblinks
2022-12-19 08:57:15 -08:00
Jörg Breitbart 218cfd256c Merge branch 'master' into fix_weblinks 2022-12-19 15:59:11 +01:00
Jörg Breitbart 1757f2733c explicit type for link test data 2022-12-19 15:49:10 +01:00
Jörg Breitbart 9411acbc73 move link tests to demo 2022-12-19 15:33:10 +01:00
Daniel Imms cb4102cdcb Merge pull request #4330 from Tyriar/2533
Add VT buttons through CSI P
2022-12-19 06:30:11 -08:00
Daniel Imms 1926c9bbbc Add sequence buttons through CSI P 2022-12-19 06:20:01 -08:00
Daniel Imms 06145c02b1 Add IL vt button
Part of #2533
2022-12-19 06:11:34 -08:00
Jörg Breitbart d65898ffed Merge branch 'master' into fix_weblinks 2022-12-19 14:19:43 +01:00
Daniel Imms b5e6066f8c Merge pull request #4327 from Tyriar/2603
Improve IAttributeData jsdoc
2022-12-18 08:51:41 -08:00
Daniel Imms 0981ca8796 Merge pull request #4328 from Tyriar/vscode166909
Remove ! to improve safety
2022-12-16 11:16:17 -08:00
Daniel Imms ffeaa2b976 Remove ! to improve safety
See microsoft/vscode#166909

This doesn't necessarily fix that, but it does remove the hacky ! which could
potentially let an error slip past TS.
2022-12-16 11:05:03 -08:00
Daniel Imms 167765f932 Improve IAttributeData jsdoc
Fixes #2603
2022-12-16 10:53:21 -08:00
Daniel Imms bcdbb61ed4 Merge pull request #4326 from Tyriar/166878
Fix NPE in webgl renderer
2022-12-16 10:51:52 -08:00
Daniel Imms ea7571c2b9 Fix NPE in webgl renderer
Theory is this can happen when a resize down happens as end will be too big

See microsoft/vscode#166878
2022-12-16 10:42:05 -08:00
Daniel Imms 880b726bcb Merge pull request #4325 from Tyriar/4056
Fix canvas renderer selection not re-rendering sometimes
2022-12-16 09:39:28 -08:00
Daniel Imms 902da2800b Merge pull request #4324 from Ry0taK/feature/secure-links-by-default
Add allowNonHttpProtocols option to ILinkHandler
2022-12-16 09:32:54 -08:00
Daniel Imms 2bd9c4e839 Fix canvas renderer selection not re-rendering sometimes
Fixes #4056
2022-12-16 09:28:02 -08:00
RyotaK b3d36478ca Properly handle errors from URL constructor 2022-12-16 17:01:00 +00:00
RyotaK b07c016fea Adjust the comment 2022-12-16 16:58:55 +00:00
Daniel Imms 0b5625581e Merge pull request #4284 from JasonXJ/a11y-2
Fix a11y issues #4269
2022-12-16 08:57:55 -08:00
RyotaK e4b473785d Make comment more clear 2022-12-16 16:54:27 +00:00
RyotaK cb8d93a1ef Implement protocol validation 2022-12-16 16:50:57 +00:00
RyotaK 8636d0f820 Add allowNonHttpProtocols to typings 2022-12-17 01:31:11 +09:00
Daniel Imms f79644bd4c Merge pull request #4298 from jerch/fix_4295
re-eval previous active link
2022-12-16 08:12:19 -08:00
Daniel Imms 917e31ee17 Merge pull request #4322 from Tyriar/4124
Explain object option edge case
2022-12-15 17:06:24 -08:00
Daniel Imms aba59e9386 Explain object option edge case
Fixes #4124
2022-12-15 16:41:31 -08:00
Daniel Imms 1c7c4eb62f Merge pull request #4315 from Tyriar/2757
Allow scroll bar interaction in demo in screenReaderMode
2022-12-15 07:58:45 -08:00
Daniel Imms fc41968131 Allow scroll bar interaction in demo in screenReaderMode
Fixes #2757
2022-12-15 07:50:45 -08:00
Daniel Imms 04177de95b Merge pull request #4313 from Tyriar/4258
Support slash and triangle custom glyphs
2022-12-14 13:36:35 -08:00
Daniel Imms c9c07aeabd Support slash and triangle custom glyphs
This also fixes an issue with padding making glyphs regress

Fixes #4258
2022-12-14 13:15:05 -08:00
Daniel Imms 0cc759e0b1 Merge pull request #4312 from xtermjs/Tyriar-patch-1
Point new issue questions at GH discussions
2022-12-14 12:30:33 -08:00
Daniel Imms 852255ea03 Point new issue questions at GH discussions 2022-12-14 12:10:48 -08:00
Daniel Imms 88f459583e Merge pull request #4311 from Tyriar/vtbuttons
Add VT arrow buttons
2022-12-14 09:37:38 -08:00
Daniel Imms fd8a0690c9 Add VT arrow buttons
Part of #2064
2022-12-14 09:28:25 -08:00
Daniel Imms 6e2744a299 Merge pull request #4310 from Tyriar/4309
Improve explanation of danger with the linkHandler API
2022-12-14 08:21:55 -08:00
Daniel Imms 15e3b1ac12 Improve explanation of danger with the linkHandler API
Fixes #4309
2022-12-14 08:10:52 -08:00
Daniel Imms 5a5ed6d767 Merge pull request #4306 from Tyriar/4161
Add combo entries to sgr test for testing multiple sgr
2022-12-11 13:42:47 -08:00
Daniel Imms 74ea0c23ca Add combo entries to sgr test for testing multiple sgr 2022-12-11 13:11:16 -08:00
Daniel Imms 6d0a287ed9 Merge pull request #4305 from Tyriar/4221
Fix line height and letter spacing on canvas renderer
2022-12-11 11:54:57 -08:00
Daniel Imms 83ff4e97c3 Merge pull request #4304 from Tyriar/4259
Simplify options API
2022-12-11 11:52:08 -08:00
Daniel Imms 7b28dc4d59 Fix line height and letter spacing on canvas renderer
Fixes #4221
2022-12-11 11:47:09 -08:00
Daniel Imms 02d3ef599a Fix internal usages that needed Required<ITerminalOptions> 2022-12-11 11:42:14 -08:00
Daniel Imms c118f04187 Sync xterm-headless.d.ts 2022-12-11 11:36:32 -08:00
Daniel Imms 2bd3869547 Use a monospace font in example 2022-12-11 11:33:23 -08:00
Daniel Imms 69379ba59b Simplify options API
Fixes #4259
2022-12-11 11:33:03 -08:00
Daniel Imms 5ed05295ba Merge pull request #4301 from Tyriar/cleanup_coreservice
Avoid passing circular callback into CoreService ctor
2022-12-11 07:44:46 -08:00
Daniel Imms 4ee4a0f988 Merge branch 'master' into cleanup_coreservice 2022-12-11 07:36:01 -08:00
Daniel Imms d6b8a21219 Avoid passing circular callback into CoreService ctor 2022-12-11 07:35:11 -08:00
Daniel Imms 2e2d275d6c Merge pull request #4289 from JasonXJ/scroll-options
Add option scrollOnUserInput
2022-12-11 07:31:31 -08:00
Daniel Imms 33bba35be7 Merge pull request #4291 from jerch/fix_4223
fix demo in epiphany
2022-12-11 07:17:08 -08:00
jerch c7660fb49c Merge pull request #4299 from xtermjs/dependabot/npm_and_yarn/express-4.17.3
Bump express from 4.17.1 to 4.17.3
2022-12-10 20:50:46 +01:00
dependabot[bot] 6d527c42b0 Bump express from 4.17.1 to 4.17.3
Bumps [express](https://github.com/expressjs/express) from 4.17.1 to 4.17.3.
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/master/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.17.1...4.17.3)

---
updated-dependencies:
- dependency-name: express
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-10 17:06:32 +00:00
Jörg Breitbart 440da4a5ff reeval previous active link, fixes #4295 2022-12-09 16:37:56 +01:00
Jörg Breitbart e66fd50383 cleanup & more comments 2022-12-09 14:37:31 +01:00
Jörg Breitbart f69f9decca allow ~ within urls 2022-12-08 17:50:55 +01:00
Jörg Breitbart e70138b63c revert temp changes in linkifier2 2022-12-08 17:46:45 +01:00
Jörg Breitbart becec91e98 better regexp, fix offset issue in #4294 2022-12-08 17:35:45 +01:00
Jörg Breitbart 03dc1908a5 Merge branch 'master' into fix_weblinks 2022-12-08 15:19:08 +01:00
Jörg Breitbart d2298d84a0 move safari check to WebglAddon ctor 2022-12-07 23:56:43 +01:00
Jörg Breitbart 0b56b56c06 skipping renderer on NUL and SP 2022-12-07 22:55:45 +01:00
Daniel Imms 6d46b7b86f Merge pull request #4115 from jerch/buffer_optimizations
some buffer handling optimizations
2022-12-07 13:43:40 -08:00
Jörg Breitbart b829ad8386 Merge branch 'master' into fix_4223 2022-12-07 22:28:00 +01:00
Jörg Breitbart 9bc65c36a6 Merge branch 'master' into buffer_optimizations 2022-12-07 19:45:14 +01:00
Jörg Breitbart a562f33d53 remove nonsense exp runtime of rescans 2022-12-07 19:43:56 +01:00
Daniel Imms fe30cd6ed8 Merge pull request #4290 from JasonXJ/alt-space
Send corresponding escape code for alt+space and ctrl+alt+space
2022-12-07 06:34:23 -08:00
Jason Lin 9e84895141 polish 2022-12-07 10:12:43 +11:00
Daniel Imms 497df06f3f Merge pull request #4293 from Tyriar/4292
Log a warning if task queues exceed deadline by 20ms
2022-12-06 14:20:37 -08:00
Daniel Imms 650cda31fd Tweak logging 2022-12-06 14:10:40 -08:00
Daniel Imms dec3791f4c Log a warning if task queues exceed deadline by 20ms
Fixes #4292
2022-12-06 14:04:10 -08:00
Jörg Breitbart e46a9e12d0 fix demo in epiphany 2022-12-06 13:40:25 +01:00
Jason Lin 7f64fa9ddd Send corresponding escape code for alt+space and ctrl+alt+space
The previous behavior is that this combinations do nothing.
2022-12-06 15:57:19 +11:00
Jason Lin 73d20c1d3b Add option scrollOnKeypress
Note that we also change the existing behavior a bit:
CoreService.triggerDataEvent() will not scroll to the bottom unless
`wasUserInput` is also true. This actually seems to be the intended
behavior according to the doc just above
ICoreService.triggerDataEvent().
2022-12-06 14:29:18 +11:00
Jörg Breitbart fd44d76262 api tests for uri and ranges 2022-12-05 22:49:25 +01:00
Jörg Breitbart 309ba47483 change batch size to 500 2022-12-05 20:03:25 +01:00
Jörg Breitbart 4084dc3949 remove _forceMemoryCleanup 2022-12-05 19:10:55 +01:00
Jörg Breitbart 42bacc3283 disabled faulty code in linkifier2 2022-12-05 18:30:25 +01:00
Jörg Breitbart 46bd97247d disabled faulty code in linkifier2 2022-12-05 18:28:42 +01:00
Jörg Breitbart 3bf72cdefc DOM renderer fix with much better performance 2022-12-05 15:50:29 +01:00
Jörg Breitbart de23616589 better line expansion 2022-12-05 13:45:47 +01:00
Jörg Breitbart 39c2e57ad3 hack around DOM renderer issue, fix api tests 2022-12-04 22:26:29 +01:00
Jörg Breitbart d19361ac85 initial rewrite of addon mechs 2022-12-04 19:53:36 +01:00
Jörg Breitbart 1f5159febe use IdleTaskQueue & Date.now; some testcases 2022-12-03 16:49:26 +01:00
Jörg Breitbart 3a2ae96960 Merge branch 'master' into buffer_optimizations 2022-12-03 11:24:51 +01:00
Daniel Imms ad1f2ac2a8 Merge pull request #4287 from xtermjs/dependabot/npm_and_yarn/minimatch-3.1.2
Bump minimatch from 3.0.4 to 3.1.2
2022-12-02 09:54:13 -08:00
dependabot[bot] b972e7f5ad Bump minimatch from 3.0.4 to 3.1.2
Bumps [minimatch](https://github.com/isaacs/minimatch) from 3.0.4 to 3.1.2.
- [Release notes](https://github.com/isaacs/minimatch/releases)
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.0.4...v3.1.2)

---
updated-dependencies:
- dependency-name: minimatch
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-12-02 17:45:59 +00:00
Daniel Imms 503fe99b0a Merge pull request #4286 from Tyriar/tyriar/mocha
Update mocha
2022-12-02 09:45:34 -08:00
Daniel Imms 953cf005c0 Update mocha
This resolves a security alert
2022-12-02 09:35:36 -08:00
Jason Lin d177acccee Fix a11y issues #4269 2022-12-02 18:35:09 +11:00
Daniel Imms 4451369c66 Merge pull request #4280 from Tyriar/4278
Add custom keymap example to attachCustomKeyEventHandler docs
2022-11-30 07:15:12 -08:00
Daniel Imms 6b09c1b576 Add custom keymap example to attachCustomKeyEventHandler docs
Fixes #4278
2022-11-30 07:06:28 -08:00
Daniel Imms 71eb514bd0 Merge pull request #4276 from bugwheels94/bugwheels94-add-dependent-to-readme
Added application that uses xterm.js
2022-11-28 13:04:48 -08:00
Ankit cd552500fc Update README.md 2022-11-29 02:23:56 +05:30
Daniel Imms ed1773c103 Merge pull request #4274 from JasonXJ/cap-atlas-pages
Cap webgl maxAtlasPages to 32
2022-11-26 07:24:47 -08:00
Jason Lin b7c119a071 Cap webgl maxAtlasPages to 32 2022-11-24 17:51:58 +11:00
Daniel Imms 452241ca92 Merge pull request #4271 from jeanp413/fix-vscode-166158
Fixes disappearance of characters in webgl renderer
2022-11-23 09:36:15 -08:00
Daniel Imms 1880399849 dirtyId -> version 2022-11-23 08:51:06 -08:00
Jean Pierre d4ee0309d9 Fixes https://github.com/microsoft/vscode/issues/166158 2022-11-22 00:07:23 -05:00
Daniel Imms a351f5758a Merge pull request #4255 from Tyriar/safari16
Enable the webgl renderer on safari 16 and above
2022-11-08 12:30:23 -08:00
Daniel Imms 3597d43788 Enable the webgl renderer on safari 16 and above
This does not fix the issue where webkit browsers cannot use it, I looked
at webkit versions in safari and was a bit confused. The wikipedia page
for safari shows v15 having webkit v612+ but I have safari v16 and it's
webkit v605, so trying to detect it based on webkit version is a risky
change.

Note that webgl2 shipped in safari 15, but it didn't work in webgl until
safari 16.

Fixes #3357
Related #3575
Related microsoft/vscode#165416
2022-11-08 12:23:20 -08:00
Daniel Imms f381783bd5 Merge pull request #4196 from silamon/ansiheadless
SerializeAsHTML(): Fallback to default ansi colors when running headless terminal
2022-11-04 08:15:59 -07:00
Daniel Imms 6969a94fe4 Merge branch 'master' into ansiheadless 2022-11-04 08:08:43 -07:00
Daniel Imms 2377472eca Merge pull request #4216 from JasonXJ/fix-csi-0-m
'CSI 0 m' should clear the extended attribute
2022-11-03 19:58:18 -07:00
Daniel Imms 0edabed595 Merge pull request #4240 from Tyriar/4222_shims
Remove promise and fetch shims and use await for fetch
2022-11-03 19:56:25 -07:00
Daniel Imms 7d98375ce4 Merge pull request #4254 from Tyriar/clear_green
Clear custom glyph test button sgr
2022-11-03 06:56:23 -07:00
Daniel Imms 862e83d7be Merge pull request #4252 from Tyriar/4243_merge
Continually merge texture atlas pages
2022-11-03 06:48:55 -07:00
Daniel Imms 7cf9b68e2c Clear custom glyph test button sgr 2022-11-03 06:48:33 -07:00
Daniel Imms 97a0df260b Remove unused event 2022-11-03 05:44:50 -07:00
Daniel Imms c66dc82286 Merge pull request #4251 from Tyriar/4250
Add 1024 character limit to web links addon
2022-11-02 06:56:22 -07:00
Daniel Imms 6c29864847 Add 1024 character limit to web links addon
Fixes #4250
2022-11-02 06:45:25 -07:00
Daniel Imms d82da195f3 Remove atlas pages from demo when they're merged 2022-11-02 06:02:53 -07:00
Daniel Imms abfe391d91 Add CJK with random SGR test button
This is good as a stress test for the renderer
2022-11-02 05:46:40 -07:00
Daniel Imms c22e684904 Fix exception when last 4 smallest pages are merged 2022-11-02 05:40:13 -07:00
Daniel Imms 6406fd79c1 Enforce a maximum page merge amount 2022-11-02 05:28:47 -07:00
Daniel Imms 0271fdf49d Merge 4 largest pages 2022-11-02 04:49:02 -07:00
Daniel Imms 805b41b80f Keep merging default texture size pages 2022-11-01 10:26:48 -07:00
Daniel Imms c9bacc80d7 Merge when max atlas pages is reached 2022-11-01 10:08:00 -07:00
Daniel Imms 54ee698ba6 Basic merging working
Part of #4243
2022-11-01 09:07:38 -07:00
Daniel Imms 295e3c7f0c Merge pull request #4249 from Tyriar/4241_fill
Remove fill polyfill
2022-10-31 12:34:17 -07:00
Daniel Imms d219da480c Remove fill polyfill
Fixes #4241
2022-10-31 12:24:59 -07:00
Daniel Imms 02fa38c334 Merge pull request #4244 from Tyriar/multi_page_atlas
Support multiple texture atlas pages
2022-10-30 18:30:11 -07:00
Daniel Imms 4d05905736 Polish 2022-10-30 18:11:24 -07:00
Jason Lin 81a3c80bc4 polish 2022-10-31 11:07:42 +11:00
Jason Lin f6fabebf8d polish 2022-10-31 11:07:42 +11:00
Jason Lin b735e7f1c4 polish 2022-10-31 11:07:42 +11:00
Jason Lin 839271096e Don't reset the url 2022-10-31 11:07:42 +11:00
Jason Lin 6a087e9896 'CSI 0 m' should clear the extended attribute
This change also removes `_currentLinkId` from `InputHandler` and uses
`_curAttrData.extended.urlId` directly so that we don't need to worry
about keep them in sync any more.
2022-10-31 11:07:42 +11:00
Daniel Imms dda3f5f545 Properly dispose of atlas event 2022-10-30 15:00:27 -07:00
Daniel Imms 04041d030f Generate bitmaps for all atlas pages 2022-10-30 14:56:27 -07:00
Daniel Imms 095ceda639 Show canvas atlas pages in demo 2022-10-30 14:53:01 -07:00
Daniel Imms 0aa9fc5bb8 Add border around atlas pages 2022-10-30 12:00:55 -07:00
Daniel Imms e092ca70e1 Simplify page reuse/creation 2022-10-30 10:33:38 -07:00
Daniel Imms 0592837620 Fill pages to capacity 2022-10-30 10:21:17 -07:00
Daniel Imms fc03a1698b Share page creation code 2022-10-30 10:16:48 -07:00
Daniel Imms c9ef4a8eb9 Increase texture size as it nears the maximum texture unit
#4243 would be a better, but more complicated, solution to this
2022-10-30 09:20:17 -07:00
Daniel Imms 846573bb01 Support up to maximum texture units (eg. 16 on my machine) 2022-10-30 03:16:29 -07:00
Daniel Imms 0f70055d76 Add CJK test button 2022-10-30 03:04:58 -07:00
Daniel Imms 3f2ed42e73 Fix glyphs getting clipped on bottom row 2022-10-29 20:57:42 -07:00
Daniel Imms 2a3c2d9520 Make better use of atlas space 2022-10-29 20:33:27 -07:00
Daniel Imms 5e95475d6b Use current row when filling in old atlas pages 2022-10-29 19:50:44 -07:00
Daniel Imms 57a65e0858 Continute filling atlas page after new pages are added 2022-10-29 19:44:12 -07:00
Daniel Imms bfa9dd5060 Clean up 2022-10-29 18:09:18 -07:00
Daniel Imms 04f79aa7b4 Share page texture binding code 2022-10-29 14:33:50 -07:00
Daniel Imms 86ccd709bf Avoid setting uniforms every frame 2022-10-29 14:26:51 -07:00
Daniel Imms c257a7247e Only upload textures that change 2022-10-29 13:27:33 -07:00
Daniel Imms 0a49a5dde1 Make texture atlas presentation nicer for multiple pages 2022-10-29 13:01:41 -07:00
Daniel Imms c6e3cc007c Stop atlas flickering on demo 2022-10-29 12:49:11 -07:00
Daniel Imms 713baeb64d Base atlas page size on device pixel ratio 2022-10-29 12:45:20 -07:00
Daniel Imms efba2c9040 Get pages working, add to demo 2022-10-28 19:30:42 -07:00
Daniel Imms 67ef455515 Get a second page kind of working 2022-10-28 19:20:26 -07:00
Daniel Imms 975d52fab9 Remove deprecated prop 2022-10-28 19:12:01 -07:00
Daniel Imms 6b37187b4d Create AtlasPage 2022-10-28 19:08:33 -07:00
Daniel Imms 84ab6a49c4 Bring texturePage partially to atlas 2022-10-28 18:56:49 -07:00
Daniel Imms 4ebc75aae0 Merge remote-tracking branch 'upstream/master' into multi_page_atlas 2022-10-28 18:42:46 -07:00
Daniel Imms 54335ef54a Create texture page attribute 2022-10-28 18:16:55 -07:00
Daniel Imms 43d8db77f9 Merge pull request #4239 from Tyriar/4224_mcr_canvas_ydisp
Correctly offset mcr check on canvas renderer
2022-10-28 14:10:25 -07:00
Daniel Imms 5a4a46f130 Remove promise and fetch shims and use await for fetch
Fixes #4222
2022-10-28 14:09:39 -07:00
Daniel Imms b0672aac09 Correctly offset mcr check on canvas renderer
Fixes #4224
2022-10-28 14:02:58 -07:00
Daniel Imms fcf378b172 Merge pull request #4237 from Tyriar/italic_bounds
Fix clipping of italic emoji
2022-10-28 09:12:07 -07:00
Daniel Imms 25551a4b98 Merge pull request #4236 from Tyriar/console_image
Add console.image helper
2022-10-28 09:04:37 -07:00
Daniel Imms 67b3faef01 Use bbox padding and width consistently 2022-10-28 09:01:22 -07:00
Daniel Imms 8b545a3fe4 Fix bbox right scan start offset
This causes particularly wide glyphs like italic emoji to get clipped
even if they're within the allowed padding.

Fixes #4229
2022-10-28 08:57:41 -07:00
Daniel Imms 4f4ce0146d Add console.image helper
This is useful for printing canvases that aren't attached to the DOM
2022-10-28 08:54:57 -07:00
Daniel Imms e7b347a2ef Merge pull request #4235 from Tyriar/opt_server_buffer
Avoid GC pressure from server data buffer
2022-10-27 22:41:07 -07:00
Daniel Imms 7908b7cf62 Avoid GC pressure from server data buffer 2022-10-27 22:30:46 -07:00
Daniel Imms 392f8e9c98 Merge pull request #4234 from Tyriar/slow_isFocused
Cache ICoreBrowserService.isFocused per task
2022-10-27 17:00:18 -07:00
Daniel Imms b5cd4f1492 Cache ICoreBrowserService.isFocused per task
This is called for every selected cell when rendering which ends up
consuming a bunch of CPU due to the DOM calls.
2022-10-27 16:50:45 -07:00
Daniel Imms 4edfc12cbf Merge pull request #4233 from Tyriar/4113_resize
Fix resize in demo
2022-10-27 14:03:17 -07:00
Daniel Imms 19c1ee56d3 Fix resize in demo
A double resize could happen due to the mismatch in canvas vs device
pixel widths

Fixes #4113
2022-10-27 13:55:24 -07:00
Daniel Imms a4f8b07a7f Merge pull request #4226 from Tyriar/dropped_frames
Fix dropped frame issue
2022-10-21 13:10:30 -07:00
Daniel Imms 8d4d5bd54b Fix dropped frame issue
This ensures decorations are refreshed in the same frame when called from
an animation frame callback

Fixes #4225
2022-10-21 13:02:24 -07:00
Daniel Imms d5710afcb3 Merge pull request #4219 from Tyriar/4218_safari_bitmap
Disable canvas ImageBitmap optimization on Safari
2022-10-19 10:58:43 -07:00
Daniel Imms 8bd8195e30 Disable canvas ImageBitmap optimization on Safari
Fixes #4218
2022-10-19 10:03:01 -07:00
Daniel Imms ffe066dfef Merge pull request #4217 from Nokel81/fix-dom-renderering-jsdom-v2
Change clearing innerText to using replaceChildren to fix testing using jsdom
2022-10-19 07:21:30 -07:00
Sebastian Malton 11e90591f9 Change clearing innerText to using replaceChildren to fix testing using jsdom
Signed-off-by: Sebastian Malton <sebastian@malton.name>
2022-10-19 10:09:12 -04:00
Megan Rogge 0b84d17b18 revert decoration dispose changes (#4215) 2022-10-18 21:32:22 +00:00
Daniel Imms 376f0d9972 Merge pull request #4211 from hackermondev/patch-1
replit branding change
2022-10-17 07:45:06 -07:00
hackermon 478535d236 replit branding change 2022-10-17 09:49:54 -04:00
Daniel Imms 3f12d2925a Merge pull request #4208 from Tyriar/todo_attach_socket
Warn/throw on unexpected attach addon socket state
2022-10-17 03:48:31 -07:00
Daniel Imms 8e65ee8832 Merge pull request #4210 from Tyriar/todo_type_tabs
Fix a bunch of TODOs
2022-10-16 07:15:19 -07:00
Daniel Imms a7f0c3868c Merge pull request #4209 from Tyriar/todo_onData_event_test
Re-enable onData event test
2022-10-16 07:08:59 -07:00
Daniel Imms 96486c414b Acquire char size service better in webgl renderer 2022-10-16 07:04:34 -07:00
Daniel Imms b560d85553 Remove unneeded params as link provider is stable 2022-10-16 07:04:24 -07:00
Daniel Imms 7796b4101a Type Buffer.tabs 2022-10-16 06:58:20 -07:00
Daniel Imms a982484392 Re-enable onData event test 2022-10-16 06:55:04 -07:00
Daniel Imms e15c6b9f3e Warn/throw on unexpected attach addon socket state 2022-10-16 06:49:27 -07:00
Daniel Imms 807183bba6 Merge pull request #4207 from Tyriar/3925_dimensions
Improve names of IRenderDimensions interface members
2022-10-16 06:41:37 -07:00
Daniel Imms dc19628014 Fix width -> height 2022-10-16 06:34:12 -07:00
Daniel Imms ca68a5c59a Fix typo in test 2022-10-15 20:13:24 -07:00
Daniel Imms 22e8a2f8e7 Remove old dimension properties 2022-10-15 18:07:05 -07:00
Daniel Imms 171ba446bd Update other files 2022-10-15 15:32:02 -07:00
Daniel Imms 89e0b87eac Move to device/css in domrenderer 2022-10-15 14:22:04 -07:00
Daniel Imms 2c9e711cae Move all other refs in webgl over 2022-10-15 13:55:36 -07:00
Daniel Imms b0f1c38fd2 Move all other refs in canvas over 2022-10-15 13:44:52 -07:00
Daniel Imms b393008b54 Use new render dimensions in canvasrenderer 2022-10-15 13:19:04 -07:00
Daniel Imms 65f41a151e Use new render dimensions in webglrenderer 2022-10-15 13:15:12 -07:00
Daniel Imms ddc4e75583 Add new dimensions properties, helper creation function
Part of #3925
2022-10-15 10:31:43 -07:00
Daniel Imms 869ce3aa35 Merge pull request #4194 from silamon/fixmissingline
Capture data between terminal creation and connected to terminal in demo
2022-10-15 10:17:51 -07:00
Daniel Imms 16ea621a5f Merge pull request #4206 from Tyriar/eslint-jsdoc
Add eslint-plugin-jsdoc with minimal comment rules
2022-10-15 08:55:07 -07:00
Daniel Imms 5204374494 Add no-multi-asterisks rule 2022-10-15 08:43:06 -07:00
Daniel Imms 9f1a71485c Fix invalid jsdoc tags 2022-10-15 08:41:49 -07:00
Daniel Imms 49e0480f78 Add rule jsdoc/check-alignment 2022-10-15 08:39:32 -07:00
Daniel Imms 114924f3fa Add eslint-plugin-jsdoc and enable check-param-names 2022-10-15 08:38:14 -07:00
Daniel Imms aeba140c75 Merge pull request #4204 from silamon/proposedApiCheck
Remove check proposed api checks
2022-10-15 08:04:12 -07:00
Daniel Imms aea7680f76 Merge pull request #4205 from Tyriar/triangle_strip
Draw quads with triangle strip
2022-10-15 08:02:07 -07:00
Daniel Imms fb4a4e6127 Draw quads with triangle strip
This reduces the number of needed vertices from 6 to 4, it may not have a
noticeable performance impact but it's less data and simpler
2022-10-15 07:54:31 -07:00
Simon Lamon cc82f605fe delete clean up 2022-10-15 10:47:59 +00:00
Simon Lamon a055948937 unsentOutput instead of logs and dispose temporary listener 2022-10-15 10:45:23 +00:00
Simon Lamon 8246440795 Merge branch 'master' into fixmissingline 2022-10-15 10:11:40 +00:00
Simon Lamon 073aa6d439 Fix test "Proposed API check" 2022-10-15 10:25:39 +02:00
Simon Lamon 6bbe5a82a8 Remove check proposed api checks 2022-10-15 07:28:21 +00:00
Daniel Imms 1e6dfec4ae Merge pull request #4201 from Tyriar/4197_2
Remove clipImageData completely
2022-10-13 06:34:37 -07:00
Daniel Imms 5285b9ac45 Merge branch 'master' into 4197_2 2022-10-13 06:25:59 -07:00
Daniel Imms 50d1834871 Remove clipImageData completely
putImageData works a little differently to drawImage which confused me
for a bit, you need to offset the destination as putImageData draws the
'dirty' parts of the texture using the same source image dimensions

Fixes #4197
2022-10-13 06:25:13 -07:00
Daniel Imms b1f785879b Merge pull request #4200 from Tyriar/4197
Optimize _clipImageData
2022-10-12 10:24:28 -07:00
Daniel Imms 28fcffe46c Optimize _clipImageData
This makes clip image data around twice as fast, it's a little tricky to
measure it though. Changes:

- Work on Uint32Array instead of Uint8 so it's 1 assignment per pixel
  instead of 1 per channel.
- Perform the clipping in-place using the original image data to avoid
  allocation a new buffer.

Fixes #4197
2022-10-12 10:16:26 -07:00
Jörg Breitbart 14b99dca76 Merge branch 'master' into buffer_optimizations 2022-10-09 21:41:06 +02:00
Daniel Imms 1b13127f0b Merge pull request #4195 from Tyriar/4190
Create onSpecificOptionChange and onMultipleOptionChange helpers
2022-10-09 11:04:59 -07:00
Daniel Imms a37ea14d10 Use options service in renderers 2022-10-09 10:57:49 -07:00
Simon Lamon f8a5d3a91e Fallback to default_ansi_colors when running headless terminal 2022-10-09 17:16:06 +00:00
Daniel Imms 8d7137417b Split renderer safe vs unsafe api on activate 2022-10-09 10:12:19 -07:00
Daniel Imms e47296c1b9 Use options service directly in renderers 2022-10-09 09:28:18 -07:00
Daniel Imms 96b26b2e4a Merge pull request #4193 from silamon/fixdemoagain
Fix demo again
2022-10-09 09:23:07 -07:00
Daniel Imms 8ac88bc6bf Move Terminal option updates into owning components 2022-10-09 09:07:01 -07:00
Daniel Imms 7eead44048 Add onMultipleOptionChange 2022-10-09 08:14:56 -07:00
Daniel Imms 49a0904857 Move onSpecificOptionChange adoption 2022-10-09 08:07:43 -07:00
Simon Lamon a4c62792df revert formatting changes 2022-10-09 15:03:54 +00:00
Simon Lamon 1c345bef48 Handle onWillFire if it fails, so it loads dom renderer 2022-10-09 15:01:15 +00:00
Daniel Imms 415100e8b8 Adopt onSpecificOptionChange 2022-10-09 07:46:10 -07:00
Simon Lamon 627410a537 Add broken demo to investigate 2022-10-09 14:26:08 +00:00
Daniel Imms cad9c4164c onOptionChange/onSpecificOptionChange tests 2022-10-09 07:16:56 -07:00
Daniel Imms a87b6c3f2e Implement IOptionsService.onSpecificOptionChange
Part of #4190
2022-10-09 07:09:35 -07:00
Daniel Imms a2a855d747 Merge pull request #4192 from silamon/ubuntudeprecation
Switch to Ubuntu 20.04
2022-10-09 06:25:11 -07:00
Simon Lamon fb91b01217 Adds logs again in server.js 2022-10-09 12:16:26 +00:00
Simon Lamon bd661fdb07 Fix demo again 2022-10-09 11:29:33 +00:00
Simon Lamon f983b3cac1 Ubuntu 18.04 deprecation 2022-10-09 10:15:13 +00:00
Daniel Imms 0ba12301a6 Merge pull request #4188 from Tyriar/4184
Create theme service
2022-10-08 15:51:44 -07:00
Daniel Imms 97e084aa50 Remove document from test 2022-10-08 15:44:21 -07:00
Daniel Imms e0fb3b4d19 Remove ColorManager 2022-10-08 15:23:29 -07:00
Daniel Imms a1050d1a11 Merge ColorManager into ThemeService 2022-10-08 15:20:15 -07:00
Daniel Imms 7f9e3b1225 Remove resolved todo 2022-10-08 14:09:00 -07:00
Daniel Imms 48f15cb543 Remove remaining setColors methods 2022-10-08 14:07:28 -07:00
Daniel Imms 6e35bcd767 Use theme service in more places 2022-10-08 13:55:23 -07:00
Daniel Imms 80e474c6ad Viewport listen to theme service 2022-10-08 13:31:31 -07:00
Daniel Imms 4dee0c5d4b Build out theme service more 2022-10-08 12:15:21 -07:00
Daniel Imms a22ad76bd8 Merge pull request #4189 from Tyriar/ensure_canvas_changed
Ensure stale bitmap is not used when drawing new characters
2022-10-08 12:04:27 -07:00
Daniel Imms 4f5154fd21 Ensure stale bitmap is not used when drawing new characters
This would cause a problem where the first time a glyph is used it would
not be visible because it was using the old invalidated bitmap.
2022-10-08 11:56:29 -07:00
Daniel Imms cc6def57e4 Use readonly color set in canvas addon 2022-10-08 11:46:41 -07:00
Daniel Imms c4750eaf77 Create basic theme service
Part of #4184
2022-10-08 11:43:19 -07:00
Daniel Imms 8db2244f5d Merge pull request #4187 from Tyriar/4183_on_naming
Lint rule for on=event emitter and rename all methods with on prefix to handle
2022-10-08 11:14:15 -07:00
Daniel Imms 5d8e4a0af4 Add lint rule to ensure _on prefix is only for event emitters 2022-10-08 11:05:52 -07:00
Daniel Imms eecf52e949 Add lint rule to ensure on prefix is only for event emitters
Fixes #4183
2022-10-08 10:57:31 -07:00
Daniel Imms d8c31d7ad0 Merge pull request #4185 from Tyriar/4167_register_emitter
Fix a bunch of memory retention problems
2022-10-08 10:35:04 -07:00
Daniel Imms d8cf3af555 Fix Marker.dispose 2022-10-08 10:23:19 -07:00
Daniel Imms d3976c05f1 Fix marker dispose unit tests 2022-10-08 09:27:58 -07:00
Daniel Imms 94c3d11239 Fix compile 2022-10-08 09:11:28 -07:00
Daniel Imms 00f6d3a88c Register all event emitters, make some services disposable 2022-10-08 09:00:14 -07:00
Daniel Imms f61d8c91a3 Properly register disposables in SearchAddon 2022-10-08 08:30:39 -07:00
Daniel Imms ac659cb412 Prevent overriding of Disposable.dispose
A common mistake made was overriding dispose and not calling super.dispose,
so a TS trick is now used to move it to a readonly property instead of a
member method which prevents it being overridden.
2022-10-08 08:27:10 -07:00
Daniel Imms 34475f24c3 Release canvas texture atlas 2022-10-08 08:11:28 -07:00
Daniel Imms 8116e035b4 Register webgl renderers 2022-10-08 08:06:43 -07:00
Daniel Imms 56b37a73ac Free WebglAddon 2022-10-08 07:37:55 -07:00
Daniel Imms fc8043a25a Ensure webgl texture atlas is released on dispose 2022-10-08 07:32:51 -07:00
Daniel Imms f88c0e7480 Polish emitter registering in canvas renderer
Part of #4167
2022-10-08 07:24:35 -07:00
Daniel Imms d948b31304 Merge pull request #4181 from Tyriar/clear_race
Fix race condition that could when clearing atlas on 2+ terminals
2022-10-07 16:34:27 -07:00
Daniel Imms 9a4d5dbe26 Merge pull request #4182 from Tyriar/delete_canvas_atlas
Remove the canvas char atlas
2022-10-07 16:32:51 -07:00
Daniel Imms 8e7093f03b Remove the canvas char atlas
This was unused and removing from cache was broken apparently
2022-10-07 16:20:01 -07:00
Daniel ImmsandMegan Rogge c4eef2f470 Fix race condition that could when clearing atlas on 2+ terminals
This is the proper fix for microsoft/vscode#162996, after a bunch of
investigation we found that this is causing the problem:

VS Code clears the texture atlas on OS resume to fix an issue where the texture
could be corrupted. This calls clearTextureAtlas twice which should be fine, but
it's not because inside WebglRenderer.clear we're calling updateModel which is
meant to only ever be called in an animation frame. This calls potentially both
renderers to have stale views of the texture atlas for those rows and our perf
caching of cells (the main purpose of the model) doesn't know it's stale.

Co-Authored-By: Megan Rogge <merogge@microsoft.com>
2022-10-07 16:17:11 -07:00
Daniel Imms 5fcb084089 Merge pull request #4180 from Tyriar/clear_atlas
Fix clearTextureAtlas call as implemented on IRenderer
2022-10-07 14:31:28 -07:00
Daniel Imms e89c507d06 Fix clearTextureAtlas call as implemented on IRenderer
This is the root cause of microsoft/vscode#162996
2022-10-07 14:22:51 -07:00
Daniel Imms eb3de3ebd5 Merge pull request #4151 from Tyriar/renderer_before_open
Allow renderer to be set before Terminal.open is called
2022-10-07 10:17:54 -07:00
Daniel Imms ffa4d921bb Merge remote-tracking branch 'upstream/master' into renderer_before_open 2022-10-07 10:10:24 -07:00
Daniel Imms a060c64a08 Merge pull request #4179 from Tyriar/4178
Split Utf8ToUtf32 decoder unit tests up
2022-10-06 10:38:03 -07:00
Daniel Imms 0d5a526e86 Split Utf8ToUtf32 decoder unit tests up
Fixes #4178
2022-10-06 10:28:54 -07:00
Daniel Imms 3ec08948ec Merge pull request #4177 from Tyriar/tyriar/revert_events
Revert "Merge pull request #4166 from Tyriar/event_with_emitter"
2022-10-06 09:59:50 -07:00
Daniel Imms 4d97729010 Revert "Merge pull request #4166 from Tyriar/event_with_emitter"
This reverts commit d22f7c9edd, reversing
changes made to 1f8e6f0952.
2022-10-06 09:48:54 -07:00
Daniel Imms 5f1651ffbc Merge pull request #4176 from xtermjs/Tyriar-patch-1
Bump copyright year
2022-10-06 09:15:02 -07:00
Daniel Imms ab0082b73a Bump copyright year 2022-10-06 09:14:50 -07:00
Daniel Imms aa55d1bde6 Merge pull request #4170 from Tyriar/shared_cache
Share texture atlas cache code between webgl and canvas renderers
2022-10-05 10:19:17 -07:00
Daniel Imms e0324e32a8 Bring back ImageBitmap optimization in canvas renderer
It's more simple than it used to be, it will clear and debounce all
glyphs when any change is made.
2022-10-04 15:24:10 -07:00
Daniel Imms 3b260a2193 Fix resolved todo 2022-10-04 14:46:00 -07:00
Daniel Imms 622acaaad7 Remove old selection start/end values 2022-10-04 14:45:19 -07:00
Daniel Imms 0d7e18d236 Add CellColorResolver to share load color code 2022-10-04 14:41:27 -07:00
Daniel Imms 83c09c34db Share selection render model code 2022-10-04 14:23:09 -07:00
Daniel Imms 03e3105c55 Fix fg/bg overrides in canvas 2022-10-04 14:10:27 -07:00
Daniel Imms c5d015c908 Merge branch 'master' into shared_cache 2022-10-04 13:54:14 -07:00
Daniel Imms e1e8f28d97 Merge pull request #4118 from coderaiser/feature/lint-by-putout
Lint using 🐊Putout: part 7
2022-10-04 10:45:24 -07:00
Daniel Imms f6a61acc86 Merge branch 'master' into feature/lint-by-putout 2022-10-04 09:52:10 -07:00
Daniel Imms 6316eee672 Comment out debug log 2022-10-02 21:32:24 -07:00
Daniel Imms 9259ec258f Fix combined chars in canvas 2022-10-02 14:27:43 -07:00
Daniel Imms c741c13409 Add ps to output string 2022-10-02 14:25:08 -07:00
Daniel Imms f2ff3a6a7a Add sgr attr test 2022-10-02 14:21:06 -07:00
Daniel Imms dbef850039 Hook up attributes to new texture atlas 2022-10-02 13:57:35 -07:00
Daniel Imms 590a8219e3 Merge remote-tracking branch 'upstream/master' into shared_cache 2022-10-02 13:42:44 -07:00
Daniel Imms d22f7c9edd Merge pull request #4166 from Tyriar/event_with_emitter
Create new event with emitter object to simplify code
2022-10-02 13:41:37 -07:00
Daniel Imms c2bf87ee34 Initial working shared atlas 2022-10-02 11:38:46 -07:00
Daniel Imms 0bc5ca8f80 Fix imports 2022-10-02 11:08:06 -07:00
Daniel Imms 5b785d29a1 Expose _listeners for debug/test 2022-10-02 09:09:05 -07:00
Daniel Imms f38a49b4d5 Move remaining webgl atlas to shared 2022-10-02 08:48:55 -07:00
Daniel Imms 26c974f9b6 Shuffle some utils 2022-10-02 08:37:13 -07:00
Daniel Imms 333b0e28ac Add ITextureAtlas and use that instead of WebglCharAtlas 2022-10-02 07:48:43 -07:00
Daniel Imms 0bc71156bf Merge remote-tracking branch 'upstream/master' into shared_cache 2022-10-02 07:26:23 -07:00
Daniel Imms dece3cc41c DynamicCharAtlas -> CanvasCharAtlas 2022-10-02 07:20:55 -07:00
Daniel Imms 8752c4fa8e Remove canvas char atlas base class 2022-10-02 07:19:57 -07:00
Daniel Imms 53d0297898 Lint client.ts 2022-10-02 07:15:12 -07:00
Daniel Imms af6f03600f Show canvas texture atlas on demo 2022-10-02 07:10:37 -07:00
Daniel Imms 1f8e6f0952 Merge pull request #4169 from Tyriar/canvas_freq
Add willReadFrequently to canvas renderer too
2022-10-01 21:46:13 -07:00
Daniel Imms f087bd121e Add willReadFrequently to canvas renderer too 2022-10-01 21:38:01 -07:00
Daniel Imms d669c6e9d6 Fix remaining references 2022-10-01 18:03:43 -07:00
Daniel Imms c5f9eacbb1 Move other render parts to shared folder 2022-10-01 17:41:16 -07:00
Daniel Imms 6fcc496547 Move cache utils to core shared/ folder
Part of #4065
2022-10-01 17:36:16 -07:00
Daniel Imms 5dee66353a Merge pull request #4168 from Tyriar/4128
Ensure texture atlas comparison uses rgba not object
2022-10-01 10:30:17 -07:00
Daniel Imms 2a602ef231 Ensure texture atlas comparison uses rgba not object
Fixes #4128
2022-10-01 10:20:37 -07:00
Daniel Imms e3e3c347ea Merge pull request #4160 from jerch/tweak_serverside_buffering
tweak buffering in server.js
2022-10-01 09:59:01 -07:00
Daniel Imms 2e8b793ba0 Format files 2022-10-01 08:56:56 -07:00
Daniel Imms 03fcf60aaf Docs 2022-10-01 08:55:01 -07:00
Daniel Imms ed15a8e170 Create new event with emitter object to simplify code 2022-10-01 08:45:40 -07:00
Daniel Imms 969db2bcf9 Merge pull request #4163 from Tyriar/4147
Inline dirty row service into input handler
2022-10-01 08:28:29 -07:00
Daniel Imms 8e378c07e9 Merge pull request #4165 from Tyriar/4164
Define all events and emitters consistently
2022-10-01 08:28:18 -07:00
Daniel Imms 1a22c44f2b Make all emitters readonly 2022-10-01 08:02:32 -07:00
Daniel Imms cf9949840b Use readonly over getter for exposing events
Fixes #4164
2022-10-01 07:52:26 -07:00
Daniel Imms 3a90f7e7c0 Inline dirty row service into input handler
Fixes #4147
2022-10-01 07:39:02 -07:00
Daniel Imms 035025dba3 Merge pull request #4162 from Tyriar/w_refactor
Move w objects to $ prefix variables
2022-10-01 07:33:45 -07:00
Daniel Imms 52ed65d7bd Move w objects to $ prefix variables
Fixes #4138
2022-10-01 07:26:07 -07:00
Jörg Breitbart 150499b7e5 comment fix 2022-09-29 10:46:25 +02:00
Jörg Breitbart 193d305dcd revert timeout to 5ms, remove log in server.js 2022-09-29 10:36:08 +02:00
Jörg Breitbart 2236e230fc Merge branch 'master' into tweak_serverside_buffering 2022-09-28 20:06:01 +02:00
Jörg Breitbart cd6f370c28 unbuffered delivery for userInput response only 2022-09-28 20:03:55 +02:00
Daniel Imms 2659de2291 Merge pull request #4159 from Tyriar/4158
Use microtask to flush the write buffer after input
2022-09-28 10:46:15 -07:00
Daniel Imms 34df17e36d Call innerWrite immediately over a microtask 2022-09-28 09:51:28 -07:00
Daniel Imms 44388e5e6c Use microtask to flush the write buffer after input
Fixes #4158
2022-09-28 09:06:07 -07:00
Jörg Breitbart 785b932d75 lazy cleanup on buffer 2022-09-28 15:57:58 +02:00
Jörg Breitbart e58f73dc82 Merge branch 'master' into buffer_optimizations 2022-09-28 13:22:16 +02:00
Jörg Breitbart 7bba07e24e rename fourByteCells to uint32Cells 2022-09-28 13:18:31 +02:00
Jörg Breitbart 39eea8ed88 remove useless condition 2022-09-28 13:15:34 +02:00
Jörg Breitbart 20c1f1a5f7 fix resize edge cases: always report cleanup state 2022-09-28 12:14:09 +02:00
Jörg Breitbart e87f70d6d6 removed EMPTY_DATA branch 2022-09-28 11:55:26 +02:00
Daniel Imms 51c580e7a4 Merge pull request #4157 from Tyriar/4156
Fix trusted types in dom renderer
2022-09-27 13:15:25 -07:00
Daniel Imms 23328a394b Fix trusted types in dom renderer
Fixes #4156
2022-09-27 13:01:36 -07:00
Daniel Imms 399621eeb5 Merge pull request #4154 from Tyriar/qmt
Remove queuemicrotask polyfill
2022-09-27 07:41:57 -07:00
Daniel Imms 66675ef649 Merge pull request #4153 from Tyriar/microtask_fix
Fix repetitive microtask problem
2022-09-27 06:57:00 -07:00
Daniel Imms c6df208a3e Merge pull request #4155 from Tyriar/fix_priority_queue
Fix priority task queue and idle callback fallback
2022-09-27 06:56:52 -07:00
Daniel Imms 8201925fe8 Fix priority task queue and idle callback fallback
Mistake of not hooking up the abstract methods, this should throw on
Safari for example.
2022-09-27 06:46:19 -07:00
Daniel Imms d74e43a42a Remove queuemicrotask polyfill
Support seems good
2022-09-27 06:44:18 -07:00
Daniel Imms 25b69e5ed0 Fix repetitive microtask problem
See #4145
2022-09-27 06:43:07 -07:00
Daniel Imms 37c722d556 Merge pull request #4152 from dstein64/master
Move real-world uses section down
2022-09-26 08:27:21 -07:00
Daniel Steinberg 2b597a82a1 Move real-world uses section down 2022-09-26 11:19:44 -04:00
Daniel Imms 318abe641b Merge pull request #4145 from Tyriar/microtask_server
Optimize critical I/O path for input latency
2022-09-25 09:39:02 -07:00
Daniel Imms eca9c721d4 Merge pull request #4144 from Tyriar/priority_queue
Add PriorityTaskQueue
2022-09-25 09:38:44 -07:00
Daniel Imms 0b8120808c Prefer microtask to timeout in write queue
This seems to make little difference but it's more correct as we want
to write as fast as possible to minimize input latency
2022-09-24 16:27:56 -07:00
Daniel Imms c6e4788344 Use microtasks on server
This reduces demo input latency on my mac approx 10ms -> 4ms
2022-09-24 16:23:14 -07:00
Daniel Imms c548977a3e Fix imports 2022-09-24 16:16:14 -07:00
Daniel Imms 04d196a32d Use task queue interface 2022-09-24 16:14:38 -07:00
Daniel Imms 494fe3a95d Fill in ITaskQueue 2022-09-24 16:13:53 -07:00
Daniel Imms 8f8813a19a Add PriorityTaskQueue 2022-09-24 15:16:02 -07:00
Daniel Imms df8a57d14b Merge pull request #4143 from Tyriar/idle_deadline
Use the browser's IdleDeadline to determine max task time
2022-09-24 15:00:02 -07:00
Daniel Imms 868fc7ec29 Allow loading canvas before open 2022-09-24 14:44:13 -07:00
Daniel Imms 2febe19222 Set renderer after screen element is attached to dom 2022-09-24 14:42:57 -07:00
Daniel Imms 693d09cbad Allow setting renderer addon before open is called 2022-09-24 14:41:48 -07:00
Daniel Imms ab90dcd0dc Clarify comment 2022-09-24 14:13:04 -07:00
Daniel Imms 1f45d5344c Use the browser's IdleDeadline to determine max task time 2022-09-24 14:10:29 -07:00
Daniel Imms a9eea3fd46 Merge pull request #4142 from Tyriar/defer_paused_resize
Defer paused renderer resize to idle callback
2022-09-24 12:35:40 -07:00
Daniel Imms aec1f2b2be Fix import 2022-09-24 11:58:17 -07:00
Daniel Imms 66efb0388a Move prop to top 2022-09-24 11:57:12 -07:00
Daniel Imms 4ac5a91c95 Make DebouncedIdleTask helper 2022-09-24 11:56:46 -07:00
Daniel Imms 4e8e6b9788 Defer paused renderer resize to idle callback 2022-09-24 11:45:48 -07:00
Daniel Imms 275a8a91ee Merge pull request #4141 from Tyriar/idle_task_queue
Do char atlas warmup via new IdleTaskQueue
2022-09-24 11:44:10 -07:00
Daniel Imms 270ac1c7d9 Docs 2022-09-24 11:17:39 -07:00
Daniel Imms c42f290eb8 Do char atlas warmup via new IdleTaskQueue 2022-09-24 11:10:54 -07:00
Daniel Imms 8b323bbf69 Merge pull request #4140 from Tyriar/fast_setTheme
Share rgba vars throughout Color.ts, fast setTheme parseColor
2022-09-24 11:09:51 -07:00
Daniel Imms e68bf3ca5b Merge remote-tracking branch 'ups/master' into fast_setTheme 2022-09-24 11:02:15 -07:00
Daniel Imms b742da4bfa Merge pull request #4137 from Tyriar/willReadFrequently
Optimize contexts for reading frequently
2022-09-24 10:40:39 -07:00
Daniel Imms 6368cb4cff Move canvas-based parsing to Color.ts lib
This removed the allowTransparency code, it didn't actually do much
before, just whether the 'advanced parsing' would allow transparency.
Since we already do the common formats, I'm not even sure what other
format it could be.
2022-09-24 10:37:40 -07:00
Daniel Imms 81d68b115c Share rgba vars throughout Color.ts, fast setTheme parseColor 2022-09-24 10:00:57 -07:00
Daniel Imms d62b4531c3 Optimize contexts for reading frequently
This was a recommendation from Edge.
Docs here: https://html.spec.whatwg.org/multipage/canvas.html#concept-canvas-will-read-frequently
2022-09-24 06:58:01 -07:00
Daniel Imms 1431210ecd Merge pull request #4125 from BattlefieldDuck/master
Update allowProposedApi comments
2022-09-23 13:27:25 -07:00
Daniel Imms 50b1e1045c Merge pull request #4134 from Tyriar/fix_upper_one_eighth_block
Fix width of upper 1/8 block char
2022-09-23 10:33:19 -07:00
Daniel Imms 09a5da5d33 Fix width of upper 1/8 block char
See microsoft/vscode#161323
2022-09-23 10:11:28 -07:00
Daniel Imms 349acbba34 Merge pull request #4131 from Tyriar/idleWarmup
Perform texture atlas warm up in an idle callback
2022-09-22 13:48:56 -07:00
Daniel Imms 78128762f9 Perform texture atlas warm up in an idle callback
Part of #4103
2022-09-22 13:06:08 -07:00
coderaiser 497cfcb74c Merge branch 'master' into feature/lint-by-putout 2022-09-19 12:00:14 +03:00
TatLead 11c806a4d0 Update xterm.d.ts 2022-09-19 05:54:11 +08:00
TatLead 9fb84cb89d Update xterm-headless.d.ts 2022-09-19 05:53:50 +08:00
Daniel Imms 1ceec0b6fa Merge pull request #4123 from Tyriar/remove_v5_publishing
Remove special v5 beta publishing
2022-09-15 13:02:17 -07:00
coderaiser bbff3e8396 Update src/common/services/InstantiationService.ts 2022-09-15 22:52:50 +03:00
Daniel Imms 73cb96ddec Remove special v5 beta publishing 2022-09-15 12:39:32 -07:00
coderaiser 25eb7d6472 chore: lint using 🐊Putout 2022-09-15 17:19:34 +03:00
Jörg Breitbart 6225cc2b9c optimize certain resize conditions 2022-09-13 14:37:29 +02:00
149 changed files with 5256 additions and 5057 deletions
+19 -2
View File
@@ -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",
+3 -3
View File
@@ -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
+23 -22
View File
@@ -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 -1
View File
@@ -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/"
+18 -4
View File
@@ -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 {
+207 -377
View File
@@ -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;
}
}
}
}
+45 -30
View File
@@ -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;
}));
}
}
+69 -89
View File
@@ -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];
+22 -104
View File
@@ -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
View File
@@ -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
View File
@@ -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();
/**
+5 -3
View File
@@ -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;
}
+11 -1
View File
@@ -13,10 +13,20 @@
"strict": true,
"types": [
"../../../node_modules/@types/mocha"
]
],
"paths": {
"browser/*": [
"../../../src/browser/*"
]
}
},
"include": [
"./**/*",
"../../../typings/xterm.d.ts"
],
"references": [
{
"path": "../../../src/browser"
}
]
}
+20 -20
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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 -1
View File
@@ -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/"
+135 -93
View File
@@ -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];
+6 -24
View File
@@ -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
View File
@@ -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;
+61 -39
View File
@@ -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();
}
}
+147 -306
View File
@@ -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;
+9 -4
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -9,7 +9,7 @@ trigger:
jobs:
- job: Linux
pool:
vmImage: 'ubuntu-18.04'
vmImage: 'ubuntu-20.04'
steps:
- task: NodeTool@0
inputs:
+2 -7
View File
@@ -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`;
-1
View File
@@ -146,7 +146,6 @@
left: 0;
top: 0;
bottom: 0;
right: 0;
z-index: 10;
color: transparent;
}
+339 -88
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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",
+26 -24
View File
@@ -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 {
-6
View File
@@ -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);
-366
View File
@@ -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);
});
});
});
-288
View File
@@ -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])
};
}
}
+3
View File
@@ -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
View File
@@ -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);
}
/**
+25 -9
View File
@@ -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) {
+4 -6
View File
@@ -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;
+5 -6
View File
@@ -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
View File
@@ -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;
}
+66 -45
View File
@@ -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;
}
+6 -9
View File
@@ -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
View File
@@ -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'],
+4 -4
View File
@@ -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';
+7 -3
View File
@@ -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
+9 -9
View File
@@ -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);
-3
View File
@@ -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 {
-56
View File
@@ -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;
}
+97 -97
View File
@@ -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 = '&nbsp;';
charElement.textContent = '\xa0'; // = &nbsp;
}
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;
}
}
@@ -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]
};
@@ -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 {
@@ -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)
));
+1
View File
@@ -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();
}
@@ -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
View File
@@ -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;
}
+3 -3
View File
@@ -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);
});
+2 -2
View File
@@ -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;
+8 -4
View File
@@ -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 {
+10 -2
View File
@@ -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;
}
}
+6 -6
View File
@@ -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])
};
+92 -49
View File
@@ -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);
});
+42 -41
View File
@@ -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
+29 -13
View File
@@ -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;
}
+367
View File
@@ -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);
});
});
});
+235
View File
@@ -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