Compare commits

..

21 Commits

Author SHA1 Message Date
Dustin L. Howett
7649fc8a06 Migrate spelling-0.0.21 changes from main 2020-03-30 09:39:17 -07:00
Dustin L. Howett
343ff0913d Migrate spelling-0.0.19 changes from main 2020-03-30 09:39:17 -07:00
Michael Niksa
39d67e3859 Merge branch 'master' into dev/miniksa/dx_bitmap 2020-03-30 09:39:17 -07:00
Mike Griese
1ae4252a7b Make conechokey use ReadConsoleInputW by default (#5148)
This PR updates our internal tool `conechokey` to use `ReadConsoleInputW` by default. It also adds a flag `-a` to force it to use `ReadConsoleInputA`.

I discovered this while digging around for #1503, but figured I'd get this checked in now while I'm still investigating.

Since this is just a helper tool, I spent as little effort writing this change - yea the whole tool could benefit from cleaner code but _ain't nobody got time for that_.
2020-03-30 16:06:27 +00:00
pi1024e
2872f147f8 Remove unneeded whitespace (#5162)
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? -->
## Summary of the Pull Request
Every single time a PR is run, there are a bunch of warnings about whitespace in the .cs files, so I ran the code format on those files, without changing their contents, so it won't be flagged anymore.
<!-- Other than the issue solved, is this relevant to any other issues/existing PRs? --> 
## References

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist
* [X] CLA signed. If not, go over [here](https://cla.opensource.microsoft.com/microsoft/Terminal) and sign the CLA
* [X] Tests added/passed

<!-- Provide a more detailed description of the PR, other things fixed or any additional comments/features here -->
## Detailed Description of the Pull Request / Additional comments

<!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well -->
## Validation Steps Performed
Ran the code-format utility on the .cs files
2020-03-30 14:33:32 +00:00
Carlos Zamora
28d108bf32 Set DxRenderer non-text alias mode (#5149)
There are two antialias modes that can be set on the ID2D1RenderTarget:
- one for text/glyph drawing [1]
- one for everything else [2]
We had to configure that in the RenderTarget.

Additionally, when clipping the background color rect, we need to make
sure that's aliased too. [3]

## References
[1] ID2D1RenderTarget::SetTextAntialiasMode
    https://docs.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-id2d1rendertarget-settextantialiasmode
[2] ID2D1RenderTarget::SetAntialiasMode
    https://docs.microsoft.com/en-us/windows/win32/api/d2d1/nf-d2d1-id2d1rendertarget-setantialiasmode)
[3] ID2D1CommandSink::PushAxisAlignedClip
    https://docs.microsoft.com/en-us/windows/win32/api/d2d1_1/nf-d2d1_1-id2d1commandsink-pushaxisalignedclip)

## Validation
Open and interact with midnight commander with the display scaling set
to...
- 100%
- 125%
- 150%
- 175%
- 200%

Closes #3626
2020-03-28 00:15:50 +00:00
Chester Liu
91b84f185e doc: add Git Bash(WOW64) to ThirdPartyToolProfiles (#5141) 2020-03-27 16:18:10 -07:00
Dustin L. Howett (MSFT)
dfc15780c7 add til::math, use it for float conversions to point, size (#5150)
This pull request introduces the `til::math` namespace, which provides some casting functions to be used in support of `til::point` and `til::size`. When point/size want to ingest a floating-point structure, they _must_ be instructed on how to convert those floating-point values into integers.

This enables:

```
Windows::Foundation::Point wfPoint = /* ... */;
til::point tp{ til::math::rounding, wfPoint };
```

Future thoughts: should the TilMath types be stackable? Right now, you cannot get "checked + rounding" behavior (where it throws if it doesn't fit) so everything is saturating.

## PR Checklist
* [x] Closes a request by Michael
* [x] I've discussed this with core contributors already
2020-03-27 22:48:49 +00:00
Michael Niksa
ef80f665d3 Correct scrolling invalidation region for tmux in pty w/ bitmap (#5122)
Correct scrolling invalidation region for tmux in pty w/ bitmap

Add tracing for circling and scrolling operations. Fix improper
invalidation within AdjustCursorPosition routine in the subsection about
scrolling down at the bottom with a set of margins enabled.

## References
- Introduced with #5024 

## Detailed Description of the Pull Request / Additional comments
- This occurs when there is a scroll region restriction applied and a
  newline operation is performed to attempt to spin the contents of just
  the scroll region. This is a frequent behavior of tmux.
- Right now, the Terminal doesn't support any sort of "scroll content"
  operation, so what happens here generally speaking is that the PTY in
  the ConHost will repaint everything when this happens.
- The PTY when doing `AdjustCursorPosition` with a scroll region
  restriction would do the following things:

1. Slide literally everything in the direction it needed to go to take
   advantage of rotating the circular buffer. (This would force a
   repaint in PTY as the PTY always forces repaint when the buffer
   circles.)
2. Copy the lines that weren't supposed to move back to where they were
   supposed to go.
3. Backfill the "revealed" region that encompasses what was supposed to
   be the newline.

- The invalidations for the three operations above were:

1. Invalidate the number of rows of the delta at the top of the buffer
   (this part was wrong)
2. Invalidate the lines that got copied back into position (probably
   unnecessary, but OK)
3. Invalidate the revealed/filled-with-spaces line (this is good).

- When we were using a simple single rectangle for invalidation, the
  union of the top row of the buffer from 1 and the bottom row of the
  buffer from 2 (and 3 was irrelevant as it was already unioned it)
  resulted in repainting the entire buffer and all was good.

- When we switched to a bitmap, it dutifully only repainted the top line
  and the bottom two lines as the middle ones weren't a consequence of
  intersect.

- The logic was wrong. We shouldn't be invalidating rows-from-the-top
  for the amount of the delta. The 1 part should be invalidating
  everything BUT the lines that were invalidated in parts 2 and 3.
  (Arguably part 2 shouldn't be happening at all, but I'm not optimizing
  for that right now.)

- So this solves it by restoring an entire screen repaint for this sort
  of slide data operation by giving the correct number of invalidated
  lines to the bitmap.

## Validation Steps Performed
- Manual validation with the steps described in #5104
- Automatic test `ConptyRoundtripTests::ScrollWithMargins`.

Closes #5104
2020-03-27 22:37:23 +00:00
Dustin L. Howett (MSFT)
d7123d571b Convert the About and Close All Tabs dialogs to xaml (#5140)
## Summary of the Pull Request

This pull request replaces about a hundred lines of manual xaml DOM code with a few lines of actual xaml, and wires up bound properties and event handlers in the good and correct way.

As part of this change, I've replaced the giant TextBlock in the about dialog with StackPanels, and replaced the Hyperlinks with HyperlinkButtons. This is in line with other platform applications.

URLs are _not_ localizable resources, so I moved them into the about dialog's xaml. Per #5138, we'll likely change them so that they get localization for "free" (dispatching based on the browser's language, without having to localize the URL in the application).
2020-03-27 21:00:32 +00:00
Chester Liu
5e9adad2a8 Doc: add Developer Command Prompt for VS profile (#5142) 2020-03-27 13:53:30 -07:00
Mike Griese
8c4ca4683b Add a note about binding multiple keys (#5015)
* Add a note about Binding multiple keys

From discussion in #4992

* Update doc/user-docs/UsingJsonSettings.md

Co-Authored-By: Josh Soref <jsoref@users.noreply.github.com>

* update the comment here to be a little clearer

Co-authored-by: Josh Soref <jsoref@users.noreply.github.com>
2020-03-27 10:51:32 -05:00
Dustin L. Howett (MSFT)
5bcf0fc3de Rework TermControl's initialization (#5051)
This commit rewrites a large swath of TermControl's initialization code.

* `TermControl` now _always_ has a `_terminal`; it will never be null
* Event registration for `_terminal` and any other available-at-init
  fixtures has been moved into the constructor.
* Event handlers how more uniformly check `_closing` if they interact
  with the _terminal.
* Swap chain attachment has been cleaned up and no longer uses a
  coroutine when it's spawned from the UI thread.
   * We have to register the renderer's swapchain change notification
     handler after we set the swap chain, otherwise it'll call us back
     when it initializes itself.
* `InitializeTerminal` now happens under the `_terminal`'s write lock
   * Certain things that InitializeTerminal were calling themselves
     attempted to take the lock. They no longer do so.
* TermControlAutomationPeer cannot take the read lock, because setting
  the scrollbar's `Maximum` during `InitializeTerminal` will trigger
  vivification of the automation peer tree; if it attempts to take the
  lock it will deadlock during initialization.
* `BlinkCursor` was renamed to `CursorTimerTick` because it's the "Tick"
  handler for the "CursorTimer".
* `DragDropHandler` was converted into a coroutine instead of just
  _calling_ a coroutine.

Caveats:

Terminal may not have a `_buffer` until InitializeTerminal happens.
There's a nasty coupling between RenderTarget and TextBuffer that means
that we need to have a renderer before we have a buffer.

There's a second nasty coupling between RenderThread and Renderer: we
can't create a RenderThread during construction because it needs to be
given a renderer, and we can't create a Renderer during construction
because it needs a RenderThread. We don't want to kick off a thread
during construction.

Testing:

I wailed on this by opening and closing and resizing terminals and panes
and tabs, up to a hundred open tabs and one tab with 51 panes. I set one
tab to update the title as fast as it possibly could and tested
teardown, zoom, resize, mouse movement, etc. while this was all
happening.

Closes #4613.
2020-03-26 16:25:11 -07:00
Dustin L. Howett (MSFT)
d47da2d617 Add a "debug tap" that lets you see the VT behind a connection (#5127)
This commit adds a debugging feature that can be activated by holding
down Left Alt _and_ Right Alt when a new tab is being created, if you
have the global setting "debugFeatures" set to true. That global setting
will default to true in DEBUG builds.

That debugging feature takes the form of a split pane that shows the raw
VT sequences being written to and received from the connection.

When those buttons are held down, every connection that's created as
part of a new tab is wrapped and split into _two_ connections: one to
capture input (and stand in for the main connection) and one to capture
output (and be displayed off to the side)

Closes #3206
2020-03-26 15:33:47 -07:00
Dustin Howett
31efd69149 Merge remote-tracking branch 'openconsole/inbox' 2020-03-25 18:21:05 -07:00
Michael Niksa
2c55ca107f Merged PR 4465022: [Git2Git] Merged PR 4464559: Console: Ingest OSS changes up to e0550798
Retrieved from https://microsoft.visualstudio.com os OS official/rs_onecore_dep_uxp cba60cafaadfcc7890a45dea3e1a24412c3d0ec6

Related work items: MSFT:25631386
2020-03-26 01:20:36 +00:00
Mike Griese
b3fa88eaed Process actions sync. on startup; don't dupe nonexistent profile (#5090)
This PR has evolved to encapsulate two related fixes that I can't really
untie anymore.

#2455 - Duplicating a tab that doesn't exist anymore

This was the bug I was originally fixing in #4429. 

When the user tries to `duplicateTab` with a profile that doesn't exist
anymore (like might happen after a settings reload), don't crash.

As I was going about adding tests for this, got blocked by the fact that
the Terminal couldn't open _any_ panes while the `TerminalPage` was size
0x0. This had two theoretical solutions: 

* Fake the `TerminalPage` into thinking it had a real size in the test -
  probably possible, though I'm unsure how it would work in practice.
* Change `Pane`s to not require an `ActualWidth`, `ActualHeight` on
  initialization. 

Fortuately, the second option was something else that was already on my
backlog of bugs. 

#4618 - `wt` command-line can't consistently parse more than one arg

Presently, the Terminal just arbitrarily dispatches a bunch of handlers
to try and handle all the commands provided on the commandline. That's
lead to a bunch of reports that not all the commands will always get
executed, nor will they all get executed in the same order. 

This PR also changes the `TerminalPage` to be able to dispatch all the
commands sequentially, all at once in the startup. No longer will there
be a hot second where the commands seem to execute themselves in from of
the user - they'll all happen behind the scenes on startup. 

This involved a couple other changes areound the `TerminalPage`
* I had to make sure that panes could be opened at a 0x0 size. Now they
  use a star sizing based off the percentage of the parent they're
  supposed to consume, so that when the parent _does_ get laid out,
  they'll take the appropriate size of that parent.
* I had to do some math ahead of time to try and calculate what a
  `SplitState::Automatic` would be evaluated as, despite the fact that
  we don't actually know how big the pane will be. 
* I had to ensure that `focus-tab` commands appropriately mark a single
  tab as focused while we're in startup, without roundtripping to the
  Dispatcher thread and back

## References

#4429 - the original PR for #2455
#5047 - a follow-up task from discussion in #4429
#4953 - a PR for making panes use star sizing, which was immensly
        helpful for this PR.

## Detailed Description of the Pull Request / Additional comments

`CascadiaSettings::BuildSettings` can throw if the GUID doesn't exist.
This wraps those calls up with a try/catch.

It also adds a couple tests - a few `SettingsTests` for try/catching
this state. It also adds a XAML-y test in `TabTests` that creates a
`TerminalPage` and then performs som UI-like actions on it. This test
required a minor change to how we generate the new tab dropdown - in the
tests, `Application::Current()` is _not_ a `TerminalApp::App`, so it
doesn't have a `Logic()` to query. So wrap that in a try/catch as well.

While working on these tests, I found that we'd crash pretty agressively
for mysterious reasons if the TestHostApp became focused while the test
was running. This was due to a call in
`TSFInputControl::NotifyFocusEnter` that would callback to
`TSFInputControl::_layoutRequested`, which would crash on setting the
`MaxSize` of the canvas to a negative value. This PR includes a hotfix
for that bug as well. 

## Validation Steps Performed

* Manual testing with a _lot_ of commands in a commandline
* run the tests
* Team tested in selfhost

Closes #2455
Closes #4618
2020-03-25 17:03:32 -07:00
Dustin Howett
9e8a716f03 Merged PR 4450465: Ingest OSS changes up to e0550798 2020-03-23 22:47:15 +00:00
Michael Niksa
78c1bc10f7 try to get present1 working and get mad because std::vector<bool> is bad. 2020-03-23 15:26:10 -07:00
Michael Niksa
b46b5d0f6f it works! 2020-03-23 14:31:12 -07:00
Michael Niksa
7e8f0398c3 Start moving dx renderer onto bitmap. 2020-03-23 09:55:01 -07:00
68 changed files with 2833 additions and 1043 deletions

View File

@@ -72,12 +72,25 @@ Assuming that you've installed Git Bash into `C:/Program Files/Git`:
```json
{
"name" : "Git Bash",
"commandline" : "C:/Program Files/Git/bin/bash.exe",
"commandline" : "C:/Program Files/Git/bin/bash.exe -li",
"icon" : "C:/Program Files/Git/mingw64/share/git/git-for-windows.ico",
"startingDirectory" : "%USERPROFILE%"
}
````
## Git Bash (WOW64)
Assuming that you've installed Git Bash into `C:/Program Files (x86)/Git`:
```json
{
"name" : "Git Bash",
"commandline" : "%ProgramFiles(x86)%/Git/bin/bash.exe -li",
"icon" : "%ProgramFiles(x86)%/Git/mingw32/share/git/git-for-windows.ico",
"startingDirectory" : "%USERPROFILE%"
}
```
## MSYS2
Assuming that you've installed MSYS2 into `C:/msys64`:
@@ -91,4 +104,16 @@ Assuming that you've installed MSYS2 into `C:/msys64`:
}
````
## Developer Command Prompt for Visual Studio
Assuming that you've installed VS 2019 Professional:
```json
{
"name" : "Developer Command Prompt for VS 2019",
"commandline" : "cmd.exe /k \"C:/Program Files (x86)/Microsoft Visual Studio/2019/Professional/Common7/Tools/VsDevCmd.bat\"",
"startingDirectory" : "%USERPROFILE%"
}
```
<!-- Adding a tool here? Make sure to add it in alphabetical order! -->

View File

@@ -107,6 +107,22 @@ add the following to your keybindings:
This will _unbind_ <kbd>Ctrl+Shift+6</kbd>, allowing vim to use the keystroke
instead of the terminal.
### Binding multiple keys
You can have multiple key chords bound to the same action. To do this, simply
add multiple bindings for the same action. For example:
```json
"keybindings" :
[
{ "command": "copy", "keys": "ctrl+shift+c" },
{ "command": "copy", "keys": "ctrl+c" },
{ "command": "copy", "keys": "enter" }
]
```
In this snippet, all three of <kbd>ctrl+shift+c</kbd>, <kbd>ctrl+c</kbd> and <kbd>enter</kbd> are bound to `copy`.
## Profiles
A profile contains the settings applied when a new WT tab is opened. Each

View File

@@ -70,6 +70,10 @@ namespace TerminalAppLocalTests
TEST_METHOD(TestTerminalArgsForBinding);
TEST_METHOD(FindMissingProfile);
TEST_METHOD(MakeSettingsForProfileThatDoesntExist);
TEST_METHOD(MakeSettingsForDefaultProfileThatDoesntExist);
TEST_METHOD(TestLayerProfileOnColorScheme);
TEST_METHOD(ValidateKeybindingsWarnings);
@@ -2094,6 +2098,146 @@ namespace TerminalAppLocalTests
}
}
void SettingsTests::FindMissingProfile()
{
// Test that CascadiaSettings::FindProfile returns null for a GUID that
// doesn't exist
const std::string settingsString{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}"
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}"
}
]
})" };
const auto settingsJsonObj = VerifyParseSucceeded(settingsString);
auto settings = CascadiaSettings::FromJson(settingsJsonObj);
const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
const Profile* const profile1 = settings->FindProfile(guid1);
const Profile* const profile2 = settings->FindProfile(guid2);
const Profile* const profile3 = settings->FindProfile(guid3);
VERIFY_IS_NOT_NULL(profile1);
VERIFY_IS_NOT_NULL(profile2);
VERIFY_IS_NULL(profile3);
VERIFY_ARE_EQUAL(L"profile0", profile1->GetName());
VERIFY_ARE_EQUAL(L"profile1", profile2->GetName());
}
void SettingsTests::MakeSettingsForProfileThatDoesntExist()
{
// Test that MakeSettings throws when the GUID doesn't exist
const std::string settingsString{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2
}
]
})" };
const auto settingsJsonObj = VerifyParseSucceeded(settingsString);
auto settings = CascadiaSettings::FromJson(settingsJsonObj);
const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
try
{
auto terminalSettings = settings->BuildSettings(guid1);
VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings);
VERIFY_ARE_EQUAL(1, terminalSettings.HistorySize());
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed");
}
try
{
auto terminalSettings = settings->BuildSettings(guid2);
VERIFY_ARE_NOT_EQUAL(nullptr, terminalSettings);
VERIFY_ARE_EQUAL(2, terminalSettings.HistorySize());
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed");
}
VERIFY_THROWS(auto terminalSettings = settings->BuildSettings(guid3), wil::ResultException, L"This call to BuildSettings should fail");
try
{
const auto [guid, termSettings] = settings->BuildSettings(nullptr);
VERIFY_ARE_NOT_EQUAL(nullptr, termSettings);
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed");
}
}
void SettingsTests::MakeSettingsForDefaultProfileThatDoesntExist()
{
// Test that MakeSettings _doesnt_ throw when we load settings with a
// defaultProfile that's not in the list, we validate the settings, and
// then call MakeSettings(nullopt). The validation should ensure that
// the default profile is something reasonable
const std::string settingsString{ R"(
{
"defaultProfile": "{6239a42c-3333-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2
}
]
})" };
const auto settingsJsonObj = VerifyParseSucceeded(settingsString);
auto settings = CascadiaSettings::FromJson(settingsJsonObj);
settings->_ValidateSettings();
VERIFY_ARE_EQUAL(2u, settings->_warnings.size());
VERIFY_ARE_EQUAL(2u, settings->_profiles.size());
VERIFY_ARE_EQUAL(settings->_globals.GetDefaultProfile(), settings->_profiles.at(0).GetGuid());
try
{
const auto [guid, termSettings] = settings->BuildSettings(nullptr);
VERIFY_ARE_NOT_EQUAL(nullptr, termSettings);
VERIFY_ARE_EQUAL(1, termSettings.HistorySize());
}
catch (...)
{
VERIFY_IS_TRUE(false, L"This call to BuildSettings should succeed");
}
}
void SettingsTests::TestLayerProfileOnColorScheme()
{
Log::Comment(NoThrowString().Format(

View File

@@ -13,8 +13,10 @@
using namespace Microsoft::Console;
using namespace TerminalApp;
using namespace winrt::TerminalApp;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
using namespace WEX::Common;
namespace TerminalAppLocalTests
{
@@ -53,11 +55,20 @@ namespace TerminalAppLocalTests
TEST_METHOD(CreateSimpleTerminalXamlType);
TEST_METHOD(CreateTerminalMuxXamlType);
TEST_METHOD(CreateTerminalPage);
TEST_METHOD(TryDuplicateBadTab);
TEST_METHOD(TryDuplicateBadPane);
TEST_CLASS_SETUP(ClassSetup)
{
InitializeJsonReader();
return true;
}
private:
void _initializeTerminalPage(winrt::com_ptr<winrt::TerminalApp::implementation::TerminalPage>& page,
std::shared_ptr<CascadiaSettings> initialSettings);
};
void TabTests::EnsureTestsActivate()
@@ -164,4 +175,321 @@ namespace TerminalAppLocalTests
VERIFY_SUCCEEDED(result);
}
void TabTests::CreateTerminalPage()
{
winrt::com_ptr<winrt::TerminalApp::implementation::TerminalPage> page{ nullptr };
auto result = RunOnUIThread([&page]() {
page = winrt::make_self<winrt::TerminalApp::implementation::TerminalPage>();
VERIFY_IS_NOT_NULL(page);
});
VERIFY_SUCCEEDED(result);
}
// Method Description:
// - This is a helper to set up a TerminalPage for a unittest. This method
// does a couple things:
// * Create()'s a TerminalPage with the given settings. Constructing a
// TerminalPage so that we can get at its implementation is wacky, so
// this helper will do it correctly for you, even if this doesn't make a
// ton of sense on the surface. This is also why you need to pass both a
// projection and a com_ptr to this method.
// * It will use the provided settings object to initialize the TerminalPage
// * It will add the TerminalPage to the test Application, so that we can
// get actual layout events. Much of the Terminal assumes there's a
// non-zero ActualSize to the Terminal window, and adding the Page to
// the Application will make it behave as expected.
// * It will wait for the TerminalPage to finish initialization before
// returning control to the caller. It does this by creating an event and
// only setting the event when the TerminalPage raises its Initialized
// event, to signal that startup is complete. At this point, there will
// be one tab with the default profile in the page.
// * It will also ensure that the first tab is focused, since that happens
// asynchronously in the application typically.
// Arguments:
// - page: a TerminalPage implementation ptr that will receive the new TerminalPage instance
// - initialSettings: a CascadiaSettings to initialize the TerminalPage with.
// Return Value:
// - <none>
void TabTests::_initializeTerminalPage(winrt::com_ptr<winrt::TerminalApp::implementation::TerminalPage>& page,
std::shared_ptr<CascadiaSettings> initialSettings)
{
// This is super wacky, but we can't just initialize the
// com_ptr<impl::TerminalPage> in the lambda and assign it back out of
// the lambda. We'll crash trying to get a weak_ref to the TerminalPage
// during TerminalPage::Create() below.
//
// Instead, create the winrt object, then get a com_ptr to the
// implementation _from_ the winrt object. This seems to work, even if
// it's weird.
winrt::TerminalApp::TerminalPage projectedPage{ nullptr };
Log::Comment(NoThrowString().Format(L"Construct the TerminalPage"));
auto result = RunOnUIThread([&projectedPage, &page, initialSettings]() {
projectedPage = winrt::TerminalApp::TerminalPage();
page.copy_from(winrt::get_self<winrt::TerminalApp::implementation::TerminalPage>(projectedPage));
page->_settings = initialSettings;
});
VERIFY_SUCCEEDED(result);
VERIFY_IS_NOT_NULL(page);
VERIFY_IS_NOT_NULL(page->_settings);
::details::Event waitForInitEvent;
if (!waitForInitEvent.IsValid())
{
VERIFY_SUCCEEDED(HRESULT_FROM_WIN32(::GetLastError()));
}
page->Initialized([&waitForInitEvent](auto&&, auto&&) {
waitForInitEvent.Set();
});
Log::Comment(L"Create() the TerminalPage");
result = RunOnUIThread([&page]() {
VERIFY_IS_NOT_NULL(page);
VERIFY_IS_NOT_NULL(page->_settings);
page->Create();
Log::Comment(L"Create()'d the page successfully");
auto app = ::winrt::Windows::UI::Xaml::Application::Current();
winrt::TerminalApp::TerminalPage pp = *page;
winrt::Windows::UI::Xaml::Window::Current().Content(pp);
winrt::Windows::UI::Xaml::Window::Current().Activate();
});
VERIFY_SUCCEEDED(result);
Log::Comment(L"Wait for the page to finish initializing...");
VERIFY_SUCCEEDED(waitForInitEvent.Wait());
Log::Comment(L"...Done");
result = RunOnUIThread([&page]() {
// In the real app, this isn't a problem, but doesn't happen
// reliably in the unit tests.
Log::Comment(L"Ensure we set the first tab as the selected one.");
auto tab{ page->_GetStrongTabImpl(0) };
page->_tabView.SelectedItem(tab->GetTabViewItem());
page->_UpdatedSelectedTab(0);
});
VERIFY_SUCCEEDED(result);
}
void TabTests::TryDuplicateBadTab()
{
// * Create a tab with a profile with GUID 1
// * Reload the settings so that GUID 1 is no longer in the list of profiles
// * Try calling _DuplicateTabViewItem on tab 1
// * No new tab should be created (and more importantly, the app should not crash)
//
// Created to test GH#2455
const std::string settingsJson0{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2
}
]
})" };
const std::string settingsJson1{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2
}
]
})" };
VerifyParseSucceeded(settingsJson0);
auto settings0 = std::make_shared<CascadiaSettings>(false);
VERIFY_IS_NOT_NULL(settings0);
settings0->_ParseJsonString(settingsJson0, false);
settings0->LayerJson(settings0->_userSettings);
settings0->_ValidateSettings();
VerifyParseSucceeded(settingsJson1);
auto settings1 = std::make_shared<CascadiaSettings>(false);
VERIFY_IS_NOT_NULL(settings1);
settings1->_ParseJsonString(settingsJson1, false);
settings1->LayerJson(settings1->_userSettings);
settings1->_ValidateSettings();
const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
// This is super wacky, but we can't just initialize the
// com_ptr<impl::TerminalPage> in the lambda and assign it back out of
// the lambda. We'll crash trying to get a weak_ref to the TerminalPage
// during TerminalPage::Create() below.
//
// Instead, create the winrt object, then get a com_ptr to the
// implementation _from_ the winrt object. This seems to work, even if
// it's weird.
winrt::com_ptr<winrt::TerminalApp::implementation::TerminalPage> page{ nullptr };
_initializeTerminalPage(page, settings0);
auto result = RunOnUIThread([&page]() {
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
});
VERIFY_SUCCEEDED(result);
Log::Comment(L"Duplicate the first tab");
result = RunOnUIThread([&page]() {
page->_DuplicateTabViewItem();
VERIFY_ARE_EQUAL(2u, page->_tabs.Size());
});
VERIFY_SUCCEEDED(result);
Log::Comment(NoThrowString().Format(
L"Change the settings of the TerminalPage so the first profile is "
L"no longer in the list of profiles"));
result = RunOnUIThread([&page, settings1]() {
page->_settings = settings1;
});
VERIFY_SUCCEEDED(result);
Log::Comment(L"Duplicate the tab, and don't crash");
result = RunOnUIThread([&page]() {
page->_DuplicateTabViewItem();
VERIFY_ARE_EQUAL(2u, page->_tabs.Size(), L"We should gracefully do nothing here - the profile no longer exists.");
});
VERIFY_SUCCEEDED(result);
}
void TabTests::TryDuplicateBadPane()
{
// * Create a tab with a profile with GUID 1
// * Reload the settings so that GUID 1 is no longer in the list of profiles
// * Try calling _SplitPane(Duplicate) on tab 1
// * No new pane should be created (and more importantly, the app should not crash)
//
// Created to test GH#2455
const std::string settingsJson0{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile0",
"guid": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"historySize": 1
},
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2
}
]
})" };
const std::string settingsJson1{ R"(
{
"defaultProfile": "{6239a42c-1111-49a3-80bd-e8fdd045185c}",
"profiles": [
{
"name" : "profile1",
"guid": "{6239a42c-2222-49a3-80bd-e8fdd045185c}",
"historySize": 2
}
]
})" };
VerifyParseSucceeded(settingsJson0);
auto settings0 = std::make_shared<CascadiaSettings>(false);
VERIFY_IS_NOT_NULL(settings0);
settings0->_ParseJsonString(settingsJson0, false);
settings0->LayerJson(settings0->_userSettings);
settings0->_ValidateSettings();
VerifyParseSucceeded(settingsJson1);
auto settings1 = std::make_shared<CascadiaSettings>(false);
VERIFY_IS_NOT_NULL(settings1);
settings1->_ParseJsonString(settingsJson1, false);
settings1->LayerJson(settings1->_userSettings);
settings1->_ValidateSettings();
const auto guid1 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-1111-49a3-80bd-e8fdd045185c}");
const auto guid2 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-2222-49a3-80bd-e8fdd045185c}");
const auto guid3 = Microsoft::Console::Utils::GuidFromString(L"{6239a42c-3333-49a3-80bd-e8fdd045185c}");
// This is super wacky, but we can't just initialize the
// com_ptr<impl::TerminalPage> in the lambda and assign it back out of
// the lambda. We'll crash trying to get a weak_ref to the TerminalPage
// during TerminalPage::Create() below.
//
// Instead, create the winrt object, then get a com_ptr to the
// implementation _from_ the winrt object. This seems to work, even if
// it's weird.
winrt::com_ptr<winrt::TerminalApp::implementation::TerminalPage> page{ nullptr };
_initializeTerminalPage(page, settings0);
auto result = RunOnUIThread([&page]() {
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
});
VERIFY_SUCCEEDED(result);
result = RunOnUIThread([&page]() {
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
auto tab = page->_GetStrongTabImpl(0);
VERIFY_ARE_EQUAL(1, tab->_GetLeafPaneCount());
});
VERIFY_SUCCEEDED(result);
Log::Comment(NoThrowString().Format(L"Duplicate the first pane"));
result = RunOnUIThread([&page]() {
page->_SplitPane(SplitState::Automatic, SplitType::Duplicate, nullptr);
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
auto tab = page->_GetStrongTabImpl(0);
VERIFY_ARE_EQUAL(2, tab->_GetLeafPaneCount());
});
VERIFY_SUCCEEDED(result);
Log::Comment(NoThrowString().Format(
L"Change the settings of the TerminalPage so the first profile is "
L"no longer in the list of profiles"));
result = RunOnUIThread([&page, settings1]() {
page->_settings = settings1;
});
VERIFY_SUCCEEDED(result);
Log::Comment(NoThrowString().Format(L"Duplicate the pane, and don't crash"));
result = RunOnUIThread([&page]() {
page->_SplitPane(SplitState::Automatic, SplitType::Duplicate, nullptr);
VERIFY_ARE_EQUAL(1u, page->_tabs.Size());
auto tab = page->_GetStrongTabImpl(0);
VERIFY_ARE_EQUAL(2,
tab->_GetLeafPaneCount(),
L"We should gracefully do nothing here - the profile no longer exists.");
});
VERIFY_SUCCEEDED(result);
auto cleanup = wil::scope_exit([] {
auto result = RunOnUIThread([]() {
// There's something causing us to crash north of
// TSFInputControl::NotifyEnter, or LayoutRequested. It's very
// unclear what that issue is. Since these tests don't run in
// CI, simply log a message so that the dev running these tests
// knows it's expected.
Log::Comment(L"This test often crashes on cleanup, even when it succeeds. If it succeeded, then crashes, that's okay.");
});
VERIFY_SUCCEEDED(result);
});
}
}

View File

@@ -0,0 +1,147 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "pch.h"
#include "DebugTapConnection.h"
using namespace ::winrt::Microsoft::Terminal::TerminalConnection;
using namespace ::winrt::Windows::Foundation;
namespace winrt::Microsoft::TerminalApp::implementation
{
// DebugInputTapConnection is an implementation detail of DebugTapConnection.
// It wraps the _actual_ connection so it can hook WriteInput and forward it
// into the actual debug panel.
class DebugInputTapConnection : public winrt::implements<DebugInputTapConnection, ITerminalConnection>
{
public:
DebugInputTapConnection(winrt::com_ptr<DebugTapConnection> pairedTap, ITerminalConnection wrappedConnection) :
_pairedTap{ pairedTap },
_wrappedConnection{ std::move(wrappedConnection) }
{
}
~DebugInputTapConnection() = default;
void Start()
{
_wrappedConnection.Start();
}
void WriteInput(hstring const& data)
{
_pairedTap->_PrintInput(data);
_wrappedConnection.WriteInput(data);
}
void Resize(uint32_t rows, uint32_t columns) { _wrappedConnection.Resize(rows, columns); }
void Close() { _wrappedConnection.Close(); }
winrt::event_token TerminalOutput(TerminalOutputHandler const& args) { return _wrappedConnection.TerminalOutput(args); };
void TerminalOutput(winrt::event_token const& token) noexcept { _wrappedConnection.TerminalOutput(token); };
winrt::event_token StateChanged(TypedEventHandler<ITerminalConnection, IInspectable> const& handler) { return _wrappedConnection.StateChanged(handler); };
void StateChanged(winrt::event_token const& token) noexcept { _wrappedConnection.StateChanged(token); };
ConnectionState State() const noexcept { return _wrappedConnection.State(); }
private:
winrt::com_ptr<DebugTapConnection> _pairedTap;
ITerminalConnection _wrappedConnection;
};
DebugTapConnection::DebugTapConnection(ITerminalConnection wrappedConnection)
{
_outputRevoker = wrappedConnection.TerminalOutput(winrt::auto_revoke, { this, &DebugTapConnection::_OutputHandler });
_stateChangedRevoker = wrappedConnection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*e*/) {
_StateChangedHandlers(*this, nullptr);
});
_wrappedConnection = wrappedConnection;
}
DebugTapConnection::~DebugTapConnection()
{
}
void DebugTapConnection::Start()
{
// presume the wrapped connection is started.
}
void DebugTapConnection::WriteInput(hstring const& data)
{
// If the user types into the tap side, forward it to the input side
if (auto strongInput{ _inputSide.get() })
{
auto inputAsTap{ winrt::get_self<DebugInputTapConnection>(strongInput) };
inputAsTap->WriteInput(data);
}
}
void DebugTapConnection::Resize(uint32_t /*rows*/, uint32_t /*columns*/)
{
// no resize events are propagated
}
void DebugTapConnection::Close()
{
_outputRevoker.revoke();
_stateChangedRevoker.revoke();
_wrappedConnection = nullptr;
}
ConnectionState DebugTapConnection::State() const noexcept
{
if (auto strongConnection{ _wrappedConnection.get() })
{
return strongConnection.State();
}
return ConnectionState::Failed;
}
static std::wstring _sanitizeString(const std::wstring_view str)
{
std::wstring newString{ str.begin(), str.end() };
for (auto& ch : newString)
{
if (ch < 0x20)
{
ch += 0x2400;
}
else if (ch == 0x20)
{
ch = 0x2423; // replace space with ␣
}
else if (ch == 0x7f)
{
ch = 0x2421; // replace del with ␡
}
}
return newString;
}
void DebugTapConnection::_OutputHandler(const hstring str)
{
_TerminalOutputHandlers(_sanitizeString(str));
}
// Called by the DebugInputTapConnection to print user input
void DebugTapConnection::_PrintInput(const hstring& str)
{
auto clean{ _sanitizeString(str) };
auto formatted{ wil::str_printf<std::wstring>(L"\x1b[91m%ls\x1b[m", clean.data()) };
_TerminalOutputHandlers(formatted);
}
// Wire us up so that we can forward input through
void DebugTapConnection::SetInputTap(const Microsoft::Terminal::TerminalConnection::ITerminalConnection& inputTap)
{
_inputSide = inputTap;
}
}
// Function Description
// - Takes one connection and returns two connections:
// 1. One that can be used in place of the original connection (wrapped)
// 2. One that will print raw VT sequences sent into and received _from_ the original connection.
std::tuple<ITerminalConnection, ITerminalConnection> OpenDebugTapConnection(ITerminalConnection baseConnection)
{
using namespace winrt::Microsoft::TerminalApp::implementation;
auto debugSide{ winrt::make_self<DebugTapConnection>(baseConnection) };
auto inputSide{ winrt::make_self<DebugInputTapConnection>(debugSide, baseConnection) };
debugSide->SetInputTap(*inputSide);
std::tuple<ITerminalConnection, ITerminalConnection> p{ *inputSide, *debugSide };
return p;
}

View File

@@ -0,0 +1,42 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
#include <winrt/Microsoft.Terminal.TerminalConnection.h>
#include "../../inc/cppwinrt_utils.h"
namespace winrt::Microsoft::TerminalApp::implementation
{
class DebugInputTapConnection;
class DebugTapConnection : public winrt::implements<DebugTapConnection, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection>
{
public:
DebugTapConnection(Microsoft::Terminal::TerminalConnection::ITerminalConnection wrappedConnection);
~DebugTapConnection();
void Start();
void WriteInput(hstring const& data);
void Resize(uint32_t rows, uint32_t columns);
void Close();
winrt::Microsoft::Terminal::TerminalConnection::ConnectionState State() const noexcept;
void SetInputTap(const Microsoft::Terminal::TerminalConnection::ITerminalConnection& inputTap);
WINRT_CALLBACK(TerminalOutput, winrt::Microsoft::Terminal::TerminalConnection::TerminalOutputHandler);
TYPED_EVENT(StateChanged, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, winrt::Windows::Foundation::IInspectable);
private:
void _PrintInput(const hstring& data);
void _OutputHandler(const hstring str);
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::TerminalOutput_revoker _outputRevoker;
winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection::StateChanged_revoker _stateChangedRevoker;
winrt::weak_ref<Microsoft::Terminal::TerminalConnection::ITerminalConnection> _wrappedConnection;
winrt::weak_ref<Microsoft::Terminal::TerminalConnection::ITerminalConnection> _inputSide;
friend class DebugInputTapConnection;
};
}
std::tuple<winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection, winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection> OpenDebugTapConnection(winrt::Microsoft::Terminal::TerminalConnection::ITerminalConnection baseConnection);

View File

@@ -41,6 +41,14 @@ static constexpr std::wstring_view LightThemeValue{ L"light" };
static constexpr std::wstring_view DarkThemeValue{ L"dark" };
static constexpr std::wstring_view SystemThemeValue{ L"system" };
static constexpr std::string_view DebugFeaturesKey{ "debugFeatures" };
#ifdef _DEBUG
static constexpr bool debugFeaturesDefault{ true };
#else
static constexpr bool debugFeaturesDefault{ false };
#endif
GlobalAppSettings::GlobalAppSettings() :
_keybindings{ winrt::make_self<winrt::TerminalApp::implementation::AppKeyBindings>() },
_keybindingsWarnings{},
@@ -59,7 +67,8 @@ GlobalAppSettings::GlobalAppSettings() :
_tabWidthMode{ TabViewWidthMode::Equal },
_wordDelimiters{ DEFAULT_WORD_DELIMITERS },
_copyOnSelect{ false },
_launchMode{ LaunchMode::DefaultMode }
_launchMode{ LaunchMode::DefaultMode },
_debugFeatures{ debugFeaturesDefault }
{
}
@@ -171,6 +180,11 @@ void GlobalAppSettings::SetConfirmCloseAllTabs(const bool confirmCloseAllTabs) n
_confirmCloseAllTabs = confirmCloseAllTabs;
}
bool GlobalAppSettings::DebugFeaturesEnabled() const noexcept
{
return _debugFeatures;
}
#pragma region ExperimentalSettings
bool GlobalAppSettings::GetShowTabsInTitlebar() const noexcept
{
@@ -237,6 +251,7 @@ Json::Value GlobalAppSettings::ToJson() const
jsonObject[JsonKey(KeybindingsKey)] = _keybindings->ToJson();
jsonObject[JsonKey(ConfirmCloseAllKey)] = _confirmCloseAllTabs;
jsonObject[JsonKey(SnapToGridOnResizeKey)] = _SnapToGridOnResize;
jsonObject[JsonKey(DebugFeaturesKey)] = _debugFeatures;
return jsonObject;
}
@@ -324,6 +339,9 @@ void GlobalAppSettings::LayerJson(const Json::Value& json)
}
JsonUtils::GetBool(json, SnapToGridOnResizeKey, _SnapToGridOnResize);
// GetBool will only override the current value if the key exists
JsonUtils::GetBool(json, DebugFeaturesKey, _debugFeatures);
}
// Method Description:

View File

@@ -77,6 +77,8 @@ public:
winrt::Microsoft::UI::Xaml::Controls::TabViewWidthMode GetTabWidthMode() const noexcept;
bool DebugFeaturesEnabled() const noexcept;
Json::Value ToJson() const;
static GlobalAppSettings FromJson(const Json::Value& json);
void LayerJson(const Json::Value& json);
@@ -115,6 +117,8 @@ private:
winrt::TerminalApp::LaunchMode _launchMode;
bool _debugFeatures;
static winrt::Windows::UI::Xaml::ElementTheme _ParseTheme(const std::wstring& themeString) noexcept;
static std::wstring_view _SerializeTheme(const winrt::Windows::UI::Xaml::ElementTheme theme) noexcept;

View File

@@ -70,7 +70,7 @@ void Pane::ResizeContent(const Size& newSize)
const auto width = newSize.Width;
const auto height = newSize.Height;
_CreateRowColDefinitions(newSize);
_CreateRowColDefinitions();
if (_splitState == SplitState::Vertical)
{
@@ -790,23 +790,24 @@ void Pane::_SetupChildCloseHandlers()
// which is stored in _desiredSplitPosition
// - Does nothing if our split state is currently set to SplitState::None
// Arguments:
// - rootSize: The dimensions in pixels that this pane (and its children should consume.)
// - <none>
// Return Value:
// - <none>
void Pane::_CreateRowColDefinitions(const Size& rootSize)
void Pane::_CreateRowColDefinitions()
{
const auto first = _desiredSplitPosition * 100.0f;
const auto second = 100.0f - first;
if (_splitState == SplitState::Vertical)
{
_root.ColumnDefinitions().Clear();
// Create two columns in this grid: one for each pane
const auto paneSizes = _CalcChildrenSizes(rootSize.Width);
auto firstColDef = Controls::ColumnDefinition();
firstColDef.Width(GridLengthHelper::FromValueAndType(paneSizes.first, GridUnitType::Star));
firstColDef.Width(GridLengthHelper::FromValueAndType(first, GridUnitType::Star));
auto secondColDef = Controls::ColumnDefinition();
secondColDef.Width(GridLengthHelper::FromValueAndType(paneSizes.second, GridUnitType::Star));
secondColDef.Width(GridLengthHelper::FromValueAndType(second, GridUnitType::Star));
_root.ColumnDefinitions().Append(firstColDef);
_root.ColumnDefinitions().Append(secondColDef);
@@ -816,35 +817,18 @@ void Pane::_CreateRowColDefinitions(const Size& rootSize)
_root.RowDefinitions().Clear();
// Create two rows in this grid: one for each pane
const auto paneSizes = _CalcChildrenSizes(rootSize.Height);
auto firstRowDef = Controls::RowDefinition();
firstRowDef.Height(GridLengthHelper::FromValueAndType(paneSizes.first, GridUnitType::Star));
firstRowDef.Height(GridLengthHelper::FromValueAndType(first, GridUnitType::Star));
auto secondRowDef = Controls::RowDefinition();
secondRowDef.Height(GridLengthHelper::FromValueAndType(paneSizes.second, GridUnitType::Star));
secondRowDef.Height(GridLengthHelper::FromValueAndType(second, GridUnitType::Star));
_root.RowDefinitions().Append(firstRowDef);
_root.RowDefinitions().Append(secondRowDef);
}
}
// Method Description:
// - Initializes our UI for a new split in this pane. Sets up row/column
// definitions, and initializes the separator grid. Does nothing if our split
// state is currently set to SplitState::None
// Arguments:
// - <none>
// Return Value:
// - <none>
void Pane::_CreateSplitContent()
{
Size actualSize{ gsl::narrow_cast<float>(_root.ActualWidth()),
gsl::narrow_cast<float>(_root.ActualHeight()) };
_CreateRowColDefinitions(actualSize);
}
// Method Description:
// - Sets the thickness of each side of our borders to match our _borders state.
// Arguments:
@@ -1077,7 +1061,7 @@ std::pair<std::shared_ptr<Pane>, std::shared_ptr<Pane>> Pane::_Split(SplitState
_control = { nullptr };
_secondChild = std::make_shared<Pane>(profile, control);
_CreateSplitContent();
_CreateRowColDefinitions();
_root.Children().Append(_firstChild->GetRootElement());
_root.Children().Append(_secondChild->GetRootElement());
@@ -1522,4 +1506,74 @@ void Pane::_SetupResources()
}
}
int Pane::GetLeafPaneCount() const noexcept
{
return _IsLeaf() ? 1 : (_firstChild->GetLeafPaneCount() + _secondChild->GetLeafPaneCount());
}
// Method Description:
// - This is a helper to determine which direction an "Automatic" split should
// happen in for a given pane, but without using the ActualWidth() and
// ActualHeight() methods. This is used during the initialization of the
// Terminal, when we could be processing many "split-pane" commands _before_
// we've ever laid out the Terminal for the first time. When this happens, the
// Pane's don't have an actual size yet. However, we'd still like to figure
// out how to do an "auto" split when these Panes are all laid out.
// - This method assumes that the Pane we're attempting to split is `target`,
// and this method should be called on the root of a tree of Panes.
// - We'll walk down the tree attempting to find `target`. As we traverse the
// tree, we'll reduce the size passed to each subsequent recursive call. The
// size passed to this method represents how much space this Pane _will_ have
// to use.
// * If this pane is a leaf, and it's the pane we're looking for, use the
// available space to calculate which direction to split in.
// * If this pane is _any other leaf_, then just return nullopt, to indicate
// that the `target` Pane is not down this branch.
// * If this pane is a parent, calculate how much space our children will be
// able to use, and recurse into them.
// Arguments:
// - target: The Pane we're attempting to split.
// - availableSpace: The theoretical space that's available for this pane to be able to split.
// Return Value:
// - nullopt if `target` is not this pane or a child of this pane, otherwise the
// SplitState that `target` would use for an `Automatic` split given
// `availableSpace`
std::optional<winrt::TerminalApp::SplitState> Pane::PreCalculateAutoSplit(const std::shared_ptr<Pane> target,
const winrt::Windows::Foundation::Size availableSpace) const
{
if (_IsLeaf())
{
if (target.get() == this)
{
//If this pane is a leaf, and it's the pane we're looking for, use
//the available space to calculate which direction to split in.
return availableSpace.Width > availableSpace.Height ? SplitState::Vertical : SplitState::Horizontal;
}
else
{
// If this pane is _any other leaf_, then just return nullopt, to
// indicate that the `target` Pane is not down this branch.
return std::nullopt;
}
}
else
{
// If this pane is a parent, calculate how much space our children will
// be able to use, and recurse into them.
const bool isVerticalSplit = _splitState == SplitState::Vertical;
const float firstWidth = isVerticalSplit ? (availableSpace.Width * _desiredSplitPosition) : availableSpace.Width;
const float secondWidth = isVerticalSplit ? (availableSpace.Width - firstWidth) : availableSpace.Width;
const float firstHeight = !isVerticalSplit ? (availableSpace.Height * _desiredSplitPosition) : availableSpace.Height;
const float secondHeight = !isVerticalSplit ? (availableSpace.Height - firstHeight) : availableSpace.Height;
const auto firstResult = _firstChild->PreCalculateAutoSplit(target, { firstWidth, firstHeight });
return firstResult.has_value() ? firstResult : _secondChild->PreCalculateAutoSplit(target, { secondWidth, secondHeight });
}
// We should not possibly be getting here - both the above branches should
// return a value.
FAIL_FAST();
}
DEFINE_EVENT(Pane, GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);

View File

@@ -63,10 +63,13 @@ public:
const GUID& profile,
const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
std::optional<winrt::TerminalApp::SplitState> PreCalculateAutoSplit(const std::shared_ptr<Pane> target, const winrt::Windows::Foundation::Size parentSize) const;
void Shutdown();
void Close();
int GetLeafPaneCount() const noexcept;
WINRT_CALLBACK(Closed, winrt::Windows::Foundation::EventHandler<winrt::Windows::Foundation::IInspectable>);
DECLARE_EVENT(GotFocus, _GotFocusHandlers, winrt::delegate<std::shared_ptr<Pane>>);
@@ -107,8 +110,7 @@ private:
const GUID& profile,
const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void _CreateRowColDefinitions(const winrt::Windows::Foundation::Size& rootSize);
void _CreateSplitContent();
void _CreateRowColDefinitions();
void _ApplySplitDefinitions();
void _UpdateBorders();
@@ -133,6 +135,9 @@ private:
float _ClampSplitPosition(const bool widthOrHeight, const float requestedValue, const float totalSize) const;
winrt::TerminalApp::SplitState _convertAutomaticSplitState(const winrt::TerminalApp::SplitState& splitType) const;
std::optional<winrt::TerminalApp::SplitState> _preCalculateAutoSplit(const std::shared_ptr<Pane> target, const winrt::Windows::Foundation::Size parentSize) const;
// Function Description:
// - Returns true if the given direction can be used with the given split
// type.

View File

@@ -162,40 +162,6 @@ Temporarily using the Windows Terminal default settings.
<data name="ReloadJsonParseErrorTitle" xml:space="preserve">
<value>Failed to reload settings</value>
</data>
<data name="AboutTitleText" xml:space="preserve">
<value>About</value>
</data>
<data name="VersionLabelText" xml:space="preserve">
<value>Version:</value>
</data>
<data name="DocumentationLabelText" xml:space="preserve">
<value>Documentation
</value>
</data>
<data name="GettingStartedLabelText" xml:space="preserve">
<value>Getting Started
</value>
</data>
<data name="ReleaseNotesLabelText" xml:space="preserve">
<value>Release Notes
</value>
</data>
<data name="PrivacyPolicyLabelText" xml:space="preserve">
<value>Privacy Policy
</value>
</data>
<data name="DocumentationUriValue" xml:space="preserve">
<value>https://aka.ms/terminal-documentation</value>
</data>
<data name="GettingStartedUriValue" xml:space="preserve">
<value>https://aka.ms/terminal-getting-started</value>
</data>
<data name="ReleaseNotesUriValue" xml:space="preserve">
<value>https://aka.ms/terminal-release-notes</value>
</data>
<data name="PrivacyPolicyUriValue" xml:space="preserve">
<value>https://aka.ms/terminal-privacy-policy</value>
</data>
<data name="FeedbackUriValue" xml:space="preserve">
<value>https://aka.ms/terminal-feedback</value>
</data>
@@ -208,15 +174,6 @@ Temporarily using the Windows Terminal default settings.
<data name="SettingsMenuItem" xml:space="preserve">
<value>Settings</value>
</data>
<data name="Cancel" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="CloseAll" xml:space="preserve">
<value>Close all</value>
</data>
<data name="CloseWindowWarningTitle" xml:space="preserve">
<value>Do you want to close all tabs?</value>
</data>
<data name="InvalidBackgroundImage" xml:space="preserve">
<value>Found a profile with an invalid "backgroundImage". Defaulting that profile to have no background image. Make sure that when setting a "backgroundImage", the value is a valid file path to an image.</value>
</data>
@@ -283,4 +240,47 @@ Temporarily using the Windows Terminal default settings.
<data name="WindowMinimizeButton.[using:Windows.UI.Xaml.Automation]AutomationProperties.Name" xml:space="preserve">
<value>Minimize</value>
</data>
</root>
<data name="AboutDialog.Title" xml:space="preserve">
<value>About</value>
</data>
<data name="AboutDialog.CloseButtonText" xml:space="preserve">
<value>OK</value>
</data>
<data name="AboutDialog_VersionLabel.Text" xml:space="preserve">
<value>Version:</value>
<comment>This is the heading for a version number label</comment>
</data>
<data name="AboutDialog_GettingStartedLink.Content" xml:space="preserve">
<value>Getting Started</value>
<comment>A hyperlink name for a guide on how to get started using Terminal</comment>
</data>
<data name="AboutDialog_DocumentationLink.Content" xml:space="preserve">
<value>Documentation</value>
<comment>A hyperlink name for user documentation</comment>
</data>
<data name="AboutDialog_ReleaseNotesLink.Content" xml:space="preserve">
<value>Release Notes</value>
<comment>A hyperlink name for the Terminal's release notes</comment>
</data>
<data name="AboutDialog_PrivacyPolicyLink.Content" xml:space="preserve">
<value>Privacy Policy</value>
<comment>A hyperlink name for the Terminal's privacy policy</comment>
</data>
<data name="AboutDialog_DisplayNameUnpackaged" xml:space="preserve">
<value>Windows Terminal (Unpackaged)</value>
<comment>This display name is used when the application's name cannot be determined</comment>
</data>
<data name="AboutDialog_VersionUnknown" xml:space="preserve">
<value>Unknown</value>
<comment>This is displayed when the version of the application cannot be determined</comment>
</data>
<data name="CloseAllDialog.CloseButtonText" xml:space="preserve">
<value>Cancel</value>
</data>
<data name="CloseAllDialog.PrimaryButtonText" xml:space="preserve">
<value>Close all</value>
</data>
<data name="CloseAllDialog.Title" xml:space="preserve">
<value>Do you want to close all tabs?</value>
</data>
</root>

View File

@@ -277,6 +277,12 @@ namespace winrt::TerminalApp::implementation
// gains focus, we'll mark it as the new active pane.
_AttachEventHandlersToPane(first);
_AttachEventHandlersToPane(second);
// Immediately update our tracker of the focused pane now. If we're
// splitting panes during startup (from a commandline), then it's
// possible that the focus events won't propagate immediately. Updating
// the focus here will give the same effect though.
_UpdateActivePane(second);
}
// Method Description:
@@ -387,6 +393,28 @@ namespace winrt::TerminalApp::implementation
});
}
// Method Description:
// - Mark the given pane as the active pane in this tab. All other panes
// will be marked as inactive. We'll also update our own UI state to
// reflect this newly active pane.
// Arguments:
// - pane: a Pane to mark as active.
// Return Value:
// - <none>
void Tab::_UpdateActivePane(std::shared_ptr<Pane> pane)
{
// Clear the active state of the entire tree, and mark only the pane as active.
_rootPane->ClearActive();
_activePane = pane;
_activePane->SetActive();
// Update our own title text to match the newly-active pane.
SetTabText(GetActiveTitle());
// Raise our own ActivePaneChanged event.
_ActivePaneChangedHandlers();
}
// Method Description:
// - Add an event handler to this pane's GotFocus event. When that pane gains
// focus, we'll mark it as the new active pane. We'll also query the title of
@@ -406,19 +434,37 @@ namespace winrt::TerminalApp::implementation
if (tab && sender != tab->_activePane)
{
// Clear the active state of the entire tree, and mark only the sender as active.
tab->_rootPane->ClearActive();
tab->_activePane = sender;
tab->_activePane->SetActive();
// Update our own title text to match the newly-active pane.
tab->SetTabText(tab->GetActiveTitle());
// Raise our own ActivePaneChanged event.
tab->_ActivePaneChangedHandlers();
tab->_UpdateActivePane(sender);
}
});
}
// Method Description:
// - Get the total number of leaf panes in this tab. This will be the number
// of actual controls hosted by this tab.
// Arguments:
// - <none>
// Return Value:
// - The total number of leaf panes hosted by this tab.
int Tab::_GetLeafPaneCount() const noexcept
{
return _rootPane->GetLeafPaneCount();
}
// Method Description:
// - This is a helper to determine which direction an "Automatic" split should
// happen in for the active pane of this tab, but without using the ActualWidth() and
// ActualHeight() methods.
// - See Pane::PreCalculateAutoSplit
// Arguments:
// - availableSpace: The theoretical space that's available for this Tab's content
// Return Value:
// - The SplitState that we should use for an `Automatic` split given
// `availableSpace`
SplitState Tab::PreCalculateAutoSplit(winrt::Windows::Foundation::Size availableSpace) const
{
return _rootPane->PreCalculateAutoSplit(_activePane, availableSpace).value_or(SplitState::Vertical);
}
DEFINE_EVENT(Tab, ActivePaneChanged, _ActivePaneChangedHandlers, winrt::delegate<>);
}

View File

@@ -5,6 +5,12 @@
#include "Pane.h"
#include "Tab.g.h"
// fwdecl unittest classes
namespace TerminalAppLocalTests
{
class TabTests;
};
namespace winrt::TerminalApp::implementation
{
struct Tab : public TabT<Tab>
@@ -32,6 +38,7 @@ namespace winrt::TerminalApp::implementation
winrt::fire_and_forget UpdateIcon(const winrt::hstring iconPath);
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
SplitState PreCalculateAutoSplit(winrt::Windows::Foundation::Size rootSize) const;
void ResizeContent(const winrt::Windows::Foundation::Size& newSize);
void ResizePane(const winrt::TerminalApp::Direction& direction);
@@ -64,5 +71,10 @@ namespace winrt::TerminalApp::implementation
void _AttachEventHandlersToControl(const winrt::Microsoft::Terminal::TerminalControl::TermControl& control);
void _AttachEventHandlersToPane(std::shared_ptr<Pane> pane);
int _GetLeafPaneCount() const noexcept;
void _UpdateActivePane(std::shared_ptr<Pane> pane);
friend class ::TerminalAppLocalTests::TabTests;
};
}

View File

@@ -15,6 +15,7 @@
#include "AzureCloudShellGenerator.h" // For AzureConnectionType
#include "TelnetGenerator.h" // For TelnetConnectionType
#include "TabRowControl.h"
#include "DebugTapConnection.h"
using namespace winrt;
using namespace winrt::Windows::UI::Xaml;
@@ -63,11 +64,20 @@ namespace winrt::TerminalApp::implementation
_tabView = _tabRow.TabView();
_rearranging = false;
// GH#3581 - There's a platform limitation that causes us to crash when we rearrange tabs.
// Xaml tries to send a drag visual (to wit: a screenshot) to the drag hosting process,
// but that process is running at a different IL than us.
// For now, we're disabling elevated drag.
const auto isElevated = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsElevated();
// GH#2455 - Make sure to try/catch calls to Application::Current,
// because that _won't_ be an instance of TerminalApp::App in the
// LocalTests
auto isElevated = false;
try
{
// GH#3581 - There's a platform limitation that causes us to crash when we rearrange tabs.
// Xaml tries to send a drag visual (to wit: a screenshot) to the drag hosting process,
// but that process is running at a different IL than us.
// For now, we're disabling elevated drag.
isElevated = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsUwp();
}
CATCH_LOG();
_tabView.CanReorderTabs(!isElevated);
_tabView.CanDragTabs(!isElevated);
@@ -137,161 +147,115 @@ namespace winrt::TerminalApp::implementation
_tabContent.SizeChanged({ this, &TerminalPage::_OnContentSizeChanged });
// Actually start the terminal.
if (_appArgs.GetStartupActions().empty())
{
_OpenNewTab(nullptr);
}
else
{
_appArgs.ValidateStartupCommands();
// Once the page is actually laid out on the screen, trigger all our
// startup actions. Things like Panes need to know at least how big the
// window will be, so they can subdivide that space.
//
// _OnFirstLayout will remove this handler so it doesn't get called more than once.
_layoutUpdatedRevoker = _tabContent.LayoutUpdated(winrt::auto_revoke, { this, &TerminalPage::_OnFirstLayout });
}
// This will kick off a chain of events to perform each startup
// action. As each startup action is completed, the next will be
// fired.
_ProcessNextStartupAction();
// Method Description:
// - This method is called once on startup, on the first LayoutUpdated event.
// We'll use this event to know that we have an ActualWidth and
// ActualHeight, so we can now attempt to process our list of startup
// actions.
// - We'll remove this event handler when the event is first handled.
// - If there are no startup actions, we'll open a single tab with the
// default profile.
// Arguments:
// - <unused>
// Return Value:
// - <none>
void TerminalPage::_OnFirstLayout(const IInspectable& /*sender*/, const IInspectable& /*eventArgs*/)
{
// Only let this succeed once.
_layoutUpdatedRevoker.revoke();
// This event fires every time the layout changes, but it is always the
// last one to fire in any layout change chain. That gives us great
// flexibility in finding the right point at which to initialize our
// renderer (and our terminal). Any earlier than the last layout update
// and we may not know the terminal's starting size.
if (_startupState == StartupState::NotInitialized)
{
_startupState = StartupState::InStartup;
_appArgs.ValidateStartupCommands();
if (_appArgs.GetStartupActions().empty())
{
_OpenNewTab(nullptr);
_startupState = StartupState::Initialized;
_InitializedHandlers(*this, nullptr);
}
else
{
_ProcessStartupActions();
}
}
}
// Method Description:
// - Process the next startup action in our list of startup actions. When
// that action is complete, fire the next (if there are any more).
// - Process all the startup actions in our list of startup actions. We'll
// do this all at once here.
// Arguments:
// - <none>
// Return Value:
// - <none>
fire_and_forget TerminalPage::_ProcessNextStartupAction()
winrt::fire_and_forget TerminalPage::_ProcessStartupActions()
{
// If there are no actions left, do nothing.
if (_appArgs.GetStartupActions().empty())
{
return;
}
// Get the next action to be processed
auto nextAction = _appArgs.GetStartupActions().front();
_appArgs.GetStartupActions().pop_front();
auto weakThis{ get_weak() };
// Handle it on the UI thread.
co_await winrt::resume_foreground(Dispatcher(), CoreDispatcherPriority::Low);
// Handle it on a subsequent pass of the UI thread.
co_await winrt::resume_foreground(Dispatcher(), CoreDispatcherPriority::Normal);
if (auto page{ weakThis.get() })
{
page->_actionDispatch->DoAction(nextAction);
// Kick off the next action to be handled (if necessary)
page->_ProcessNextStartupAction();
for (const auto& action : _appArgs.GetStartupActions())
{
_actionDispatch->DoAction(action);
}
_startupState = StartupState::Initialized;
_InitializedHandlers(*this, nullptr);
}
}
// Method Description:
// - Show a ContentDialog with a single "Ok" button to dismiss. Looks up the
// the title and text from our Resources using the provided keys.
// - Only one dialog can be visible at a time. If another dialog is visible
// when this is called, nothing happens. See _ShowDialog for details
// Arguments:
// - titleKey: The key to use to lookup the title text from our resources.
// - contentKey: The key to use to lookup the content text from our resources.
void TerminalPage::ShowOkDialog(const winrt::hstring& titleKey,
const winrt::hstring& contentKey)
{
auto title = GetLibraryResourceString(titleKey);
auto message = GetLibraryResourceString(contentKey);
auto buttonText = RS_(L"Ok");
WUX::Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.Content(winrt::box_value(message));
dialog.CloseButtonText(buttonText);
dialog.DefaultButton(WUX::Controls::ContentDialogButton::Close);
_showDialogHandlers(*this, dialog);
}
// Method Description:
// - Show a dialog with "About" information. Displays the app's Display
// Name, version, getting started link, documentation link, release
// Notes link, and privacy policy link.
void TerminalPage::_ShowAboutDialog()
{
const auto title = RS_(L"AboutTitleText");
const auto versionLabel = RS_(L"VersionLabelText");
const auto gettingStartedLabel = RS_(L"GettingStartedLabelText");
const auto documentationLabel = RS_(L"DocumentationLabelText");
const auto releaseNotesLabel = RS_(L"ReleaseNotesLabelText");
const auto privacyPolicyLabel = RS_(L"PrivacyPolicyLabelText");
const auto gettingStartedUriValue = RS_(L"GettingStartedUriValue");
const auto documentationUriValue = RS_(L"DocumentationUriValue");
const auto releaseNotesUriValue = RS_(L"ReleaseNotesUriValue");
const auto privacyPolicyUriValue = RS_(L"PrivacyPolicyUriValue");
const auto package = winrt::Windows::ApplicationModel::Package::Current();
const auto packageName = package.DisplayName();
const auto version = package.Id().Version();
winrt::Windows::UI::Xaml::Documents::Run about;
winrt::Windows::UI::Xaml::Documents::Run gettingStarted;
winrt::Windows::UI::Xaml::Documents::Run documentation;
winrt::Windows::UI::Xaml::Documents::Run releaseNotes;
winrt::Windows::UI::Xaml::Documents::Run privacyPolicy;
winrt::Windows::UI::Xaml::Documents::Hyperlink gettingStartedLink;
winrt::Windows::UI::Xaml::Documents::Hyperlink documentationLink;
winrt::Windows::UI::Xaml::Documents::Hyperlink releaseNotesLink;
winrt::Windows::UI::Xaml::Documents::Hyperlink privacyPolicyLink;
std::wstringstream aboutTextStream;
_showDialogHandlers(*this, FindName(L"AboutDialog").try_as<WUX::Controls::ContentDialog>());
}
gettingStarted.Text(gettingStartedLabel);
documentation.Text(documentationLabel);
releaseNotes.Text(releaseNotesLabel);
privacyPolicy.Text(privacyPolicyLabel);
winrt::hstring TerminalPage::ApplicationDisplayName()
{
try
{
const auto package{ winrt::Windows::ApplicationModel::Package::Current() };
return package.DisplayName();
}
CATCH_LOG();
winrt::Windows::Foundation::Uri gettingStartedUri{ gettingStartedUriValue };
winrt::Windows::Foundation::Uri documentationUri{ documentationUriValue };
winrt::Windows::Foundation::Uri releaseNotesUri{ releaseNotesUriValue };
winrt::Windows::Foundation::Uri privacyPolicyUri{ privacyPolicyUriValue };
return RS_(L"AboutDialog_DisplayNameUnpackaged");
}
gettingStartedLink.NavigateUri(gettingStartedUri);
documentationLink.NavigateUri(documentationUri);
releaseNotesLink.NavigateUri(releaseNotesUri);
privacyPolicyLink.NavigateUri(privacyPolicyUri);
winrt::hstring TerminalPage::ApplicationVersion()
{
try
{
const auto package{ winrt::Windows::ApplicationModel::Package::Current() };
const auto version{ package.Id().Version() };
winrt::hstring formatted{ wil::str_printf<std::wstring>(L"%u.%u.%u.%u", version.Major, version.Minor, version.Build, version.Revision) };
return formatted;
}
CATCH_LOG();
gettingStartedLink.Inlines().Append(gettingStarted);
documentationLink.Inlines().Append(documentation);
releaseNotesLink.Inlines().Append(releaseNotes);
privacyPolicyLink.Inlines().Append(privacyPolicy);
// Format our about text. It will look like the following:
// <Display Name>
// Version: <Major>.<Minor>.<Build>.<Revision>
// Getting Started
// Documentation
// Release Notes
// Privacy Policy
aboutTextStream << packageName.c_str() << L"\n";
aboutTextStream << versionLabel.c_str() << L" ";
aboutTextStream << version.Major << L"." << version.Minor << L"." << version.Build << L"." << version.Revision << L"\n";
winrt::hstring aboutText{ aboutTextStream.str() };
about.Text(aboutText);
const auto buttonText = RS_(L"Ok");
WUX::Controls::TextBlock aboutTextBlock;
aboutTextBlock.Inlines().Append(about);
aboutTextBlock.Inlines().Append(gettingStartedLink);
aboutTextBlock.Inlines().Append(documentationLink);
aboutTextBlock.Inlines().Append(releaseNotesLink);
aboutTextBlock.Inlines().Append(privacyPolicyLink);
aboutTextBlock.IsTextSelectionEnabled(true);
WUX::Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.Content(aboutTextBlock);
dialog.CloseButtonText(buttonText);
dialog.DefaultButton(WUX::Controls::ContentDialogButton::Close);
_showDialogHandlers(*this, dialog);
return RS_(L"AboutDialog_VersionUnknown");
}
// Method Description:
@@ -303,19 +267,7 @@ namespace winrt::TerminalApp::implementation
// when this is called, nothing happens. See _ShowDialog for details
void TerminalPage::_ShowCloseWarningDialog()
{
auto title = RS_(L"CloseWindowWarningTitle");
auto primaryButtonText = RS_(L"CloseAll");
auto closeButtonText = RS_(L"Cancel");
WUX::Controls::ContentDialog dialog;
dialog.Title(winrt::box_value(title));
dialog.CloseButtonText(closeButtonText);
dialog.PrimaryButtonText(primaryButtonText);
dialog.DefaultButton(WUX::Controls::ContentDialogButton::Primary);
auto token = dialog.PrimaryButtonClick({ this, &TerminalPage::_CloseWarningPrimaryButtonOnClick });
_showDialogHandlers(*this, dialog);
_showDialogHandlers(*this, FindName(L"CloseAllDialog").try_as<WUX::Controls::ContentDialog>());
}
// Method Description:
@@ -406,7 +358,15 @@ namespace winrt::TerminalApp::implementation
// add static items
{
const auto isUwp = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsUwp();
// GH#2455 - Make sure to try/catch calls to Application::Current,
// because that _won't_ be an instance of TerminalApp::App in the
// LocalTests
auto isUwp = false;
try
{
isUwp = ::winrt::Windows::UI::Xaml::Application::Current().as<::winrt::TerminalApp::App>().Logic().IsUwp();
}
CATCH_LOG();
if (!isUwp)
{
@@ -476,6 +436,7 @@ namespace winrt::TerminalApp::implementation
// control which profile is created and with possible other
// configurations. See CascadiaSettings::BuildSettings for more details.
void TerminalPage::_OpenNewTab(const winrt::TerminalApp::NewTerminalArgs& newTerminalArgs)
try
{
const auto [profileGuid, settings] = _settings->BuildSettings(newTerminalArgs);
@@ -499,6 +460,7 @@ namespace winrt::TerminalApp::implementation
TraceLoggingKeyword(MICROSOFT_KEYWORD_MEASURES),
TelemetryPrivacyDataTag(PDT_ProductAndServicePerformance));
}
CATCH_LOG();
winrt::fire_and_forget TerminalPage::_RemoveOnCloseRoutine(Microsoft::UI::Xaml::Controls::TabViewItem tabViewItem, winrt::com_ptr<TerminalPage> page)
{
@@ -514,11 +476,25 @@ namespace winrt::TerminalApp::implementation
// - settings: the TerminalSettings object to use to create the TerminalControl with.
void TerminalPage::_CreateNewTabFromSettings(GUID profileGuid, TerminalSettings settings)
{
const bool isFirstTab = _tabs.Size() == 0;
// Initialize the new tab
// Create a connection based on the values in our settings object.
const auto connection = _CreateConnectionFromSettings(profileGuid, settings);
auto connection = _CreateConnectionFromSettings(profileGuid, settings);
TerminalConnection::ITerminalConnection debugConnection{ nullptr };
if (_settings->GlobalSettings().DebugFeaturesEnabled())
{
const CoreWindow window = CoreWindow::GetForCurrentThread();
const auto rAltState = window.GetKeyState(VirtualKey::RightMenu);
const auto lAltState = window.GetKeyState(VirtualKey::LeftMenu);
const bool bothAltsPressed = WI_IsFlagSet(lAltState, CoreVirtualKeyStates::Down) &&
WI_IsFlagSet(rAltState, CoreVirtualKeyStates::Down);
if (bothAltsPressed)
{
std::tie(connection, debugConnection) = OpenDebugTapConnection(connection);
}
}
TermControl term{ settings, connection };
// Add the new tab to the list of our tabs.
@@ -568,20 +544,17 @@ namespace winrt::TerminalApp::implementation
}
});
// If this is the first tab, we don't need to kick off the event to get
// the tab's content added to the root of the page. just do it
// immediately.
if (isFirstTab)
if (debugConnection) // this will only be set if global debugging is on and tap is active
{
_tabContent.Children().Clear();
_tabContent.Children().Append(newTabImpl->GetRootElement());
}
else
{
// This kicks off TabView::SelectionChanged, in response to which
// we'll attach the terminal's Xaml control to the Xaml root.
_tabView.SelectedItem(tabViewItem);
TermControl newControl{ settings, debugConnection };
_RegisterTerminalEvents(newControl, *newTabImpl);
// Split (auto) with the debug tap.
newTabImpl->SplitPane(SplitState::Automatic, profileGuid, newControl);
}
// This kicks off TabView::SelectionChanged, in response to which
// we'll attach the terminal's Xaml control to the Xaml root.
_tabView.SelectedItem(tabViewItem);
}
// Method Description:
@@ -810,13 +783,30 @@ namespace winrt::TerminalApp::implementation
{
if (auto index{ _GetFocusedTabIndex() })
{
auto focusedTab = _GetStrongTabImpl(*index);
const auto& profileGuid = focusedTab->GetFocusedProfile();
if (profileGuid.has_value())
try
{
const auto settings = _settings->BuildSettings(profileGuid.value());
_CreateNewTabFromSettings(profileGuid.value(), settings);
auto focusedTab = _GetStrongTabImpl(*index);
// TODO: GH#5047 - In the future, we should get the Profile of
// the focused pane, and use that to build a new instance of the
// settings so we can duplicate this tab/pane.
//
// Currently, if the profile doesn't exist anymore in our
// settings, we'll silently do nothing.
//
// In the future, it will be preferable to just duplicate the
// current control's settings, but we can't do that currently,
// because we won't be able to create a new instance of the
// connection without keeping an instance of the original Profile
// object around.
const auto& profileGuid = focusedTab->GetFocusedProfile();
if (profileGuid.has_value())
{
const auto settings = _settings->BuildSettings(profileGuid.value());
_CreateNewTabFromSettings(profileGuid.value(), settings);
}
}
CATCH_LOG();
}
}
@@ -901,20 +891,36 @@ namespace winrt::TerminalApp::implementation
// Wraparound math. By adding tabCount and then calculating modulo tabCount,
// we clamp the values to the range [0, tabCount) while still supporting moving
// leftward from 0 to tabCount - 1.
_SetFocusedTabIndex(((tabCount + *index + (bMoveRight ? 1 : -1)) % tabCount));
const auto newTabIndex = ((tabCount + *index + (bMoveRight ? 1 : -1)) % tabCount);
_SelectTab(newTabIndex);
}
}
// Method Description:
// - Sets focus to the desired tab. Returns false if the provided tabIndex
// is greater than the number of tabs we have.
// - During startup, we'll immediately set the selected tab as focused.
// - After startup, we'll dispatch an async method to set the the selected
// item of the TabView, which will then also trigger a
// TabView::SelectionChanged, handled in
// TerminalPage::_OnTabSelectionChanged
// Return Value:
// true iff we were able to select that tab index, false otherwise
bool TerminalPage::_SelectTab(const uint32_t tabIndex)
{
if (tabIndex >= 0 && tabIndex < _tabs.Size())
{
_SetFocusedTabIndex(tabIndex);
if (_startupState == StartupState::InStartup)
{
auto tab{ _GetStrongTabImpl(tabIndex) };
_tabView.SelectedItem(tab->GetTabViewItem());
_UpdatedSelectedTab(tabIndex);
}
else
{
_SetFocusedTabIndex(tabIndex);
}
return true;
}
return false;
@@ -967,6 +973,16 @@ namespace winrt::TerminalApp::implementation
return std::nullopt;
}
// Method Description:
// - An async method for changing the focused tab on the UI thread. This
// method will _only_ set the selected item of the TabView, which will
// then also trigger a TabView::SelectionChanged event, which we'll handle
// in TerminalPage::_OnTabSelectionChanged, where we'll mark the new tab
// as focused.
// Arguments:
// - tabIndex: the index in the list of tabs to focus.
// Return Value:
// - <none>
winrt::fire_and_forget TerminalPage::_SetFocusedTabIndex(const uint32_t tabIndex)
{
// GH#1117: This is a workaround because _tabView.SelectedIndex(tabIndex)
@@ -1075,42 +1091,65 @@ namespace winrt::TerminalApp::implementation
return;
}
auto focusedTab = _GetStrongTabImpl(*indexOpt);
winrt::Microsoft::Terminal::Settings::TerminalSettings controlSettings;
GUID realGuid;
bool profileFound = false;
if (splitMode == TerminalApp::SplitType::Duplicate)
try
{
std::optional<GUID> current_guid = focusedTab->GetFocusedProfile();
if (current_guid)
auto focusedTab = _GetStrongTabImpl(*indexOpt);
winrt::Microsoft::Terminal::Settings::TerminalSettings controlSettings;
GUID realGuid;
bool profileFound = false;
if (splitMode == TerminalApp::SplitType::Duplicate)
{
profileFound = true;
controlSettings = _settings->BuildSettings(current_guid.value());
realGuid = current_guid.value();
std::optional<GUID> current_guid = focusedTab->GetFocusedProfile();
if (current_guid)
{
profileFound = true;
controlSettings = _settings->BuildSettings(current_guid.value());
realGuid = current_guid.value();
}
// TODO: GH#5047 - In the future, we should get the Profile of
// the focused pane, and use that to build a new instance of the
// settings so we can duplicate this tab/pane.
//
// Currently, if the profile doesn't exist anymore in our
// settings, we'll silently do nothing.
//
// In the future, it will be preferable to just duplicate the
// current control's settings, but we can't do that currently,
// because we won't be able to create a new instance of the
// connection without keeping an instance of the original Profile
// object around.
}
if (!profileFound)
{
std::tie(realGuid, controlSettings) = _settings->BuildSettings(newTerminalArgs);
}
const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings);
const auto canSplit = focusedTab->CanSplitPane(splitType);
if (!canSplit && _startupState == StartupState::Initialized)
{
return;
}
auto realSplitType = splitType;
if (realSplitType == SplitState::Automatic && _startupState < StartupState::Initialized)
{
float contentWidth = gsl::narrow_cast<float>(_tabContent.ActualWidth());
float contentHeight = gsl::narrow_cast<float>(_tabContent.ActualHeight());
realSplitType = focusedTab->PreCalculateAutoSplit({ contentWidth, contentHeight });
}
TermControl newControl{ controlSettings, controlConnection };
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(newControl, *focusedTab);
focusedTab->SplitPane(realSplitType, realGuid, newControl);
}
if (!profileFound)
{
std::tie(realGuid, controlSettings) = _settings->BuildSettings(newTerminalArgs);
}
const auto controlConnection = _CreateConnectionFromSettings(realGuid, controlSettings);
const auto canSplit = focusedTab->CanSplitPane(splitType);
if (!canSplit)
{
return;
}
TermControl newControl{ controlSettings, controlConnection };
// Hookup our event handlers to the new terminal
_RegisterTerminalEvents(newControl, *focusedTab);
focusedTab->SplitPane(splitType, realGuid, newControl);
CATCH_LOG();
}
// Method Description:
@@ -1426,6 +1465,31 @@ namespace winrt::TerminalApp::implementation
}
}
void TerminalPage::_UpdatedSelectedTab(const int32_t index)
{
// Unfocus all the tabs.
for (auto tab : _tabs)
{
auto tabImpl{ _GetStrongTabImpl(tab) };
tabImpl->SetFocused(false);
}
if (index >= 0)
{
try
{
auto tab{ _GetStrongTabImpl(index) };
_tabContent.Children().Clear();
_tabContent.Children().Append(tab->GetRootElement());
tab->SetFocused(true);
_titleChangeHandlers(*this, Title());
}
CATCH_LOG();
}
}
// Method Description:
// - Responds to the TabView control's Selection Changed event (to move a
// new terminal control into focus) when not in in the middle of a tab rearrangement.
@@ -1438,28 +1502,7 @@ namespace winrt::TerminalApp::implementation
{
auto tabView = sender.as<MUX::Controls::TabView>();
auto selectedIndex = tabView.SelectedIndex();
// Unfocus all the tabs.
for (auto tab : _tabs)
{
auto tabImpl{ _GetStrongTabImpl(tab) };
tabImpl->SetFocused(false);
}
if (selectedIndex >= 0)
{
try
{
auto tab{ _GetStrongTabImpl(selectedIndex) };
_tabContent.Children().Clear();
_tabContent.Children().Append(tab->GetRootElement());
tab->SetFocused(true);
_titleChangeHandlers(*this, Title());
}
CATCH_LOG();
}
_UpdatedSelectedTab(selectedIndex);
}
}
@@ -1525,16 +1568,28 @@ namespace winrt::TerminalApp::implementation
for (auto& profile : profiles)
{
const GUID profileGuid = profile.GetGuid();
const auto settings = _settings->BuildSettings(profileGuid);
for (auto tab : _tabs)
try
{
// Attempt to reload the settings of any panes with this profile
auto tabImpl{ _GetStrongTabImpl(tab) };
tabImpl->UpdateSettings(settings, profileGuid);
// BuildSettings can throw an exception if the profileGuid does
// not belong to an actual profile in the list of profiles.
const auto settings = _settings->BuildSettings(profileGuid);
for (auto tab : _tabs)
{
// Attempt to reload the settings of any panes with this profile
auto tabImpl{ _GetStrongTabImpl(tab) };
tabImpl->UpdateSettings(settings, profileGuid);
}
}
CATCH_LOG();
}
// GH#2455: If there are any panes with controls that had been
// initialized with a Profile that no longer exists in our list of
// profiles, we'll leave it unmodified. The profile doesn't exist
// anymore, so we can't possibly update its settings.
// Update the icon of the tab for the currently focused profile in that tab.
for (auto tab : _tabs)
{

View File

@@ -20,6 +20,13 @@ namespace TerminalAppLocalTests
namespace winrt::TerminalApp::implementation
{
enum StartupState : int
{
NotInitialized = 0,
InStartup = 1,
Initialized = 2
};
struct TerminalPage : TerminalPageT<TerminalPage>
{
public:
@@ -31,12 +38,13 @@ namespace winrt::TerminalApp::implementation
hstring Title();
void ShowOkDialog(const winrt::hstring& titleKey, const winrt::hstring& contentKey);
void TitlebarClicked();
float CalcSnappedDimension(const bool widthOrHeight, const float dimension) const;
winrt::hstring ApplicationDisplayName();
winrt::hstring ApplicationVersion();
void CloseWindow();
int32_t SetStartupCommandline(winrt::array_view<const hstring> args);
@@ -48,8 +56,11 @@ namespace winrt::TerminalApp::implementation
DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(SetTitleBarContent, _setTitleBarContentHandlers, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::UIElement);
DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(ShowDialog, _showDialogHandlers, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::Controls::ContentDialog);
DECLARE_EVENT_WITH_TYPED_EVENT_HANDLER(ToggleFullscreen, _toggleFullscreenHandlers, winrt::Windows::Foundation::IInspectable, winrt::TerminalApp::ToggleFullscreenEventArgs);
TYPED_EVENT(Initialized, winrt::Windows::Foundation::IInspectable, winrt::Windows::UI::Xaml::RoutedEventArgs);
private:
friend struct TerminalPageT<TerminalPage>; // for Xaml to bind events
// If you add controls here, but forget to null them either here or in
// the ctor, you're going to have a bad time. It'll mysteriously fail to
// activate the app.
@@ -75,9 +86,12 @@ namespace winrt::TerminalApp::implementation
winrt::com_ptr<ShortcutActionDispatch> _actionDispatch{ winrt::make_self<ShortcutActionDispatch>() };
winrt::Windows::UI::Xaml::Controls::Grid::LayoutUpdated_revoker _layoutUpdatedRevoker;
StartupState _startupState{ StartupState::NotInitialized };
::TerminalApp::AppCommandlineArgs _appArgs;
int _ParseArgs(winrt::array_view<const hstring>& args);
fire_and_forget _ProcessNextStartupAction();
winrt::fire_and_forget _ProcessStartupActions();
void _ShowAboutDialog();
void _ShowCloseWarningDialog();
@@ -141,6 +155,8 @@ namespace winrt::TerminalApp::implementation
void _OnTabItemsChanged(const IInspectable& sender, const Windows::Foundation::Collections::IVectorChangedEventArgs& eventArgs);
void _OnContentSizeChanged(const IInspectable& /*sender*/, Windows::UI::Xaml::SizeChangedEventArgs const& e);
void _OnTabCloseRequested(const IInspectable& sender, const Microsoft::UI::Xaml::Controls::TabViewTabCloseRequestedEventArgs& eventArgs);
void _OnFirstLayout(const IInspectable& sender, const IInspectable& eventArgs);
void _UpdatedSelectedTab(const int32_t index);
void _Find();

View File

@@ -13,10 +13,15 @@ namespace TerminalApp
Int32 SetStartupCommandline(String[] commands);
String EarlyExitMessage { get; };
// XAML bound properties
String ApplicationDisplayName { get; };
String ApplicationVersion { get; };
event Windows.Foundation.TypedEventHandler<Object, String> TitleChanged;
event Windows.Foundation.TypedEventHandler<Object, LastTabClosedEventArgs> LastTabClosed;
event Windows.Foundation.TypedEventHandler<Object, Windows.UI.Xaml.UIElement> SetTitleBarContent;
event Windows.Foundation.TypedEventHandler<Object, Windows.UI.Xaml.Controls.ContentDialog> ShowDialog;
event Windows.Foundation.TypedEventHandler<Object, ToggleFullscreenEventArgs> ToggleFullscreen;
event Windows.Foundation.TypedEventHandler<Object, Windows.UI.Xaml.RoutedEventArgs> Initialized;
}
}

View File

@@ -19,5 +19,39 @@ the MIT License. See LICENSE in the project root for license information. -->
<local:TabRowControl x:Name="TabRow" Grid.Row="0" />
<Grid x:Name="TabContent" Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch" />
<ContentDialog
x:Load="False"
x:Name="AboutDialog"
x:Uid="AboutDialog"
DefaultButton="Close">
<StackPanel Orientation="Vertical">
<TextBlock IsTextSelectionEnabled="True">
<Run Text="{x:Bind ApplicationDisplayName}" /> <LineBreak />
<Run x:Uid="AboutDialog_VersionLabel" />
<Run Text="{x:Bind ApplicationVersion}" />
</TextBlock>
<HyperlinkButton
x:Uid="AboutDialog_GettingStartedLink"
NavigateUri="https://aka.ms/terminal-getting-started" />
<HyperlinkButton
x:Uid="AboutDialog_DocumentationLink"
NavigateUri="https://aka.ms/terminal-documentation" />
<HyperlinkButton
x:Uid="AboutDialog_ReleaseNotesLink"
NavigateUri="https://aka.ms/terminal-release-notes" />
<HyperlinkButton
x:Uid="AboutDialog_PrivacyPolicyLink"
NavigateUri="https://aka.ms/terminal-privacy-policy" />
</StackPanel>
</ContentDialog>
<ContentDialog
x:Load="False"
x:Name="CloseAllDialog"
x:Uid="CloseAllDialog"
DefaultButton="Primary"
PrimaryButtonClick="_CloseWarningPrimaryButtonOnClick">
</ContentDialog>
</Grid>
</Page>

View File

@@ -107,6 +107,7 @@
<ClInclude Include="../ActionAndArgs.h">
<DependentUpon>../ActionArgs.idl</DependentUpon>
</ClInclude>
<ClInclude Include="../DebugTapConnection.h" />
<ClInclude Include="../AppKeyBindings.h">
<DependentUpon>../AppKeyBindings.idl</DependentUpon>
</ClInclude>
@@ -153,6 +154,7 @@
<ClCompile Include="../WslDistroGenerator.cpp" />
<ClCompile Include="../AzureCloudShellGenerator.cpp" />
<ClCompile Include="../Pane.LayoutSizeNode.cpp" />
<ClCompile Include="../DebugTapConnection.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>

View File

@@ -172,8 +172,13 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
TextBlock().FontSize(fontSizePx);
TextBlock().FontFamily(Media::FontFamily(fontArgs->FontFace()));
const auto widthToTerminalEnd = Canvas().ActualWidth() - ::base::ClampedNumeric<double>(clientCursorPos.X);
TextBlock().MaxWidth(widthToTerminalEnd);
const auto canvasActualWidth = Canvas().ActualWidth();
const auto widthToTerminalEnd = canvasActualWidth - ::base::ClampedNumeric<double>(clientCursorPos.X);
// Make sure that we're setting the MaxWidth to a positive number - a
// negative number here will crash us in mysterious ways with a useless
// stack trace
const auto newMaxWidth = std::max<double>(0.0, widthToTerminalEnd);
TextBlock().MaxWidth(newMaxWidth);
// Set the text block bounds
const auto yOffset = ::base::ClampedNumeric<float>(TextBlock().ActualHeight()) - fontHeight;

View File

@@ -70,13 +70,36 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_lastMouseClickTimestamp{},
_lastMouseClickPos{},
_selectionNeedsToBeCopied{ false },
_searchBox{ nullptr },
_textCursor{ Windows::UI::Core::CoreCursorType::IBeam, 0 },
_pointerCursor{ Windows::UI::Core::CoreCursorType::Arrow, 0 }
_searchBox{ nullptr }
{
_EnsureStaticInitialization();
InitializeComponent();
_terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1);
_terminal->SetTitleChangedCallback(pfnTitleChanged);
auto pfnBackgroundColorChanged = std::bind(&TermControl::_BackgroundColorChanged, this, std::placeholders::_1);
_terminal->SetBackgroundCallback(pfnBackgroundColorChanged);
auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
_terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged);
// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
_terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);
auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1);
_terminal->SetWriteInputCallback(inputFn);
// Subscribe to the connection's disconnected event and call our connection closed handlers.
_connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) {
_ConnectionStateChangedHandlers(*this, nullptr);
});
// Initialize the terminal only once the swapchainpanel is loaded - that
// way, we'll be able to query the real pixel size it got on layout
_layoutUpdatedRevoker = SwapChainPanel().LayoutUpdated(winrt::auto_revoke, [this](auto /*s*/, auto /*e*/) {
@@ -92,10 +115,9 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
});
// Subscribe to the connection's disconnected event and call our connection closed handlers.
_connectionStateChangedRevoker = _connection.StateChanged(winrt::auto_revoke, [this](auto&& /*s*/, auto&& /*v*/) {
_ConnectionStateChangedHandlers(*this, nullptr);
});
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
_autoScrollTimer.Interval(AutoScrollUpdateInterval);
_autoScrollTimer.Tick({ this, &TermControl::_UpdateAutoScroll });
_ApplyUISettings();
}
@@ -128,7 +150,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - <none>
void TermControl::_Search(const winrt::hstring& text, const bool goForward, const bool caseSensitive)
{
if (text.size() == 0)
if (text.size() == 0 || _closing)
{
return;
}
@@ -185,6 +207,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// If 'weakThis' is locked, then we can safely work with 'this'
if (auto control{ weakThis.get() })
{
if (_closing)
{
return;
}
// Update our control settings
_ApplyUISettings();
@@ -414,7 +441,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
Windows::UI::Xaml::Automation::Peers::AutomationPeer TermControl::OnCreateAutomationPeer()
try
{
if (GetUiaData())
if (_initializedTerminal && !_closing) // only set up the automation peer if we're ready to go live
{
// create a custom automation peer with this code pattern:
// (https://docs.microsoft.com/en-us/windows/uwp/design/accessibility/custom-automation-peers)
@@ -452,11 +479,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
return _connection.State();
}
winrt::fire_and_forget TermControl::SwapChainChanged()
winrt::fire_and_forget TermControl::RenderEngineSwapChainChanged()
{
if (!_initializedTerminal)
{
return;
{ // lock scope
auto terminalLock = _terminal->LockForReading();
if (!_initializedTerminal)
{
return;
}
}
auto chain = _renderEngine->GetSwapChain();
@@ -464,192 +494,153 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
co_await winrt::resume_foreground(Dispatcher());
// If 'weakThis' is locked, then we can safely work with 'this'
if (auto control{ weakThis.get() })
{
if (_terminal)
{
auto lock = _terminal->LockForWriting();
auto nativePanel = SwapChainPanel().as<ISwapChainPanelNative>();
nativePanel->SetSwapChain(chain.Get());
}
auto terminalLock = _terminal->LockForWriting();
_AttachDxgiSwapChainToXaml(chain.Get());
}
}
winrt::fire_and_forget TermControl::_SwapChainRoutine()
void TermControl::_AttachDxgiSwapChainToXaml(IDXGISwapChain1* swapChain)
{
auto chain = _renderEngine->GetSwapChain();
auto weakThis{ get_weak() };
co_await winrt::resume_foreground(Dispatcher());
if (auto control{ weakThis.get() })
{
if (_terminal)
{
auto lock = _terminal->LockForWriting();
auto nativePanel = SwapChainPanel().as<ISwapChainPanelNative>();
nativePanel->SetSwapChain(chain.Get());
}
}
auto nativePanel = SwapChainPanel().as<ISwapChainPanelNative>();
nativePanel->SetSwapChain(swapChain);
}
bool TermControl::_InitializeTerminal()
{
if (_initializedTerminal)
{
return false;
}
{ // scope for terminalLock
auto terminalLock = _terminal->LockForWriting();
const auto windowWidth = SwapChainPanel().ActualWidth(); // Width() and Height() are NaN?
const auto windowHeight = SwapChainPanel().ActualHeight();
if (windowWidth == 0 || windowHeight == 0)
{
return false;
}
_terminal = std::make_unique<::Microsoft::Terminal::Core::Terminal>();
// First create the render thread.
// Then stash a local pointer to the render thread so we can initialize it and enable it
// to paint itself *after* we hand off its ownership to the renderer.
// We split up construction and initialization of the render thread object this way
// because the renderer and render thread have circular references to each other.
auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>();
auto* const localPointerToThread = renderThread.get();
// Now create the renderer and initialize the render thread.
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread));
::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer;
THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
// Set up the DX Engine
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
_renderer->AddRenderEngine(dxEngine.get());
// Initialize our font with the renderer
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
// and react accordingly.
_UpdateFont(true);
const COORD windowSize{ static_cast<short>(windowWidth), static_cast<short>(windowHeight) };
// Fist set up the dx engine with the window size in pixels.
// Then, using the font, get the number of characters that can fit.
// Resize our terminal connection to match that size, and initialize the terminal with that size.
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
// Update DxEngine's SelectionBackground
dxEngine->SetSelectionBackground(_settings.SelectionBackground());
const auto vp = dxEngine->GetViewportInCharacters(viewInPixels);
const auto width = vp.Width();
const auto height = vp.Height();
_connection.Resize(height, width);
// Override the default width and height to match the size of the swapChainPanel
_settings.InitialCols(width);
_settings.InitialRows(height);
_terminal->CreateFromSettings(_settings, renderTarget);
// Tell the DX Engine to notify us when the swap chain changes.
dxEngine->SetCallback(std::bind(&TermControl::SwapChainChanged, this));
// TODO:GH#3927 - Make it possible to hot-reload this setting. Right
// here, the setting will only be used when the Terminal is initialized.
dxEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect());
// TODO:GH#3927 - hot-reload this one too
// Update DxEngine's AntialiasingMode
switch (_settings.AntialiasingMode())
{
case TextAntialiasingMode::Cleartype:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
break;
case TextAntialiasingMode::Aliased:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED);
break;
case TextAntialiasingMode::Grayscale:
default:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
break;
}
THROW_IF_FAILED(dxEngine->Enable());
_renderEngine = std::move(dxEngine);
// This event is explicitly revoked in the destructor: does not need weak_ref
auto onReceiveOutputFn = [this](const hstring str) {
_terminal->Write(str);
};
_connectionOutputEventToken = _connection.TerminalOutput(onReceiveOutputFn);
auto inputFn = std::bind(&TermControl::_SendInputToConnection, this, std::placeholders::_1);
_terminal->SetWriteInputCallback(inputFn);
_terminal->SetMouseModeChangedCallback([weakThis = get_weak()]() {
if (auto strongThis{ weakThis.get() })
if (_initializedTerminal)
{
strongThis->_TerminalMouseModeChanged();
return false;
}
});
_SwapChainRoutine();
const auto windowWidth = SwapChainPanel().ActualWidth(); // Width() and Height() are NaN?
const auto windowHeight = SwapChainPanel().ActualHeight();
// Set up the height of the ScrollViewer and the grid we're using to fake our scrolling height
auto bottom = _terminal->GetViewport().BottomExclusive();
auto bufferHeight = bottom;
if (windowWidth == 0 || windowHeight == 0)
{
return false;
}
ScrollBar().Maximum(bufferHeight - bufferHeight);
ScrollBar().Minimum(0);
ScrollBar().Value(0);
ScrollBar().ViewportSize(bufferHeight);
// First create the render thread.
// Then stash a local pointer to the render thread so we can initialize it and enable it
// to paint itself *after* we hand off its ownership to the renderer.
// We split up construction and initialization of the render thread object this way
// because the renderer and render thread have circular references to each other.
auto renderThread = std::make_unique<::Microsoft::Console::Render::RenderThread>();
auto* const localPointerToThread = renderThread.get();
localPointerToThread->EnablePainting();
// Now create the renderer and initialize the render thread.
_renderer = std::make_unique<::Microsoft::Console::Render::Renderer>(_terminal.get(), nullptr, 0, std::move(renderThread));
::Microsoft::Console::Render::IRenderTarget& renderTarget = *_renderer;
auto pfnTitleChanged = std::bind(&TermControl::_TerminalTitleChanged, this, std::placeholders::_1);
_terminal->SetTitleChangedCallback(pfnTitleChanged);
THROW_IF_FAILED(localPointerToThread->Initialize(_renderer.get()));
auto pfnBackgroundColorChanged = std::bind(&TermControl::_BackgroundColorChanged, this, std::placeholders::_1);
_terminal->SetBackgroundCallback(pfnBackgroundColorChanged);
// Set up the DX Engine
auto dxEngine = std::make_unique<::Microsoft::Console::Render::DxEngine>();
_renderer->AddRenderEngine(dxEngine.get());
auto pfnScrollPositionChanged = std::bind(&TermControl::_TerminalScrollPositionChanged, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3);
_terminal->SetScrollPositionChangedCallback(pfnScrollPositionChanged);
// Initialize our font with the renderer
// We don't have to care about DPI. We'll get a change message immediately if it's not 96
// and react accordingly.
_UpdateFont(true);
static constexpr auto AutoScrollUpdateInterval = std::chrono::microseconds(static_cast<int>(1.0 / 30.0 * 1000000));
_autoScrollTimer.Interval(AutoScrollUpdateInterval);
_autoScrollTimer.Tick({ get_weak(), &TermControl::_UpdateAutoScroll });
const COORD windowSize{ static_cast<short>(windowWidth), static_cast<short>(windowHeight) };
// Set up blinking cursor
int blinkTime = GetCaretBlinkTime();
if (blinkTime != INFINITE)
{
// Create a timer
_cursorTimer = std::make_optional(DispatcherTimer());
_cursorTimer.value().Interval(std::chrono::milliseconds(blinkTime));
_cursorTimer.value().Tick({ get_weak(), &TermControl::_BlinkCursor });
_cursorTimer.value().Start();
}
else
{
// The user has disabled cursor blinking
_cursorTimer = std::nullopt;
}
// Fist set up the dx engine with the window size in pixels.
// Then, using the font, get the number of characters that can fit.
// Resize our terminal connection to match that size, and initialize the terminal with that size.
const auto viewInPixels = Viewport::FromDimensions({ 0, 0 }, windowSize);
THROW_IF_FAILED(dxEngine->SetWindowSize({ viewInPixels.Width(), viewInPixels.Height() }));
// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime() * 1000;
// Update DxEngine's SelectionBackground
dxEngine->SetSelectionBackground(_settings.SelectionBackground());
// Focus the control here. If we do it during control initialization, then
// focus won't actually get passed to us. I believe this is because
// we're not technically a part of the UI tree yet, so focusing us
// becomes a no-op.
this->Focus(FocusState::Programmatic);
const auto vp = dxEngine->GetViewportInCharacters(viewInPixels);
const auto width = vp.Width();
const auto height = vp.Height();
_connection.Resize(height, width);
// Override the default width and height to match the size of the swapChainPanel
_settings.InitialCols(width);
_settings.InitialRows(height);
_terminal->CreateFromSettings(_settings, renderTarget);
// TODO:GH#3927 - Make it possible to hot-reload this setting. Right
// here, the setting will only be used when the Terminal is initialized.
dxEngine->SetRetroTerminalEffects(_settings.RetroTerminalEffect());
// TODO:GH#3927 - hot-reload this one too
// Update DxEngine's AntialiasingMode
switch (_settings.AntialiasingMode())
{
case TextAntialiasingMode::Cleartype:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_CLEARTYPE);
break;
case TextAntialiasingMode::Aliased:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_ALIASED);
break;
case TextAntialiasingMode::Grayscale:
default:
dxEngine->SetAntialiasingMode(D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE);
break;
}
THROW_IF_FAILED(dxEngine->Enable());
_renderEngine = std::move(dxEngine);
_AttachDxgiSwapChainToXaml(_renderEngine->GetSwapChain().Get());
// Tell the DX Engine to notify us when the swap chain changes.
// We do this after we initially set the swapchain so as to avoid unnecessary callbacks (and locking problems)
_renderEngine->SetCallback(std::bind(&TermControl::RenderEngineSwapChainChanged, this));
auto bottom = _terminal->GetViewport().BottomExclusive();
auto bufferHeight = bottom;
ScrollBar().Maximum(bufferHeight - bufferHeight);
ScrollBar().Minimum(0);
ScrollBar().Value(0);
ScrollBar().ViewportSize(bufferHeight);
localPointerToThread->EnablePainting();
// Set up blinking cursor
int blinkTime = GetCaretBlinkTime();
if (blinkTime != INFINITE)
{
// Create a timer
DispatcherTimer cursorTimer;
cursorTimer.Interval(std::chrono::milliseconds(blinkTime));
cursorTimer.Tick({ get_weak(), &TermControl::_CursorTimerTick });
cursorTimer.Start();
_cursorTimer.emplace(std::move(cursorTimer));
}
else
{
// The user has disabled cursor blinking
_cursorTimer = std::nullopt;
}
// import value from WinUser (convert from milli-seconds to micro-seconds)
_multiClickTimer = GetDoubleClickTime() * 1000;
// Focus the control here. If we do it during control initialization, then
// focus won't actually get passed to us. I believe this is because
// we're not technically a part of the UI tree yet, so focusing us
// becomes a no-op.
this->Focus(FocusState::Programmatic);
_connection.Start();
_initializedTerminal = true;
} // scope for TerminalLock
// call this event dispatcher outside of lock
_connection.Start();
_initializedTerminal = true;
_InitializedHandlers(*this, nullptr);
return true;
}
@@ -722,11 +713,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
e.OriginalKey() == VirtualKey::RightWindows)
{
if (!_closing && e.OriginalKey() == VirtualKey::Shift)
{
// If the user presses or releases shift, check whether we're in mouse mode and the cursor needs updating
_TerminalMouseModeChanged();
}
e.Handled(true);
return;
}
@@ -778,23 +764,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
e.Handled(handled);
}
void TermControl::_KeyUpHandler(winrt::Windows::Foundation::IInspectable const& /*sender*/,
Input::KeyRoutedEventArgs const& e)
{
// If the current focused element is a child element of searchbox,
// we do not send this event up to terminal
if (_searchBox && _searchBox->ContainsFocus())
{
return;
}
if (!_closing && e.OriginalKey() == VirtualKey::Shift)
{
// If the user presses or releases shift, check whether we're in mouse mode and the cursor needs updating
_TerminalMouseModeChanged();
}
}
// Method Description:
// - Send this particular key event to the terminal.
// See Terminal::SendKeyEvent for more information.
@@ -908,6 +877,10 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - point: the PointerPoint object representing a mouse event from our XAML input handler
bool TermControl::_CanSendVTMouseInput()
{
if (!_terminal)
{
return false;
}
// If the user is holding down Shift, suppress mouse events
// TODO GH#4875: disable/customize this functionality
const auto modifiers = _GetPressedModifierKeys();
@@ -918,18 +891,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
return _terminal->IsTrackingMouseInput();
}
// Method Description:
// - Handles changes in mouse mode state
winrt::fire_and_forget TermControl::_TerminalMouseModeChanged()
{
co_await Dispatcher();
if (_oldCursor) // if we have an active cursor transition
{
auto coreWindow = Window::Current().CoreWindow();
coreWindow.PointerCursor(_CanSendVTMouseInput() ? _pointerCursor : _textCursor);
}
}
// Method Description:
// - handle a mouse click event. Begin selection process.
// Arguments:
@@ -938,6 +899,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_PointerPressedHandler(Windows::Foundation::IInspectable const& sender,
Input::PointerRoutedEventArgs const& args)
{
if (_closing)
{
return;
}
_CapturePointer(sender, args);
const auto ptr = args.Pointer();
@@ -1045,6 +1011,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_PointerMovedHandler(Windows::Foundation::IInspectable const& /*sender*/,
Input::PointerRoutedEventArgs const& args)
{
if (_closing)
{
return;
}
const auto ptr = args.Pointer();
const auto point = args.GetCurrentPoint(*this);
@@ -1148,6 +1119,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_PointerReleasedHandler(Windows::Foundation::IInspectable const& sender,
Input::PointerRoutedEventArgs const& args)
{
if (_closing)
{
return;
}
const auto ptr = args.Pointer();
const auto point = args.GetCurrentPoint(*this);
@@ -1189,45 +1165,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
args.Handled(true);
}
// Method Description:
// - Event handler for the PointerEntered event. We use this for cursor manipulation.
// Arguments:
// - sender: the XAML element responding to the pointer input
// - args: event data
void TermControl::_PointerEnteredHandler(Windows::Foundation::IInspectable const& /*sender*/,
Input::PointerRoutedEventArgs const& /*args*/)
{
if (_closing)
{
return;
}
auto coreWindow = Window::Current().CoreWindow();
_oldCursor = coreWindow.PointerCursor();
if (_terminal->IsTrackingMouseInput())
{
return;
}
coreWindow.PointerCursor(_textCursor);
}
// Method Description:
// - Event handler for the PointerExited event. We use this for cursor manipulation.
// Arguments:
// - sender: the XAML element responding to the pointer input
// - args: event data
void TermControl::_PointerExitedHandler(Windows::Foundation::IInspectable const& /*sender*/,
Input::PointerRoutedEventArgs const& /*args*/)
{
if (auto oldCursor{ std::exchange(_oldCursor, std::nullopt) })
{
auto coreWindow = Window::Current().CoreWindow();
coreWindow.PointerCursor(*oldCursor);
}
}
// Method Description:
// - Event handler for the PointerWheelChanged event. This is raised in
// response to mouse wheel changes. Depending upon what modifier keys are
@@ -1237,6 +1174,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_MouseWheelHandler(Windows::Foundation::IInspectable const& /*sender*/,
Input::PointerRoutedEventArgs const& args)
{
if (_closing)
{
return;
}
const auto point = args.GetCurrentPoint(*this);
if (_CanSendVTMouseInput())
@@ -1367,7 +1309,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_ScrollbarChangeHandler(Windows::Foundation::IInspectable const& /*sender*/,
Controls::Primitives::RangeBaseValueChangedEventArgs const& args)
{
if (_isTerminalInitiatedScroll)
if (_isTerminalInitiatedScroll || _closing)
{
return;
}
@@ -1552,6 +1494,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
{
return;
}
_focused = false;
if (_uiaEngine.get())
@@ -1623,8 +1566,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// concerned with initialization process. Value forwarded to event handler.
void TermControl::_UpdateFont(const bool initialUpdate)
{
auto lock = _terminal->LockForWriting();
const int newDpi = static_cast<int>(static_cast<double>(USER_DEFAULT_SCREEN_DPI) * SwapChainPanel().CompositionScaleX());
// TODO: MSFT:20895307 If the font doesn't exist, this doesn't
@@ -1651,11 +1592,12 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// Refresh our font with the renderer
_UpdateFont();
auto lock = _terminal->LockForWriting();
// Resize the terminal's BUFFER to match the new font size. This does
// NOT change the size of the window, because that can lead to more
// problems (like what happens when you change the font size while the
// window is maximized?)
auto lock = _terminal->LockForWriting();
_DoResize(SwapChainPanel().ActualWidth(), SwapChainPanel().ActualHeight());
}
CATCH_LOG();
@@ -1669,7 +1611,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_SwapChainSizeChanged(winrt::Windows::Foundation::IInspectable const& /*sender*/,
SizeChangedEventArgs const& e)
{
if (!_initializedTerminal)
if (!_initializedTerminal || _closing)
{
return;
}
@@ -1700,8 +1642,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// Arguments:
// - sender: not used
// - e: not used
void TermControl::_BlinkCursor(Windows::Foundation::IInspectable const& /* sender */,
Windows::Foundation::IInspectable const& /* e */)
void TermControl::_CursorTimerTick(Windows::Foundation::IInspectable const& /* sender */,
Windows::Foundation::IInspectable const& /* e */)
{
if ((_closing) || (!_terminal->IsCursorBlinkingAllowed() && _terminal->IsCursorVisible()))
{
@@ -1853,10 +1795,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
hstring TermControl::Title()
{
if (!_initializedTerminal)
return L"";
hstring hstr(_terminal->GetConsoleTitle());
hstring hstr{ _terminal->GetConsoleTitle() };
return hstr;
}
@@ -1873,8 +1812,13 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - collapseText: collapse all of the text to one line
bool TermControl::CopySelectionToClipboard(bool collapseText)
{
if (_closing)
{
return false;
}
// no selection --> nothing to copy
if (_terminal == nullptr || !_terminal->IsSelectionActive())
if (!_terminal->IsSelectionActive())
{
return false;
}
@@ -1941,6 +1885,7 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
_connectionStateChangedRevoker.revoke();
TSFInputControl().Close(); // Disconnect the TSF input control so it doesn't receive EditContext events.
_autoScrollTimer.Stop();
if (auto localConnection{ std::exchange(_connection, nullptr) })
{
@@ -1958,11 +1903,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// renderEngine is destroyed
}
if (auto localTerminal{ std::exchange(_terminal, nullptr) })
{
_initializedTerminal = false;
// terminal is destroyed.
}
// we don't destroy _terminal here; it now has the same lifetime as the
// control.
}
}
@@ -2276,6 +2218,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - <none>
void TermControl::_CompositionCompleted(winrt::hstring text)
{
if (_closing)
{
return;
}
_connection.WriteInput(text);
}
@@ -2288,11 +2235,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - <none>
void TermControl::_CurrentCursorPositionHandler(const IInspectable& /*sender*/, const CursorPositionEventArgs& eventArgs)
{
// If we haven't initialized yet, just quick return.
if (!_terminal)
auto lock = _terminal->LockForReading();
if (!_initializedTerminal)
{
// fake it
eventArgs.CurrentPosition({ 0, 0 });
return;
}
const COORD cursorPos = _terminal->GetCursorPosition();
Windows::Foundation::Point p = { gsl::narrow_cast<float>(cursorPos.X), gsl::narrow_cast<float>(cursorPos.Y) };
eventArgs.CurrentPosition(p);
@@ -2362,8 +2312,14 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
// - e: The DragEventArgs from the Drop event
// Return Value:
// - <none>
winrt::fire_and_forget TermControl::_DoDragDrop(DragEventArgs const e)
winrt::fire_and_forget TermControl::_DragDropHandler(Windows::Foundation::IInspectable const& /*sender*/,
DragEventArgs const e)
{
if (_closing)
{
return;
}
if (e.DataView().Contains(StandardDataFormats::StorageItems()))
{
auto items = co_await e.DataView().GetStorageItemsAsync();
@@ -2398,22 +2354,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
}
}
// Method Description:
// - Synchronous handler for the "Drop" event. We'll dispatch the async
// _DoDragDrop method to handle this, because getting information about
// the file that was potentially dropped onto us must be done off the UI
// thread.
// Arguments:
// - e: The DragEventArgs from the Drop event
// Return Value:
// - <none>
void TermControl::_DragDropHandler(Windows::Foundation::IInspectable const& /*sender*/,
DragEventArgs const& e)
{
// Dispatch an async method to handle the drop event.
_DoDragDrop(e);
}
// Method Description:
// - Handle the DragOver event. We'll signal that the drag operation we
// support is the "copy" operation, and we'll also customize the
@@ -2427,6 +2367,11 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void TermControl::_DragOverHandler(Windows::Foundation::IInspectable const& /*sender*/,
DragEventArgs const& e)
{
if (_closing)
{
return;
}
if (!e.DataView().Contains(StandardDataFormats::StorageItems()))
{
// We can't do anything for non-storageitems right now.

View File

@@ -77,7 +77,8 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void AdjustFontSize(int fontSizeDelta);
void ResetFontSize();
winrt::fire_and_forget SwapChainChanged();
winrt::fire_and_forget RenderEngineSwapChainChanged();
void _AttachDxgiSwapChainToXaml(IDXGISwapChain1* swapChain);
void CreateSearchBoxControl();
@@ -167,10 +168,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
winrt::Windows::UI::Xaml::Controls::SwapChainPanel::LayoutUpdated_revoker _layoutUpdatedRevoker;
std::optional<winrt::Windows::UI::Core::CoreCursor> _oldCursor; // when we toggle the cursor, we have to save it here to restore it
winrt::Windows::UI::Core::CoreCursor _textCursor;
winrt::Windows::UI::Core::CoreCursor _pointerCursor;
void _ApplyUISettings();
void _InitializeBackgroundBrush();
winrt::fire_and_forget _BackgroundColorChanged(const uint32_t color);
@@ -179,26 +176,21 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
void _SetFontSize(int fontSize);
void _TappedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::TappedRoutedEventArgs const& e);
void _KeyDownHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
void _KeyUpHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::KeyRoutedEventArgs const& e);
void _CharacterHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::CharacterReceivedRoutedEventArgs const& e);
void _PointerPressedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _PointerMovedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _PointerReleasedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _PointerEnteredHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _PointerExitedHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _MouseWheelHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Input::PointerRoutedEventArgs const& e);
void _ScrollbarChangeHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::Controls::Primitives::RangeBaseValueChangedEventArgs const& e);
void _GotFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e);
void _LostFocusHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::RoutedEventArgs const& e);
void _DragDropHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::DragEventArgs const& e);
winrt::fire_and_forget _DragDropHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::DragEventArgs const e);
void _DragOverHandler(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::DragEventArgs const& e);
winrt::fire_and_forget _DoDragDrop(Windows::UI::Xaml::DragEventArgs const e);
void _BlinkCursor(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
void _CursorTimerTick(Windows::Foundation::IInspectable const& sender, Windows::Foundation::IInspectable const& e);
void _SetEndSelectionPointAtCursor(Windows::Foundation::Point const& cursorPosition);
void _SendInputToConnection(const std::wstring& wstr);
void _SendPastedTextToConnection(const std::wstring& wstr);
winrt::fire_and_forget _SwapChainRoutine();
void _SwapChainSizeChanged(Windows::Foundation::IInspectable const& sender, Windows::UI::Xaml::SizeChangedEventArgs const& e);
void _SwapChainScaleChanged(Windows::UI::Xaml::Controls::SwapChainPanel const& sender, Windows::Foundation::IInspectable const& args);
void _DoResize(const double newWidth, const double newHeight);
@@ -223,7 +215,6 @@ namespace winrt::Microsoft::Terminal::TerminalControl::implementation
bool _TrySendKeyEvent(const WORD vkey, const WORD scanCode, ::Microsoft::Terminal::Core::ControlKeyStates modifiers);
bool _TrySendMouseEvent(Windows::UI::Input::PointerPoint const& point);
bool _CanSendVTMouseInput();
winrt::fire_and_forget _TerminalMouseModeChanged();
const COORD _GetTerminalPosition(winrt::Windows::Foundation::Point cursorPosition);
const unsigned int _NumberOfClicks(winrt::Windows::Foundation::Point clickPos, Timestamp clickTime);

View File

@@ -19,7 +19,6 @@
Tapped="_TappedHandler"
PointerWheelChanged="_MouseWheelHandler"
PreviewKeyDown="_KeyDownHandler"
KeyUp="_KeyUpHandler"
CharacterReceived="_CharacterHandler"
GotFocus="_GotFocusHandler"
LostFocus="_LostFocusHandler">
@@ -44,9 +43,7 @@
CompositionScaleChanged="_SwapChainScaleChanged"
PointerPressed="_PointerPressedHandler"
PointerMoved="_PointerMovedHandler"
PointerReleased="_PointerReleasedHandler"
PointerEntered="_PointerEnteredHandler"
PointerExited="_PointerExitedHandler" />
PointerReleased="_PointerReleasedHandler" />
<!-- Putting this in a grid w/ the SwapChainPanel
ensures that it's always aligned w/ the scrollbar -->

View File

@@ -701,6 +701,7 @@ void Terminal::_AdjustCursorPosition(const COORD proposedPosition)
if (notifyScroll)
{
// TODO: don't do this, thanks migrie
_buffer->GetRenderTarget().TriggerRedrawAll();
_NotifyScrollEvent();
}
@@ -791,12 +792,3 @@ bool Terminal::IsCursorBlinkingAllowed() const noexcept
const auto& cursor = _buffer->GetCursor();
return cursor.IsBlinkingAllowed();
}
// Routine Description:
// - Sets up the callback for mouse input mode changes
// Parameters:
// - mouseModeChangedCallback: the callback
void Terminal::SetMouseModeChangedCallback(std::function<void()> mouseModeChangedCallback) noexcept
{
_terminalInput->SetMouseModeChangedCallback(std::move(mouseModeChangedCallback));
}

View File

@@ -172,7 +172,6 @@ public:
void SetCursorOn(const bool isOn) noexcept;
bool IsCursorBlinkingAllowed() const noexcept;
void SetMouseModeChangedCallback(std::function<void()> mouseModeChangedCallback) noexcept;
#pragma region TextSelection
// These methods are defined in TerminalSelection.cpp

View File

@@ -53,6 +53,9 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final
static const SHORT TerminalViewWidth = 80;
static const SHORT TerminalViewHeight = 32;
// This test class is for tests that are supposed to emit something in the PTY layer
// and then check that they've been staged for presentation correctly inside
// the Terminal application. Which sequences were used to get here don't matter.
TEST_CLASS(ConptyRoundtripTests);
TEST_CLASS_SETUP(ClassSetup)
@@ -171,6 +174,8 @@ class TerminalCoreUnitTests::ConptyRoundtripTests final
TEST_METHOD(TestResizeHeight);
TEST_METHOD(ScrollWithMargins);
private:
bool _writeCallback(const char* const pch, size_t const cch);
void _flushFirstFrame();
@@ -1055,3 +1060,245 @@ void ConptyRoundtripTests::PassthroughHardReset()
TestUtils::VerifyExpectedString(termTb, std::wstring(TerminalViewWidth, L' '), { 0, y });
}
}
void ConptyRoundtripTests::ScrollWithMargins()
{
auto& g = ServiceLocator::LocateGlobals();
auto& renderer = *g.pRender;
auto& gci = g.getConsoleInformation();
auto& si = gci.GetActiveOutputBuffer();
auto& hostSm = si.GetStateMachine();
auto& hostTb = si.GetTextBuffer();
auto& termTb = *term->_buffer;
const auto initialTermView = term->GetViewport();
Log::Comment(L"Flush first frame.");
_flushFirstFrame();
// Fill up the buffer with some text.
// We're going to write something like this:
// AAAA
// BBBB
// CCCC
// ........
// QQQQ
// ****************
// The letters represent the data in the TMUX pane.
// The final *** line represents the mode line which we will
// attempt to hold in place and not scroll.
Log::Comment(L"Fill host with text pattern by feeding it into VT parser.");
const auto rowsToWrite = initialTermView.Height() - 1;
// For all lines but the last one, write out a few of a letter.
for (auto i = 0; i < rowsToWrite; ++i)
{
const wchar_t wch = static_cast<wchar_t>(L'A' + i);
hostSm.ProcessCharacter(wch);
hostSm.ProcessCharacter(wch);
hostSm.ProcessCharacter(wch);
hostSm.ProcessCharacter(wch);
hostSm.ProcessCharacter('\n');
}
// For the last one, write out the asterisks for the mode line.
for (auto i = 0; i < initialTermView.Width(); ++i)
{
hostSm.ProcessCharacter('*');
}
// no newline in the bottom right corner or it will move unexpectedly.
// Now set up the verification that the buffers are full of the pattern we expect.
// This function will verify the text backing buffers.
auto verifyBuffer = [&](const TextBuffer& tb) {
auto& cursor = tb.GetCursor();
// Verify the cursor is waiting in the bottom right corner
VERIFY_ARE_EQUAL(initialTermView.Height() - 1, cursor.GetPosition().Y);
VERIFY_ARE_EQUAL(initialTermView.Width() - 1, cursor.GetPosition().X);
// For all rows except the last one, verify that we have a run of four letters.
for (auto i = 0; i < rowsToWrite; ++i)
{
const std::wstring expectedString(4, static_cast<wchar_t>(L'A' + i));
const COORD expectedPos{ 0, gsl::narrow<SHORT>(i) };
TestUtils::VerifyExpectedString(tb, expectedString, expectedPos);
}
// For the last row, verify we have an entire row of asterisks for the mode line.
const std::wstring expectedModeLine(initialTermView.Width(), L'*');
const COORD expectedPos{ 0, gsl::narrow<SHORT>(rowsToWrite) };
TestUtils::VerifyExpectedString(tb, expectedModeLine, expectedPos);
};
// This will verify the text emitted from the PTY.
for (auto i = 0; i < rowsToWrite; ++i)
{
const std::string expectedString(4, static_cast<char>('A' + i));
expectedOutput.push_back(expectedString);
expectedOutput.push_back("\r\n");
}
{
const std::string expectedString(initialTermView.Width(), '*');
expectedOutput.push_back(expectedString);
// Cursor gets reset into bottom right corner as we're writing all the way into that corner.
std::stringstream ss;
ss << "\x1b[" << initialTermView.Height() << ";" << initialTermView.Width() << "H";
expectedOutput.push_back(ss.str());
}
Log::Comment(L"Verify host buffer contains pattern.");
// Verify the host side.
verifyBuffer(hostTb);
Log::Comment(L"Emit PTY frame and validate it transmits the right data.");
// Paint the frame
VERIFY_SUCCEEDED(renderer.PaintFrame());
Log::Comment(L"Verify terminal buffer contains pattern.");
// Verify the terminal side.
verifyBuffer(termTb);
Log::Comment(L"!!! OK. Set up the scroll region and let's get scrolling!");
// This is a simulation of what TMUX does to scroll everything except the mode line.
// First build up our VT strings...
std::wstring reducedScrollRegion;
{
std::wstringstream wss;
// For 20 tall buffer...
// ESC[1;19r
// Set scroll region to lines 1-19.
wss << L"\x1b[1;" << initialTermView.Height() - 1 << L"r";
reducedScrollRegion = wss.str();
}
std::wstring completeScrollRegion;
{
std::wstringstream wss;
// For 20 tall buffer...
// ESC[1;20r
// Set scroll region to lines 1-20. (or the whole buffer)
wss << L"\x1b[1;" << initialTermView.Height() << L"r";
completeScrollRegion = wss.str();
}
std::wstring reducedCursorBottomRight;
{
std::wstringstream wss;
// For 20 tall and 100 wide buffer
// ESC[19;100H
// Put cursor on line 19 (1 before last) and the right most column 100.
// (Remember that in VT, we start counting from 1 not 0.)
wss << L"\x1b[" << initialTermView.Height() - 1 << L";" << initialTermView.Width() << "H";
reducedCursorBottomRight = wss.str();
}
std::wstring completeCursorAtPromptLine;
{
std::wstringstream wss;
// For 20 tall and 100 wide buffer
// ESC[19;1H
// Put cursor on line 19 (1 before last) and the left most column 1.
// (Remember that in VT, we start counting from 1 not 0.)
wss << L"\x1b[" << initialTermView.Height() - 1 << L";1H";
completeCursorAtPromptLine = wss.str();
}
Log::Comment(L"Perform all the operations on the buffer.");
// OK this is what TMUX does.
// 1. Mark off the scroll area as everything but the mode line.
hostSm.ProcessString(reducedScrollRegion);
// 2. Put the cursor in the bottom-right corner of the scroll region.
hostSm.ProcessString(reducedCursorBottomRight);
// 3. Send a single newline which should do the heavy lifting
// of pushing everything in the scroll region up by 1 line and
// leave everything outside the region alone.
// This entire block is subject to change in the future with optimizations.
{
// Cursor gets redrawn in the bottom right of the scroll region with the repaint that is forced
// early while the screen is rotated.
std::stringstream ss;
ss << "\x1b[" << initialTermView.Height() - 1 << ";" << initialTermView.Width() << "H";
expectedOutput.push_back(ss.str());
expectedOutput.push_back("\x1b[?25h"); // turn the cursor back on too.
}
hostSm.ProcessString(L"\n");
// 4. Remove the scroll area by setting it to the entire size of the viewport.
hostSm.ProcessString(completeScrollRegion);
// 5. Put the cursor back at the beginning of the new line that was just revealed.
hostSm.ProcessString(completeCursorAtPromptLine);
// Set up the verifications like above.
auto verifyBufferAfter = [&](const TextBuffer& tb) {
auto& cursor = tb.GetCursor();
// Verify the cursor is waiting on the freshly revealed line (1 above mode line)
// and in the left most column.
VERIFY_ARE_EQUAL(initialTermView.Height() - 2, cursor.GetPosition().Y);
VERIFY_ARE_EQUAL(0, cursor.GetPosition().X);
// For all rows except the last two, verify that we have a run of four letters.
for (auto i = 0; i < rowsToWrite - 1; ++i)
{
// Start with B this time because the A line got scrolled off the top.
const std::wstring expectedString(4, static_cast<wchar_t>(L'B' + i));
const COORD expectedPos{ 0, gsl::narrow<SHORT>(i) };
TestUtils::VerifyExpectedString(tb, expectedString, expectedPos);
}
// For the second to last row, verify that it is blank.
{
const std::wstring expectedBlankLine(initialTermView.Width(), L' ');
const COORD blankLinePos{ 0, gsl::narrow<SHORT>(rowsToWrite - 1) };
TestUtils::VerifyExpectedString(tb, expectedBlankLine, blankLinePos);
}
// For the last row, verify we have an entire row of asterisks for the mode line.
{
const std::wstring expectedModeLine(initialTermView.Width(), L'*');
const COORD modeLinePos{ 0, gsl::narrow<SHORT>(rowsToWrite) };
TestUtils::VerifyExpectedString(tb, expectedModeLine, modeLinePos);
}
};
// This will verify the text emitted from the PTY.
expectedOutput.push_back("\x1b[H"); // cursor returns to top left corner.
for (auto i = 0; i < rowsToWrite - 1; ++i)
{
const std::string expectedString(4, static_cast<char>('B' + i));
expectedOutput.push_back(expectedString);
expectedOutput.push_back("\x1b[K"); // erase the rest of the line.
expectedOutput.push_back("\r\n");
}
{
expectedOutput.push_back(""); // nothing for the empty line
expectedOutput.push_back("\x1b[K"); // erase the rest of the line.
expectedOutput.push_back("\r\n");
}
{
const std::string expectedString(initialTermView.Width(), '*');
expectedOutput.push_back(expectedString);
}
{
// Cursor gets reset into second line from bottom, left most column
std::stringstream ss;
ss << "\x1b[" << initialTermView.Height() - 1 << ";1H";
expectedOutput.push_back(ss.str());
}
expectedOutput.push_back("\x1b[?25h"); // turn the cursor back on too.
Log::Comment(L"Verify host buffer contains pattern moved up one and mode line still in place.");
// Verify the host side.
verifyBufferAfter(hostTb);
Log::Comment(L"Emit PTY frame and validate it transmits the right data.");
// Paint the frame
VERIFY_SUCCEEDED(renderer.PaintFrame());
Log::Comment(L"Verify terminal buffer contains pattern moved up one and mode line still in place.");
// Verify the terminal side.
verifyBufferAfter(termTb);
}

View File

@@ -38,7 +38,7 @@ namespace Microsoft.Terminal.Wpf
WM_MOUSEACTIVATE = 0x0021,
WM_GETOBJECT = 0x003D,
/// <summary>
/// The WM_WINDOWPOSCHANGED message is sent to a window whose size, position, or place in the Z order has changed as a result of a call to the SetWindowPos function or another window-management function.
/// </summary>

View File

@@ -26,7 +26,7 @@ namespace Microsoft.Terminal.Wpf
private DispatcherTimer blinkTimer;
private NativeMethods.ScrollCallback scrollCallback;
private NativeMethods.WriteCallback writeCallback;
/// <summary>
/// Initializes a new instance of the <see cref="TerminalContainer"/> class.
/// </summary>
@@ -35,7 +35,7 @@ namespace Microsoft.Terminal.Wpf
this.MessageHook += this.TerminalContainer_MessageHook;
this.GotFocus += this.TerminalContainer_GotFocus;
this.Focusable = true;
var blinkTime = NativeMethods.GetCaretBlinkTime();
if (blinkTime != uint.MaxValue)

View File

@@ -9,7 +9,7 @@ namespace Microsoft.Terminal.Wpf
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
/// <summary>
/// A basic terminal control. This control can receive and render standard VT100 sequences.
/// </summary>

View File

@@ -171,7 +171,33 @@ constexpr unsigned int LOCAL_BUFFER_SIZE = 100;
// displays the correct text.
if (newViewOrigin == viewport.Origin())
{
Viewport invalid = Viewport::FromDimensions(viewport.Origin(), { viewport.Width(), delta });
// Inside this block, we're shifting down at the bottom.
// This means that we had something like this:
// AAAA
// BBBB
// CCCC
// DDDD
// EEEE
//
// Our margins were set for lines A-D, but not on line E.
// So we circled the whole buffer up by one:
// BBBB
// CCCC
// DDDD
// EEEE
// <blank, was AAAA>
//
// Then we scrolled the contents of everything OUTSIDE the margin frame down.
// BBBB
// CCCC
// DDDD
// <blank, filled during scroll down of EEEE>
// EEEE
//
// And now we need to report that only the bottom line didn't "move" as we put the EEEE
// back where it started, but everything else moved.
// In this case, delta was 1. So the amount that moved is the entire viewport height minus the delta.
Viewport invalid = Viewport::FromDimensions(viewport.Origin(), { viewport.Width(), viewport.Height() - delta });
screenInfo.GetRenderTarget().TriggerRedraw(invalid);
}

View File

@@ -481,7 +481,7 @@ namespace Conhost.UIA.Tests
TextPatternRange testRange = visibleRanges.First().Clone();
// assumes that range is a line range at the top of the screen buffer
Action<TextPatternRange> testTopBoundary = delegate(TextPatternRange range)
Action<TextPatternRange> testTopBoundary = delegate (TextPatternRange range)
{
// the first visible range is at the top of the screen
// buffer, we shouldn't be able to move the starting endpoint up

View File

@@ -31,7 +31,7 @@ namespace Conhost.UIA.Tests.Common
~ShortcutHelper()
{
this.Dispose(false);
this.Dispose(false);
}
public void Dispose()

View File

@@ -52,7 +52,7 @@ namespace Conhost.UIA.Tests.Elements
tab.Click();
Globals.WaitForTimeout();
this.PopulateItemsOnNavigate(this.propDialog.PropWindow);
}

View File

@@ -33,7 +33,7 @@ namespace Conhost.UIA.Tests.Elements
this.tabs.Add(new FontTab(this.propDialog));
this.tabs.Add(new LayoutTab(this.propDialog));
this.tabs.Add(new ColorsTab(this.propDialog));
}
private Tabs()

View File

@@ -105,7 +105,7 @@ namespace Conhost.UIA.Tests.Elements
app.UIRoot.SendKeys(Keys.Escape);
this.state = ViewportStates.Normal;
}
public void EnterMode(ViewportStates state)
{
if (state == ViewportStates.Normal)

View File

@@ -111,7 +111,7 @@ namespace Conhost.UIA.Tests
default:
throw new NotSupportedException();
}
afterScroll = app.GetScreenBufferInfo();
switch (dir)

View File

@@ -57,7 +57,7 @@ namespace Conhost.UIA.Tests
reg.BackupRegistry(); // we're going to modify the virtual terminal state for this, so back it up first.
VersionSelector.SetConsoleVersion(reg, ConsoleVersion.V2);
reg.SetDefaultValue(VIRTUAL_TERMINAL_KEY_NAME, VIRTUAL_TERMINAL_ON_VALUE);
bool haveVtAppPath = !string.IsNullOrEmpty(vtAppLocation);
Verify.IsTrue(haveVtAppPath, "Ensure that we passed in the location to VtApp.exe");
@@ -114,12 +114,12 @@ namespace Conhost.UIA.Tests
Log.Comment("Move cursor to the middle-ish");
Point cursorExpected = new Point();
// H is at 5, 1. VT coords are 1-based and buffer is 0-based so adjust.
cursorExpected.Y = 5 - 1;
cursorExpected.Y = 5 - 1;
cursorExpected.X = 1 - 1;
app.UIRoot.SendKeys("H");
// Move to middle-ish from here. 10 Bs and 10 Cs should about do it.
for (int i=0; i < 10; i++)
for (int i = 0; i < 10; i++)
{
app.UIRoot.SendKeys("BC");
cursorExpected.Y++;
@@ -715,7 +715,7 @@ namespace Conhost.UIA.Tests
}
delegate char GetExpectedChar(int rowId, int colId, int height, int width);
private static void ScreenFillHelper(CmdApp app, ViewportArea area, IntPtr hConsole)
{
Log.Comment("Fill screen with junk");

View File

@@ -212,7 +212,7 @@ namespace Conhost.UIA.Tests
[TestMethod]
public void TestSelection()
{
RunTest(TestSelectionImpl);
RunTest(TestSelectionImpl);
}
private void TestSelectionImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue<EventData> expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal)
@@ -295,7 +295,7 @@ namespace Conhost.UIA.Tests
[TestMethod]
public void TestLaunchAndExitChild()
{
RunTest(TestLaunchAndExitChildImpl);
RunTest(TestLaunchAndExitChildImpl);
}
private void TestLaunchAndExitChildImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue<EventData> expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal)
@@ -357,11 +357,11 @@ namespace Conhost.UIA.Tests
{
RunTest(TestScrollByWheelImpl);
}
private void TestScrollByWheelImpl(CmdApp app, ViewportArea area, IntPtr hConsole, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiex, Queue<EventData> expected, WinCon.CONSOLE_SCREEN_BUFFER_INFO_EX sbiexOriginal)
{
int rowsPerScroll = app.GetRowsPerScroll();
int scrollDelta;
int scrollDelta;
// A. Scroll down.
{

View File

@@ -27,6 +27,9 @@ using namespace Microsoft::Console::Types;
class ConptyOutputTests
{
// This test class is to write some things into the PTY and then check that
// the rendering that is coming out of the VT-sequence generator is exactly
// as we expect it to be.
BEGIN_TEST_CLASS(ConptyOutputTests)
TEST_CLASS_PROPERTY(L"IsolationLevel", L"Class")
END_TEST_CLASS()

View File

@@ -7,11 +7,12 @@
#include "til/at.h"
#include "til/color.h"
#include "til/math.h"
#include "til/some.h"
#include "til/size.h"
#include "til/point.h"
#include "til/rectangle.h"
#include "til/operators.h"
#include "til/rectangle.h"
#include "til/bitmap.h"
#include "til/u8u16convert.h"

View File

@@ -285,7 +285,8 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
for (const auto pt : rc)
{
til::at(_bits, _rc.index_of(pt)) = true;
auto idx = _rc.index_of(pt);
til::at(_bits, idx) = true;
}
_dirty |= rc;

85
src/inc/til/math.h Normal file
View File

@@ -0,0 +1,85 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#pragma once
namespace til
{
// The til::math namespace contains TIL math guidance casts;
// they are intended to be used as the first argument to
// floating-point universal converters elsewhere in the til namespace.
namespace math
{
namespace details
{
struct ceiling_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
return ::base::saturated_cast<O>(::std::ceil(val));
}
else
{
return ::base::saturated_cast<O>(val);
}
}
};
struct flooring_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
return ::base::saturated_cast<O>(::std::floor(val));
}
else
{
return ::base::saturated_cast<O>(val);
}
}
};
struct rounding_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
return ::base::saturated_cast<O>(::std::round(val));
}
else
{
return ::base::saturated_cast<O>(val);
}
}
};
struct truncating_t
{
template<typename O, typename T>
static O cast(T val)
{
if constexpr (std::is_floating_point_v<T>)
{
THROW_HR_IF(E_ABORT, ::std::isnan(val));
}
return ::base::saturated_cast<O>(val);
}
};
}
static constexpr details::ceiling_t ceiling; // positives become more positive, negatives become less negative
static constexpr details::flooring_t flooring; // positives become less positive, negatives become more negative
static constexpr details::rounding_t rounding; // it's rounding, from math class
static constexpr details::truncating_t truncating; // drop the decimal point, regardless of how close it is to the next value
}
}

View File

@@ -3,10 +3,6 @@
#pragma once
#include "rectangle.h"
#include "size.h"
#include "bitmap.h"
namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
// Operators go here when they involve two headers that can't/don't include each other.

View File

@@ -53,6 +53,22 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
{
}
// This template will convert to size from anything that has a x and a y field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr point(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().x)> && std::is_floating_point_v<decltype(std::declval<TOther>().y)>, int> /*sentinel*/ = 0) :
point(TilMath::template cast<ptrdiff_t>(other.x), TilMath::template cast<ptrdiff_t>(other.y))
{
}
constexpr bool operator==(const point& other) const noexcept
{
return _x == other._x &&
@@ -107,6 +123,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return point{ x, y };
}
point& operator+=(const point& other)
{
*this = *this + other;
return *this;
}
point operator-(const point& other) const
{
ptrdiff_t x;
@@ -118,6 +140,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return point{ x, y };
}
point& operator-=(const point& other)
{
*this = *this - other;
return *this;
}
point operator*(const point& other) const
{
ptrdiff_t x;
@@ -129,6 +157,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return point{ x, y };
}
point& operator*=(const point& other)
{
*this = *this * other;
return *this;
}
point operator/(const point& other) const
{
ptrdiff_t x;
@@ -140,6 +174,12 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return point{ x, y };
}
point& operator/=(const point& other)
{
*this = *this / other;
return *this;
}
constexpr ptrdiff_t x() const noexcept
{
return _x;

View File

@@ -3,10 +3,6 @@
#pragma once
#include "point.h"
#include "size.h"
#include "some.h"
#ifdef UNIT_TESTING
class RectangleTests;
#endif
@@ -636,6 +632,32 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
return *this;
}
// MUL will scale the entire rectangle up by the size factor
rectangle operator*(const size& size)
{
auto topLeft = _topLeft;
auto bottomRight = _bottomRight;
topLeft = topLeft * size;
bottomRight = bottomRight * size;
return til::rectangle{ topLeft, bottomRight };
}
// DIV will scale the entire rectangle down by the size factor,
// but rounds the bottom-right corner out.
rectangle operator/(const size& size)
{
auto topLeft = _topLeft;
auto bottomRight = _bottomRight;
topLeft = topLeft / size;
// Move bottom right point into a size
// Use size specialization of divide_ceil to round up against the size given.
// Add leading addition to point to convert it back into a point.
bottomRight = til::point{} + til::size{ right(), bottom() }.divide_ceil(size);
return til::rectangle{ topLeft, bottomRight };
}
#pragma endregion
constexpr ptrdiff_t top() const noexcept

View File

@@ -53,6 +53,30 @@ namespace til // Terminal Implementation Library. Also: "Today I Learned"
{
}
// This template will convert to size from anything that has a X and a Y field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().X)> && std::is_floating_point_v<decltype(std::declval<TOther>().Y)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.X), TilMath::template cast<ptrdiff_t>(other.Y))
{
}
// This template will convert to size from anything that has a cx and a cy field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().cx)> && std::is_floating_point_v<decltype(std::declval<TOther>().cy)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.cx), TilMath::template cast<ptrdiff_t>(other.cy))
{
}
// This template will convert to size from anything that has a Width and a Height field that are floating-point;
// a math type is required.
template<typename TilMath, typename TOther>
constexpr size(TilMath, const TOther& other, std::enable_if_t<std::is_floating_point_v<decltype(std::declval<TOther>().Width)> && std::is_floating_point_v<decltype(std::declval<TOther>().Height)>, int> /*sentinel*/ = 0) :
size(TilMath::template cast<ptrdiff_t>(other.Width), TilMath::template cast<ptrdiff_t>(other.Height))
{
}
constexpr bool operator==(const size& other) const noexcept
{
return _width == other._width &&

View File

@@ -273,7 +273,12 @@ using namespace Microsoft::Console::Render;
rect.right = std::accumulate(advancesSpan.cbegin(), advancesSpan.cend(), rect.right);
// Clip all drawing in this glyph run to where we expect.
d2dContext->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_PER_PRIMITIVE);
// We need the AntialiasMode here to be Aliased to ensure
// that background boxes line up with each other and don't leave behind
// stray colors.
// See GH#3626 for more details.
d2dContext->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_ALIASED);
// Ensure we pop it on the way out
auto popclip = wil::scope_exit([&d2dContext]() noexcept {
d2dContext->PopAxisAlignedClip();

View File

@@ -65,9 +65,8 @@ using namespace Microsoft::Console::Types;
// TODO GH 2683: The default constructor should not throw.
DxEngine::DxEngine() :
RenderEngineBase(),
_isInvalidUsed{ false },
_invalidRect{ 0 },
_invalidScroll{ 0 },
_invalidMap{},
_invalidScroll{},
_presentParams{ 0 },
_presentReady{ false },
_presentScroll{ 0 },
@@ -75,16 +74,16 @@ DxEngine::DxEngine() :
_presentOffset{ 0 },
_isEnabled{ false },
_isPainting{ false },
_displaySizePixels{ 0 },
_displaySizePixels{},
_foregroundColor{ 0 },
_backgroundColor{ 0 },
_selectionBackground{},
_glyphCell{ 0 },
_glyphCell{},
_haveDeviceResources{ false },
_retroTerminalEffects{ false },
_antialiasingMode{ D2D1_TEXT_ANTIALIAS_MODE_GRAYSCALE },
_hwndTarget{ static_cast<HWND>(INVALID_HANDLE_VALUE) },
_sizeTarget{ 0 },
_sizeTarget{},
_dpi{ USER_DEFAULT_SCREEN_DPI },
_scale{ 1.0f },
_chainMode{ SwapChainMode::ForComposition },
@@ -238,8 +237,8 @@ HRESULT DxEngine::_SetupTerminalEffects()
// Setup the viewport.
D3D11_VIEWPORT vp;
vp.Width = static_cast<FLOAT>(_displaySizePixels.cx);
vp.Height = static_cast<FLOAT>(_displaySizePixels.cy);
vp.Width = _displaySizePixels.width<float>();
vp.Height = _displaySizePixels.height<float>();
vp.MinDepth = 0.0f;
vp.MaxDepth = 1.0f;
vp.TopLeftX = 0;
@@ -370,7 +369,7 @@ void DxEngine::_ComputePixelShaderSettings() noexcept
// You can find out how to install it here:
// https://docs.microsoft.com/en-us/windows/uwp/gaming/use-the-directx-runtime-and-visual-studio-graphics-diagnostic-features
// clang-format on
// D3D11_CREATE_DEVICE_DEBUG |
D3D11_CREATE_DEVICE_DEBUG |
D3D11_CREATE_DEVICE_SINGLETHREADED;
const std::array<D3D_FEATURE_LEVEL, 5> FeatureLevels{ D3D_FEATURE_LEVEL_11_1,
@@ -424,8 +423,7 @@ void DxEngine::_ComputePixelShaderSettings() noexcept
{
switch (_chainMode)
{
case SwapChainMode::ForHwnd:
{
case SwapChainMode::ForHwnd: {
// use the HWND's dimensions for the swap chain dimensions.
RECT rect = { 0 };
RETURN_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &rect));
@@ -454,11 +452,10 @@ void DxEngine::_ComputePixelShaderSettings() noexcept
break;
}
case SwapChainMode::ForComposition:
{
case SwapChainMode::ForComposition: {
// Use the given target size for compositions.
SwapChainDesc.Width = _displaySizePixels.cx;
SwapChainDesc.Height = _displaySizePixels.cy;
SwapChainDesc.Width = _displaySizePixels.width<UINT>();
SwapChainDesc.Height = _displaySizePixels.height<UINT>();
// We're doing advanced composition pretty much for the purpose of pretty alpha, so turn it on.
SwapChainDesc.AlphaMode = DXGI_ALPHA_MODE_PREMULTIPLIED;
@@ -533,6 +530,11 @@ void DxEngine::_ComputePixelShaderSettings() noexcept
&props,
&_d2dRenderTarget));
// We need the AntialiasMode for non-text object to be Aliased to ensure
// that background boxes line up with each other and don't leave behind
// stray colors.
// See GH#3626 for more details.
_d2dRenderTarget->SetAntialiasMode(D2D1_ANTIALIAS_MODE_ALIASED);
_d2dRenderTarget->SetTextAntialiasMode(_antialiasingMode);
RETURN_IF_FAILED(_d2dRenderTarget->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::DarkRed),
@@ -628,8 +630,8 @@ void DxEngine::_ReleaseDeviceResources() noexcept
return _dwriteFactory->CreateTextLayout(string,
gsl::narrow<UINT32>(stringLength),
_dwriteTextFormat.Get(),
gsl::narrow<float>(_displaySizePixels.cx),
_glyphCell.cy != 0 ? _glyphCell.cy : gsl::narrow<float>(_displaySizePixels.cy),
_displaySizePixels.width<float>(),
_glyphCell.height() != 0 ? _glyphCell.height<float>() : _displaySizePixels.height<float>(),
ppTextLayout);
}
@@ -650,9 +652,7 @@ void DxEngine::_ReleaseDeviceResources() noexcept
[[nodiscard]] HRESULT DxEngine::SetWindowSize(const SIZE Pixels) noexcept
{
_sizeTarget = Pixels;
RETURN_IF_FAILED(InvalidateAll());
_invalidMap.resize(_sizeTarget / _glyphCell, true);
return S_OK;
}
@@ -686,7 +686,8 @@ Microsoft::WRL::ComPtr<IDXGISwapChain1> DxEngine::GetSwapChain()
{
RETURN_HR_IF_NULL(E_INVALIDARG, psrRegion);
_InvalidOr(*psrRegion);
_invalidMap.set(Viewport::FromExclusive(*psrRegion).ToInclusive());
return S_OK;
}
@@ -700,8 +701,9 @@ Microsoft::WRL::ComPtr<IDXGISwapChain1> DxEngine::GetSwapChain()
{
RETURN_HR_IF_NULL(E_INVALIDARG, pcoordCursor);
const SMALL_RECT sr = Microsoft::Console::Types::Viewport::FromCoord(*pcoordCursor).ToInclusive();
return Invalidate(&sr);
_invalidMap.set(*pcoordCursor);
return S_OK;
}
// Routine Description:
@@ -711,13 +713,17 @@ Microsoft::WRL::ComPtr<IDXGISwapChain1> DxEngine::GetSwapChain()
// Return Value:
// - S_OK
[[nodiscard]] HRESULT DxEngine::InvalidateSystem(const RECT* const prcDirtyClient) noexcept
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, prcDirtyClient);
_InvalidOr(*prcDirtyClient);
// Dirty client is in pixels. Use divide specialization against glyph factor to make conversion
// to cells.
_invalidMap.set(til::rectangle{ *prcDirtyClient } / _glyphCell);
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - Invalidates a series of character rectangles
@@ -743,50 +749,24 @@ Microsoft::WRL::ComPtr<IDXGISwapChain1> DxEngine::GetSwapChain()
// Return Value:
// - S_OK
[[nodiscard]] HRESULT DxEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept
try
{
if (pcoordDelta->X != 0 || pcoordDelta->Y != 0)
const til::point deltaCells{ *pcoordDelta };
if (deltaCells != til::point{ 0, 0 })
{
try
{
POINT delta = { 0 };
delta.x = pcoordDelta->X * _glyphCell.cx;
delta.y = pcoordDelta->Y * _glyphCell.cy;
const til::point deltaPixels = deltaCells * _glyphCell;
_InvalidOffset(delta);
// Shift the contents of the map and fill in revealed area.
_invalidMap.translate(deltaCells, true);
_invalidScroll.cx += delta.x;
_invalidScroll.cy += delta.y;
// Add the revealed portion of the screen from the scroll to the invalid area.
const RECT display = _GetDisplayRect();
RECT reveal = display;
// X delta first
OffsetRect(&reveal, delta.x, 0);
IntersectRect(&reveal, &reveal, &display);
SubtractRect(&reveal, &display, &reveal);
if (!IsRectEmpty(&reveal))
{
_InvalidOr(reveal);
}
// Y delta second (subtract rect won't work if you move both)
reveal = display;
OffsetRect(&reveal, 0, delta.y);
IntersectRect(&reveal, &reveal, &display);
SubtractRect(&reveal, &display, &reveal);
if (!IsRectEmpty(&reveal))
{
_InvalidOr(reveal);
}
}
CATCH_RETURN();
// TODO: should we just maintain it all in cells?
_invalidScroll += deltaPixels;
}
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - Invalidates the entire window area
@@ -795,12 +775,12 @@ Microsoft::WRL::ComPtr<IDXGISwapChain1> DxEngine::GetSwapChain()
// Return Value:
// - S_OK
[[nodiscard]] HRESULT DxEngine::InvalidateAll() noexcept
try
{
const RECT screen = _GetDisplayRect();
_InvalidOr(screen);
_invalidMap.set_all();
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - This currently has no effect in this renderer.
@@ -822,23 +802,17 @@ Microsoft::WRL::ComPtr<IDXGISwapChain1> DxEngine::GetSwapChain()
// - <none>
// Return Value:
// - X by Y area in pixels of the surface
[[nodiscard]] SIZE DxEngine::_GetClientSize() const noexcept
[[nodiscard]] til::size DxEngine::_GetClientSize() const noexcept
{
switch (_chainMode)
{
case SwapChainMode::ForHwnd:
{
case SwapChainMode::ForHwnd: {
RECT clientRect = { 0 };
LOG_IF_WIN32_BOOL_FALSE(GetClientRect(_hwndTarget, &clientRect));
SIZE clientSize = { 0 };
clientSize.cx = clientRect.right - clientRect.left;
clientSize.cy = clientRect.bottom - clientRect.top;
return clientSize;
return til::rectangle{ clientRect }.size();
}
case SwapChainMode::ForComposition:
{
case SwapChainMode::ForComposition: {
SIZE size = _sizeTarget;
size.cx = static_cast<LONG>(size.cx * _scale);
size.cy = static_cast<LONG>(size.cy * _scale);
@@ -865,90 +839,6 @@ void _ScaleByFont(RECT& cellsToPixels, SIZE fontSize) noexcept
cellsToPixels.bottom *= fontSize.cy;
}
// Routine Description:
// - Retrieves a rectangle representation of the pixel size of the
// surface we are drawing on
// Arguments:
// - <none>
// Return Value;
// - Origin-placed rectangle representing the pixel size of the surface
[[nodiscard]] RECT DxEngine::_GetDisplayRect() const noexcept
{
return { 0, 0, _displaySizePixels.cx, _displaySizePixels.cy };
}
// Routine Description:
// - Helper to shift the existing dirty rectangle by a pixel offset
// and crop it to still be within the bounds of the display surface
// Arguments:
// - delta - Adjustment distance in pixels
// - -Y is up, Y is down, -X is left, X is right.
// Return Value:
// - <none>
void DxEngine::_InvalidOffset(POINT delta)
{
if (_isInvalidUsed)
{
// Copy the existing invalid rect
RECT invalidNew = _invalidRect;
// Offset it to the new position
THROW_IF_WIN32_BOOL_FALSE(OffsetRect(&invalidNew, delta.x, delta.y));
// Get the rect representing the display
const RECT rectScreen = _GetDisplayRect();
// Ensure that the new invalid rectangle is still on the display
IntersectRect(&invalidNew, &invalidNew, &rectScreen);
_invalidRect = invalidNew;
}
}
// Routine description:
// - Adds the given character rectangle to the total dirty region
// - Will scale internally to pixels based on the current font.
// Arguments:
// - sr - character rectangle
// Return Value:
// - <none>
void DxEngine::_InvalidOr(SMALL_RECT sr) noexcept
{
RECT region;
region.left = sr.Left;
region.top = sr.Top;
region.right = sr.Right;
region.bottom = sr.Bottom;
_ScaleByFont(region, _glyphCell);
region.right += _glyphCell.cx;
region.bottom += _glyphCell.cy;
_InvalidOr(region);
}
// Routine Description:
// - Adds the given pixel rectangle to the total dirty region
// Arguments:
// - rc - Dirty pixel rectangle
// Return Value:
// - <none>
void DxEngine::_InvalidOr(RECT rc) noexcept
{
if (_isInvalidUsed)
{
UnionRect(&_invalidRect, &_invalidRect, &rc);
const RECT rcScreen = _GetDisplayRect();
IntersectRect(&_invalidRect, &_invalidRect, &rcScreen);
}
else
{
_invalidRect = rc;
_isInvalidUsed = true;
}
}
// Routine Description:
// - This is unused by this renderer.
// Arguments:
@@ -971,24 +861,19 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
// - Any DirectX error, a memory error, etc.
[[nodiscard]] HRESULT DxEngine::StartPaint() noexcept
{
FAIL_FAST_IF_FAILED(InvalidateAll());
RETURN_HR_IF(E_NOT_VALID_STATE, _isPainting); // invalid to start a paint while painting.
if (TraceLoggingProviderEnabled(g_hDxRenderProvider, WINEVENT_LEVEL_VERBOSE, 0))
{
const auto invalidatedStr = _invalidMap.to_string();
const auto invalidated = invalidatedStr.c_str();
#pragma warning(suppress : 26477 26485 26494 26482 26446 26447) // We don't control TraceLoggingWrite
TraceLoggingWrite(g_hDxRenderProvider,
"Invalid",
TraceLoggingInt32(_invalidRect.bottom - _invalidRect.top, "InvalidHeight"),
TraceLoggingInt32((_invalidRect.bottom - _invalidRect.top) / _glyphCell.cy, "InvalidHeightChars"),
TraceLoggingInt32(_invalidRect.right - _invalidRect.left, "InvalidWidth"),
TraceLoggingInt32((_invalidRect.right - _invalidRect.left) / _glyphCell.cx, "InvalidWidthChars"),
TraceLoggingInt32(_invalidRect.left, "InvalidX"),
TraceLoggingInt32(_invalidRect.left / _glyphCell.cx, "InvalidXChars"),
TraceLoggingInt32(_invalidRect.top, "InvalidY"),
TraceLoggingInt32(_invalidRect.top / _glyphCell.cy, "InvalidYChars"),
TraceLoggingInt32(_invalidScroll.cx, "ScrollWidth"),
TraceLoggingInt32(_invalidScroll.cx / _glyphCell.cx, "ScrollWidthChars"),
TraceLoggingInt32(_invalidScroll.cy, "ScrollHeight"),
TraceLoggingInt32(_invalidScroll.cy / _glyphCell.cy, "ScrollHeightChars"));
TraceLoggingWrite(g_hDxRenderProvider,
"Invalid",
TraceLoggingWideString(invalidated),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
}
if (_isEnabled)
{
@@ -999,8 +884,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
{
RETURN_IF_FAILED(_CreateDeviceResources(true));
}
else if (_displaySizePixels.cy != clientSize.cy ||
_displaySizePixels.cx != clientSize.cx)
else if (_displaySizePixels != clientSize)
{
// OK, we're going to play a dangerous game here for the sake of optimizing resize
// First, set up a complete clear of all device resources if something goes terribly wrong.
@@ -1013,7 +897,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
_d2dRenderTarget.Reset();
// Change the buffer size and recreate the render target (and surface)
RETURN_IF_FAILED(_dxgiSwapChain->ResizeBuffers(2, clientSize.cx, clientSize.cy, DXGI_FORMAT_B8G8R8A8_UNORM, 0));
RETURN_IF_FAILED(_dxgiSwapChain->ResizeBuffers(2, clientSize.width<UINT>(), clientSize.height<UINT>(), DXGI_FORMAT_B8G8R8A8_UNORM, 0));
RETURN_IF_FAILED(_PrepareRenderTarget());
// OK we made it past the parts that can cause errors. We can release our failure handler.
@@ -1039,6 +923,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
// Return Value:
// - Any DirectX error, a memory error, etc.
[[nodiscard]] HRESULT DxEngine::EndPaint() noexcept
try
{
RETURN_HR_IF(E_INVALIDARG, !_isPainting); // invalid to end paint when we're not painting
@@ -1052,21 +937,33 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
if (SUCCEEDED(hr))
{
if (_invalidScroll.cy != 0 || _invalidScroll.cx != 0)
if (_invalidScroll != til::point{ 0, 0 })
{
_presentDirty = _invalidRect;
// Copy `til::rectangles` into RECT map.
_presentDirty.assign(_invalidMap.begin(), _invalidMap.end());
const RECT display = _GetDisplayRect();
SubtractRect(&_presentScroll, &display, &_presentDirty);
_presentOffset.x = _invalidScroll.cx;
_presentOffset.y = _invalidScroll.cy;
// The scroll rect is the entire screen minus the revealed areas.
// Get the entire screen into a rectangle.
til::rectangle scrollArea{ _displaySizePixels };
_presentParams.DirtyRectsCount = 1;
_presentParams.pDirtyRects = &_presentDirty;
// Reduce the size of the rectangle by the scroll.
scrollArea -= til::size{} - _invalidScroll;
// Assign the area to the present storage
_presentScroll = scrollArea;
// Pass the offset.
_presentOffset = _invalidScroll;
// Now fill up the parameters structure from the member variables.
_presentParams.DirtyRectsCount = gsl::narrow<UINT>(_presentDirty.size());
_presentParams.pDirtyRects = _presentDirty.data();
_presentParams.pScrollOffset = &_presentOffset;
_presentParams.pScrollRect = &_presentScroll;
// The scroll rect will be empty if we scrolled >= 1 full screen size.
// Present1 doesn't like that. So clear it out. Everything will be dirty anyway.
if (IsRectEmpty(&_presentScroll))
{
_presentParams.pScrollRect = nullptr;
@@ -1083,13 +980,13 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
}
}
_invalidRect = { 0 };
_isInvalidUsed = false;
_invalidMap.reset_all();
_invalidScroll = { 0 };
_invalidScroll = {};
return hr;
}
CATCH_RETURN()
// Routine Description:
// - Copies the front surface of the swap chain (the one being displayed)
@@ -1141,8 +1038,8 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
{
HRESULT hr = S_OK;
hr = _dxgiSwapChain->Present(1, 0);
/*hr = _dxgiSwapChain->Present1(1, 0, &_presentParams);*/
/*hr = _dxgiSwapChain->Present(1, 0);*/
hr = _dxgiSwapChain->Present1(1, 0, &_presentParams);
if (FAILED(hr))
{
@@ -1161,7 +1058,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
RETURN_IF_FAILED(_CopyFrontToBack());
_presentReady = false;
_presentDirty = { 0 };
_presentDirty.clear();
_presentOffset = { 0 };
_presentScroll = { 0 };
_presentParams = { 0 };
@@ -1191,21 +1088,19 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
// - S_OK
[[nodiscard]] HRESULT DxEngine::PaintBackground() noexcept
{
switch (_chainMode)
{
case SwapChainMode::ForHwnd:
_d2dRenderTarget->FillRectangle(D2D1::RectF(static_cast<float>(_invalidRect.left),
static_cast<float>(_invalidRect.top),
static_cast<float>(_invalidRect.right),
static_cast<float>(_invalidRect.bottom)),
_d2dBrushBackground.Get());
break;
case SwapChainMode::ForComposition:
D2D1_COLOR_F nothing = { 0 };
D2D1_COLOR_F nothing = { 0 };
// Runs are counts of cells.
// Use a transform by the size of one cell to convert cells-to-pixels
// as we clear.
_d2dRenderTarget->SetTransform(D2D1::Matrix3x2F::Scale(_glyphCell));
for (const auto rect : _invalidMap.runs())
{
_d2dRenderTarget->PushAxisAlignedClip(rect, D2D1_ANTIALIAS_MODE_ALIASED);
_d2dRenderTarget->Clear(nothing);
break;
_d2dRenderTarget->PopAxisAlignedClip();
}
_d2dRenderTarget->SetTransform(D2D1::Matrix3x2F::Identity());
return S_OK;
}
@@ -1226,9 +1121,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
try
{
// Calculate positioning of our origin.
D2D1_POINT_2F origin;
origin.x = static_cast<float>(coord.X * _glyphCell.cx);
origin.y = static_cast<float>(coord.Y * _glyphCell.cy);
D2D1_POINT_2F origin = til::point{ coord } * _glyphCell;
// Create the text layout
CustomTextLayout layout(_dwriteFactory.Get(),
@@ -1236,7 +1129,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
_dwriteTextFormat.Get(),
_dwriteFontFace.Get(),
clusters,
_glyphCell.cx);
_glyphCell.width());
// Get the baseline for this font as that's where we draw from
DWRITE_LINE_SPACING spacing;
@@ -1248,7 +1141,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
_d2dBrushBackground.Get(),
_dwriteFactory.Get(),
spacing,
D2D1::SizeF(gsl::narrow<FLOAT>(_glyphCell.cx), gsl::narrow<FLOAT>(_glyphCell.cy)),
_glyphCell,
D2D1_DRAW_TEXT_OPTIONS_ENABLE_COLOR_FONT);
// Layout then render the text
@@ -1279,10 +1172,8 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
_d2dBrushForeground->SetColor(_ColorFFromColorRef(color));
const auto font = _GetFontSize();
D2D_POINT_2F target;
target.x = static_cast<float>(coordTarget.X) * font.X;
target.y = static_cast<float>(coordTarget.Y) * font.Y;
const auto font = _glyphCell;
D2D_POINT_2F target = til::point{ coordTarget } * font;
D2D_POINT_2F start = { 0 };
D2D_POINT_2F end = { 0 };
@@ -1295,7 +1186,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
if (lines & GridLines::Top)
{
end = start;
end.x += font.X;
end.x += font.width();
_d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get());
}
@@ -1303,7 +1194,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
if (lines & GridLines::Left)
{
end = start;
end.y += font.Y;
end.y += font.height();
_d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get());
}
@@ -1316,28 +1207,28 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
// The top right corner inclusive is at 7,0 which is X (0) + Font Height (8) - 1 = 7.
// 0.5 pixel offset for crisp lines; -0.5 on the Y to fit _in_ the cell, not outside it.
start = { target.x + 0.5f, target.y + font.Y - 0.5f };
start = { target.x + 0.5f, target.y + font.height() - 0.5f };
if (lines & GridLines::Bottom)
{
end = start;
end.x += font.X - 1.f;
end.x += font.width() - 1.f;
_d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get());
}
start = { target.x + font.X - 0.5f, target.y + 0.5f };
start = { target.x + font.width() - 0.5f, target.y + 0.5f };
if (lines & GridLines::Right)
{
end = start;
end.y += font.Y - 1.f;
end.y += font.height() - 1.f;
_d2dRenderTarget->DrawLine(start, end, _d2dBrushForeground.Get(), 1.0f, _strokeStyle.Get());
}
// Move to the next character in this run.
target.x += font.X;
target.x += font.width();
}
return S_OK;
@@ -1356,17 +1247,7 @@ void DxEngine::_InvalidOr(RECT rc) noexcept
_d2dBrushForeground->SetColor(_selectionBackground);
const auto resetColorOnExit = wil::scope_exit([&]() noexcept { _d2dBrushForeground->SetColor(existingColor); });
RECT pixels;
pixels.left = rect.Left * _glyphCell.cx;
pixels.top = rect.Top * _glyphCell.cy;
pixels.right = rect.Right * _glyphCell.cx;
pixels.bottom = rect.Bottom * _glyphCell.cy;
D2D1_RECT_F draw = { 0 };
draw.left = static_cast<float>(pixels.left);
draw.top = static_cast<float>(pixels.top);
draw.right = static_cast<float>(pixels.right);
draw.bottom = static_cast<float>(pixels.bottom);
const D2D1_RECT_F draw = til::rectangle{ rect } *_glyphCell;
_d2dRenderTarget->FillRectangle(draw, _d2dBrushForeground.Get());
@@ -1394,52 +1275,42 @@ enum class CursorPaintType
{
return S_FALSE;
}
// Create rectangular block representing where the cursor can fill.
D2D1_RECT_F rect = { 0 };
rect.left = static_cast<float>(options.coordCursor.X * _glyphCell.cx);
rect.top = static_cast<float>(options.coordCursor.Y * _glyphCell.cy);
rect.right = static_cast<float>(rect.left + _glyphCell.cx);
rect.bottom = static_cast<float>(rect.top + _glyphCell.cy);
// Create rectangular block representing where the cursor can fill.s
D2D1_RECT_F rect = til::rectangle{ til::point{options.coordCursor} } *_glyphCell;
// If we're double-width, make it one extra glyph wider
if (options.fIsDoubleWidth)
{
rect.right += _glyphCell.cx;
rect.right += _glyphCell.width();
}
CursorPaintType paintType = CursorPaintType::Fill;
switch (options.cursorType)
{
case CursorType::Legacy:
{
case CursorType::Legacy: {
// Enforce min/max cursor height
ULONG ulHeight = std::clamp(options.ulCursorHeightPercent, s_ulMinCursorHeightPercent, s_ulMaxCursorHeightPercent);
ulHeight = gsl::narrow<ULONG>((_glyphCell.cy * ulHeight) / 100);
ulHeight = (_glyphCell.height<ULONG>() * ulHeight) / 100;
rect.top = rect.bottom - ulHeight;
break;
}
case CursorType::VerticalBar:
{
case CursorType::VerticalBar: {
// It can't be wider than one cell or we'll have problems in invalidation, so restrict here.
// It's either the left + the proposed width from the ease of access setting, or
// it's the right edge of the block cursor as a maximum.
rect.right = std::min(rect.right, rect.left + options.cursorPixelWidth);
break;
}
case CursorType::Underscore:
{
case CursorType::Underscore: {
rect.top = rect.bottom - 1;
break;
}
case CursorType::EmptyBox:
{
case CursorType::EmptyBox: {
paintType = CursorPaintType::Outline;
break;
}
case CursorType::FullBox:
{
case CursorType::FullBox: {
break;
}
default:
@@ -1456,13 +1327,11 @@ enum class CursorPaintType
switch (paintType)
{
case CursorPaintType::Fill:
{
case CursorPaintType::Fill: {
_d2dRenderTarget->FillRectangle(rect, brush.Get());
break;
}
case CursorPaintType::Outline:
{
case CursorPaintType::Outline: {
// DrawRectangle in straddles physical pixels in an attempt to draw a line
// between them. To avoid this, bump the rectangle around by half the stroke width.
rect.top += 0.5f;
@@ -1576,6 +1445,7 @@ CATCH_RETURN()
// Return Value:
// - S_OK or relevant DirectX error
[[nodiscard]] HRESULT DxEngine::UpdateFont(const FontInfoDesired& pfiFontInfoDesired, FontInfo& fiFontInfo) noexcept
try
{
RETURN_IF_FAILED(_GetProposedFont(pfiFontInfoDesired,
fiFontInfo,
@@ -1584,22 +1454,16 @@ CATCH_RETURN()
_dwriteTextAnalyzer,
_dwriteFontFace));
try
{
const auto size = fiFontInfo.GetSize();
_glyphCell.cx = size.X;
_glyphCell.cy = size.Y;
}
CATCH_RETURN();
_glyphCell = fiFontInfo.GetSize();
return S_OK;
}
CATCH_RETURN();
[[nodiscard]] Viewport DxEngine::GetViewportInCharacters(const Viewport& viewInPixels) noexcept
{
const short widthInChars = gsl::narrow_cast<short>(viewInPixels.Width() / _glyphCell.cx);
const short heightInChars = gsl::narrow_cast<short>(viewInPixels.Height() / _glyphCell.cy);
const short widthInChars = gsl::narrow_cast<short>(viewInPixels.Width() / _glyphCell.width());
const short heightInChars = gsl::narrow_cast<short>(viewInPixels.Height() / _glyphCell.height());
return Viewport::FromDimensions(viewInPixels.Origin(), { widthInChars, heightInChars });
}
@@ -1688,29 +1552,7 @@ float DxEngine::GetScaling() const noexcept
// - Rectangle describing dirty area in characters.
[[nodiscard]] std::vector<til::rectangle> DxEngine::GetDirtyArea()
{
SMALL_RECT r;
r.Top = gsl::narrow<SHORT>(floor(_invalidRect.top / _glyphCell.cy));
r.Left = gsl::narrow<SHORT>(floor(_invalidRect.left / _glyphCell.cx));
r.Bottom = gsl::narrow<SHORT>(floor(_invalidRect.bottom / _glyphCell.cy));
r.Right = gsl::narrow<SHORT>(floor(_invalidRect.right / _glyphCell.cx));
// Exclusive to inclusive
r.Bottom--;
r.Right--;
return { r };
}
// Routine Description:
// - Gets COORD packed with shorts of each glyph (character) cell's
// height and width.
// Arguments:
// - <none>
// Return Value:
// - Nearest integer short x and y values for each cell.
[[nodiscard]] COORD DxEngine::_GetFontSize() const noexcept
{
return { gsl::narrow<SHORT>(_glyphCell.cx), gsl::narrow<SHORT>(_glyphCell.cy) };
return _invalidMap.runs();
}
// Routine Description:
@@ -1720,10 +1562,12 @@ float DxEngine::GetScaling() const noexcept
// Return Value:
// - S_OK
[[nodiscard]] HRESULT DxEngine::GetFontSize(_Out_ COORD* const pFontSize) noexcept
try
{
*pFontSize = _GetFontSize();
*pFontSize = _glyphCell;
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - Currently unused by this renderer.
@@ -1733,30 +1577,28 @@ float DxEngine::GetScaling() const noexcept
// Return Value:
// - S_OK or relevant DirectWrite error.
[[nodiscard]] HRESULT DxEngine::IsGlyphWideByFont(const std::wstring_view glyph, _Out_ bool* const pResult) noexcept
try
{
RETURN_HR_IF_NULL(E_INVALIDARG, pResult);
try
{
const Cluster cluster(glyph, 0); // columns don't matter, we're doing analysis not layout.
const Cluster cluster(glyph, 0); // columns don't matter, we're doing analysis not layout.
// Create the text layout
CustomTextLayout layout(_dwriteFactory.Get(),
_dwriteTextAnalyzer.Get(),
_dwriteTextFormat.Get(),
_dwriteFontFace.Get(),
{ &cluster, 1 },
_glyphCell.cx);
// Create the text layout
CustomTextLayout layout(_dwriteFactory.Get(),
_dwriteTextAnalyzer.Get(),
_dwriteTextFormat.Get(),
_dwriteFontFace.Get(),
{ &cluster, 1 },
_glyphCell.width());
UINT32 columns = 0;
RETURN_IF_FAILED(layout.GetColumns(&columns));
UINT32 columns = 0;
RETURN_IF_FAILED(layout.GetColumns(&columns));
*pResult = columns != 1;
}
CATCH_RETURN();
*pResult = columns != 1;
return S_OK;
}
CATCH_RETURN();
// Method Description:
// - Updates the window's title string.
@@ -2130,12 +1972,10 @@ float DxEngine::GetScaling() const noexcept
switch (_chainMode)
{
case SwapChainMode::ForHwnd:
{
case SwapChainMode::ForHwnd: {
return D2D1::ColorF(rgb);
}
case SwapChainMode::ForComposition:
{
case SwapChainMode::ForComposition: {
// Get the A value we've snuck into the highest byte
const BYTE a = ((color >> 24) & 0xFF);
const float aFloat = a / 255.0f;

View File

@@ -121,7 +121,7 @@ namespace Microsoft::Console::Render
SwapChainMode _chainMode;
HWND _hwndTarget;
SIZE _sizeTarget;
til::size _sizeTarget;
int _dpi;
float _scale;
@@ -130,8 +130,8 @@ namespace Microsoft::Console::Render
bool _isEnabled;
bool _isPainting;
SIZE _displaySizePixels;
SIZE _glyphCell;
til::size _displaySizePixels;
til::size _glyphCell;
D2D1_COLOR_F _defaultForegroundColor;
D2D1_COLOR_F _defaultBackgroundColor;
@@ -140,19 +140,11 @@ namespace Microsoft::Console::Render
D2D1_COLOR_F _backgroundColor;
D2D1_COLOR_F _selectionBackground;
[[nodiscard]] RECT _GetDisplayRect() const noexcept;
bool _isInvalidUsed;
RECT _invalidRect;
SIZE _invalidScroll;
void _InvalidOr(SMALL_RECT sr) noexcept;
void _InvalidOr(RECT rc) noexcept;
void _InvalidOffset(POINT pt);
til::bitmap _invalidMap;
til::point _invalidScroll;
bool _presentReady;
RECT _presentDirty;
std::vector<RECT> _presentDirty;
RECT _presentScroll;
POINT _presentOffset;
DXGI_PRESENT_PARAMETERS _presentParams;
@@ -244,9 +236,7 @@ namespace Microsoft::Console::Render
::Microsoft::WRL::ComPtr<IDWriteTextAnalyzer1>& textAnalyzer,
::Microsoft::WRL::ComPtr<IDWriteFontFace1>& fontFace) const noexcept;
[[nodiscard]] COORD _GetFontSize() const noexcept;
[[nodiscard]] SIZE _GetClientSize() const noexcept;
[[nodiscard]] til::size _GetClientSize() const noexcept;
[[nodiscard]] D2D1_COLOR_F _ColorFFromColorRef(const COLORREF color) noexcept;

View File

@@ -4,9 +4,11 @@
#pragma once
// This includes support libraries from the CRT, STL, WIL, and GSL
#define BLOCK_TIL // We want to include it later, after DX.
#include "LibraryIncludes.h"
#include <windows.h>
#include <winmeta.h>
#include "..\host\conddkrefs.h"
#include <condrv.h>
@@ -34,4 +36,7 @@
#include <dwrite_2.h>
#include <dwrite_3.h>
// Re-include TIL at the bottom to gain DX superpowers.
#include "til.h"
#pragma hdrstop

View File

@@ -343,19 +343,20 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for failing to allocate or write.
[[nodiscard]] HRESULT XtermEngine::ScrollFrame() noexcept
try
{
if (_scrollDelta.X != 0)
if (_scrollDelta.x() != 0)
{
// No easy way to shift left-right. Everything needs repainting.
return InvalidateAll();
}
if (_scrollDelta.Y == 0)
if (_scrollDelta.y() == 0)
{
// There's nothing to do here. Do nothing.
return S_OK;
}
const short dy = _scrollDelta.Y;
const short dy = _scrollDelta.y<SHORT>();
const short absDy = static_cast<short>(abs(dy));
HRESULT hr = S_OK;
@@ -391,6 +392,7 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
return hr;
}
CATCH_RETURN();
// Routine Description:
// - Notifies us that the console is attempting to scroll the existing screen
@@ -402,25 +404,23 @@ XtermEngine::XtermEngine(_In_ wil::unique_hfile hPipe,
// Return Value:
// - S_OK if we succeeded, else an appropriate HRESULT for safemath failure
[[nodiscard]] HRESULT XtermEngine::InvalidateScroll(const COORD* const pcoordDelta) noexcept
try
{
const short dx = pcoordDelta->X;
const short dy = pcoordDelta->Y;
const til::point delta{ *pcoordDelta };
if (dx != 0 || dy != 0)
if (delta != til::point{ 0, 0 })
{
_trace.TraceInvalidateScroll(delta);
// Scroll the current offset and invalidate the revealed area
_invalidMap.translate(til::point(*pcoordDelta), true);
_invalidMap.translate(delta, true);
COORD invalidScrollNew;
RETURN_IF_FAILED(ShortAdd(_scrollDelta.X, dx, &invalidScrollNew.X));
RETURN_IF_FAILED(ShortAdd(_scrollDelta.Y, dy, &invalidScrollNew.Y));
// Store if safemath succeeded
_scrollDelta = invalidScrollNew;
_scrollDelta += delta;
}
return S_OK;
}
CATCH_RETURN();
// Routine Description:
// - Draws one line of the buffer to the screen. Writes the characters to the

View File

@@ -121,6 +121,8 @@ CATCH_RETURN();
_circled = true;
}
_trace.TraceTriggerCircling(*pForcePaint);
return S_OK;
}

View File

@@ -63,8 +63,8 @@ void VtEngine::_OrRect(_Inout_ SMALL_RECT* const pRectExisting, const SMALL_RECT
// - true iff only the next character is invalid
bool VtEngine::_WillWriteSingleChar() const
{
// If there is scroll delta, return false.
if (til::point{ 0, 0 } != til::point{ _scrollDelta })
// If there is no scroll delta, return false.
if (til::point{ 0, 0 } != _scrollDelta)
{
return false;
}

View File

@@ -27,7 +27,7 @@ using namespace Microsoft::Console::Types;
// If there's nothing to do, quick return
bool somethingToDo = _invalidMap.any() ||
(_scrollDelta.X != 0 || _scrollDelta.Y != 0) ||
_scrollDelta != til::point{ 0, 0 } ||
_cursorMoved ||
_titleChanged;
@@ -52,7 +52,7 @@ using namespace Microsoft::Console::Types;
_invalidMap.reset_all();
_scrollDelta = { 0 };
_scrollDelta = { 0, 0 };
_clearedAllThisFrame = false;
_cursorMoved = false;
_firstPaint = false;

View File

@@ -38,7 +38,7 @@ VtEngine::VtEngine(_In_ wil::unique_hfile pipe,
_invalidMap(initialViewport.Dimensions()),
_lastRealCursor({ 0 }),
_lastText({ 0 }),
_scrollDelta({ 0 }),
_scrollDelta({ 0, 0 }),
_quickReturn(false),
_clearedAllThisFrame(false),
_cursorMoved(false),

View File

@@ -127,6 +127,23 @@ void RenderTracing::TraceTriggerCircling(const bool newFrame) const
#endif UNIT_TESTING
}
void RenderTracing::TraceInvalidateScroll(const til::point scroll) const
{
#ifndef UNIT_TESTING
if (TraceLoggingProviderEnabled(g_hConsoleVtRendererTraceProvider, WINEVENT_LEVEL_VERBOSE, 0))
{
const auto scrollDeltaStr = scroll.to_string();
const auto scrollDelta = scrollDeltaStr.c_str();
TraceLoggingWrite(g_hConsoleVtRendererTraceProvider,
"VtEngine_TraceInvalidateScroll",
TraceLoggingWideString(scrollDelta),
TraceLoggingLevel(WINEVENT_LEVEL_VERBOSE));
}
#else
UNREFERENCED_PARAMETER(scroll);
#endif
}
void RenderTracing::TraceStartPaint(const bool quickReturn,
const til::bitmap invalidMap,
const til::rectangle lastViewport,

View File

@@ -34,6 +34,7 @@ namespace Microsoft::Console::VirtualTerminal
void TracePaintCursor(const til::point coordCursor) const;
void TraceInvalidateAll(const til::rectangle view) const;
void TraceTriggerCircling(const bool newFrame) const;
void TraceInvalidateScroll(const til::point scroll) const;
void TraceStartPaint(const bool quickReturn,
const til::bitmap invalidMap,
const til::rectangle lastViewport,

View File

@@ -14,3 +14,7 @@ TEST_CODE = 1
# -------------------------------------
C_DEFINES = $(C_DEFINES) -DINLINE_TEST_METHOD_MARKUP -DUNIT_TESTING
INCLUDES = \
$(INCLUDES); \
$(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \

View File

@@ -126,7 +126,7 @@ namespace Microsoft::Console::Render
COORD _lastRealCursor;
COORD _lastText;
COORD _scrollDelta;
til::point _scrollDelta;
bool _quickReturn;
bool _clearedAllThisFrame;

View File

@@ -47,15 +47,6 @@ void TerminalInput::EnableDefaultTracking(const bool enable) noexcept
_mouseInputState.trackingMode = enable ? TrackingMode::Default : TrackingMode::None;
_mouseInputState.lastPos = { -1, -1 }; // Clear out the last saved mouse position & button.
_mouseInputState.lastButton = 0;
if (_mouseModeChangedCallback)
{
try
{
_mouseModeChangedCallback();
}
CATCH_LOG();
}
}
// Routine Description:
@@ -72,15 +63,6 @@ void TerminalInput::EnableButtonEventTracking(const bool enable) noexcept
_mouseInputState.trackingMode = enable ? TrackingMode::ButtonEvent : TrackingMode::None;
_mouseInputState.lastPos = { -1, -1 }; // Clear out the last saved mouse position & button.
_mouseInputState.lastButton = 0;
if (_mouseModeChangedCallback)
{
try
{
_mouseModeChangedCallback();
}
CATCH_LOG();
}
}
// Routine Description:
@@ -97,15 +79,6 @@ void TerminalInput::EnableAnyEventTracking(const bool enable) noexcept
_mouseInputState.trackingMode = enable ? TrackingMode::AnyEvent : TrackingMode::None;
_mouseInputState.lastPos = { -1, -1 }; // Clear out the last saved mouse position & button.
_mouseInputState.lastButton = 0;
if (_mouseModeChangedCallback)
{
try
{
_mouseModeChangedCallback();
}
CATCH_LOG();
}
}
// Routine Description:
@@ -140,12 +113,3 @@ void TerminalInput::UseMainScreenBuffer() noexcept
{
_mouseInputState.inAlternateBuffer = false;
}
// Routine Description:
// - Sets up the callback for mouse input mode changes
// Parameters:
// - mouseModeChangedCallback: the callback
void TerminalInput::SetMouseModeChangedCallback(std::function<void()> mouseModeChangedCallback) noexcept
{
_mouseModeChangedCallback = std::move(mouseModeChangedCallback);
}

View File

@@ -60,13 +60,10 @@ namespace Microsoft::Console::VirtualTerminal
void EnableAlternateScroll(const bool enable) noexcept;
void UseAlternateScreenBuffer() noexcept;
void UseMainScreenBuffer() noexcept;
void SetMouseModeChangedCallback(std::function<void()> mouseModeChangedCallback) noexcept;
#pragma endregion
private:
std::function<void(std::deque<std::unique_ptr<IInputEvent>>&)> _pfnWriteEvents;
std::function<void()> _mouseModeChangedCallback;
// storage location for the leading surrogate of a utf-16 surrogate pair
std::optional<wchar_t> _leadingSurrogate;

View File

@@ -50,6 +50,7 @@ SOURCES = \
INCLUDES = \
$(INCLUDES); \
$(ONECORESDKTOOLS_INTERNAL_INC_PATH_L)\wextest\cue; \
TARGETLIBS = \
$(TARGETLIBS) \

View File

@@ -0,0 +1,127 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.
#include "precomp.h"
#include "til/math.h"
using namespace WEX::Common;
using namespace WEX::Logging;
using namespace WEX::TestExecution;
class MathTests
{
TEST_CLASS(MathTests);
template<typename TG, typename TX = ptrdiff_t>
struct TestCase
{
TG given;
TX expected;
};
template<class TilMath, typename TG, typename TX, int N>
static void _RunCases(TilMath, const std::array<TestCase<TG, TX>, N>& cases)
{
for (const auto& tc : cases)
{
VERIFY_ARE_EQUAL(tc.expected, TilMath::template cast<decltype(tc.expected)>(tc.given));
}
}
TEST_METHOD(Truncating)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 1 },
{ -7.1, -7 },
{ -8.5, -8 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::truncating, cases);
const auto fn = []() {
const auto v = til::math::details::truncating_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(Ceiling)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 2 },
{ -7.1, -7 },
{ -8.5, -8 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::ceiling, cases);
const auto fn = []() {
const auto v = til::math::details::ceiling_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(Flooring)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 1 },
{ -7.1, -8 },
{ -8.5, -9 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::flooring, cases);
const auto fn = []() {
const auto v = til::math::details::flooring_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(Rounding)
{
std::array<TestCase<long double, ptrdiff_t>, 8> cases{
TestCase<long double, ptrdiff_t>{ 1., 1 },
{ 1.9, 2 },
{ -7.1, -7 },
{ -8.5, -9 },
{ PTRDIFF_MAX + 0.5, PTRDIFF_MAX },
{ PTRDIFF_MIN - 0.5, PTRDIFF_MIN },
{ INFINITY, PTRDIFF_MAX },
{ -INFINITY, PTRDIFF_MIN },
};
_RunCases(til::math::rounding, cases);
const auto fn = []() {
const auto v = til::math::details::rounding_t::cast<ptrdiff_t>(NAN);
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
TEST_METHOD(NormalIntegers)
{
std::array<TestCase<ptrdiff_t, int>, 4> cases{
TestCase<ptrdiff_t, int>{ 1, 1 },
{ -1, -1 },
{ PTRDIFF_MAX, INT_MAX },
{ PTRDIFF_MIN, INT_MIN },
};
_RunCases(til::math::rounding, cases);
}
};

View File

@@ -207,6 +207,50 @@ class PointTests
}
}
TEST_METHOD(AdditionInplace)
{
Log::Comment(L"0.) Addition of two things that should be in bounds.");
{
const til::point pt{ 5, 10 };
const til::point pt2{ 23, 47 };
const til::point expected{ pt.x() + pt2.x(), pt.y() + pt2.y() };
auto actual = pt;
actual += pt2;
VERIFY_ARE_EQUAL(expected, actual);
}
Log::Comment(L"1.) Addition results in value that is too large (x).");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ bigSize, static_cast<ptrdiff_t>(0) };
const til::point pt2{ 1, 1 };
auto fn = [&]() {
auto actual = pt;
actual += pt2;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
Log::Comment(L"2.) Addition results in value that is too large (y).");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ static_cast<ptrdiff_t>(0), bigSize };
const til::point pt2{ 1, 1 };
auto fn = [&]() {
auto actual = pt;
actual += pt2;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
}
TEST_METHOD(Subtraction)
{
Log::Comment(L"0.) Subtraction of two things that should be in bounds.");
@@ -246,6 +290,50 @@ class PointTests
}
}
TEST_METHOD(SubtractionInplace)
{
Log::Comment(L"0.) Subtraction of two things that should be in bounds.");
{
const til::point pt{ 5, 10 };
const til::point pt2{ 23, 47 };
const til::point expected{ pt.x() - pt2.x(), pt.y() - pt2.y() };
auto actual = pt;
actual -= pt2;
VERIFY_ARE_EQUAL(expected, actual);
}
Log::Comment(L"1.) Subtraction results in value that is too small (x).");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ bigSize, static_cast<ptrdiff_t>(0) };
const til::point pt2{ -2, -2 };
auto fn = [&]() {
auto actual = pt2;
actual -= pt;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
Log::Comment(L"2.) Subtraction results in value that is too small (y).");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ static_cast<ptrdiff_t>(0), bigSize };
const til::point pt2{ -2, -2 };
auto fn = [&]() {
auto actual = pt2;
actual -= pt;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
}
TEST_METHOD(Multiplication)
{
Log::Comment(L"0.) Multiplication of two things that should be in bounds.");
@@ -285,6 +373,50 @@ class PointTests
}
}
TEST_METHOD(MultiplicationInplace)
{
Log::Comment(L"0.) Multiplication of two things that should be in bounds.");
{
const til::point pt{ 5, 10 };
const til::point pt2{ 23, 47 };
const til::point expected{ pt.x() * pt2.x(), pt.y() * pt2.y() };
auto actual = pt;
actual *= pt2;
VERIFY_ARE_EQUAL(expected, actual);
}
Log::Comment(L"1.) Multiplication results in value that is too large (x).");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ bigSize, static_cast<ptrdiff_t>(0) };
const til::point pt2{ 10, 10 };
auto fn = [&]() {
auto actual = pt;
actual *= pt2;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
Log::Comment(L"2.) Multiplication results in value that is too large (y).");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ static_cast<ptrdiff_t>(0), bigSize };
const til::point pt2{ 10, 10 };
auto fn = [&]() {
auto actual = pt;
actual *= pt2;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
}
TEST_METHOD(Division)
{
Log::Comment(L"0.) Division of two things that should be in bounds.");
@@ -311,6 +443,35 @@ class PointTests
}
}
TEST_METHOD(DivisionInplace)
{
Log::Comment(L"0.) Division of two things that should be in bounds.");
{
const til::point pt{ 555, 510 };
const til::point pt2{ 23, 47 };
const til::point expected{ pt.x() / pt2.x(), pt.y() / pt2.y() };
auto actual = pt;
actual /= pt2;
VERIFY_ARE_EQUAL(expected, actual);
}
Log::Comment(L"1.) Division by zero");
{
constexpr ptrdiff_t bigSize = std::numeric_limits<ptrdiff_t>().max();
const til::point pt{ bigSize, static_cast<ptrdiff_t>(0) };
const til::point pt2{ 1, 1 };
auto fn = [&]() {
auto actual = pt2;
actual /= pt;
};
VERIFY_THROWS_SPECIFIC(fn(), wil::ResultException, [](wil::ResultException& e) { return e.GetErrorCode() == E_ABORT; });
}
}
TEST_METHOD(X)
{
const til::point pt{ 5, 10 };
@@ -443,4 +604,101 @@ class PointTests
// All ptrdiff_ts fit into a float, so there's no exception tests.
}
template<typename T>
struct PointTypeWith_xy
{
T x, y;
};
template<typename T>
struct PointTypeWith_XY
{
T X, Y;
};
TEST_METHOD(CastFromFloatWithMathTypes)
{
PointTypeWith_xy<float> xyFloatIntegral{ 1.f, 2.f };
PointTypeWith_xy<float> xyFloat{ 1.6f, 2.4f };
PointTypeWith_XY<double> XYDoubleIntegral{ 3., 4. };
PointTypeWith_XY<double> XYDouble{ 3.6, 4.4 };
Log::Comment(L"0.) Ceiling");
{
{
til::point converted{ til::math::ceiling, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::ceiling, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 2, 3 }), converted);
}
{
til::point converted{ til::math::ceiling, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::ceiling, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 4, 5 }), converted);
}
}
Log::Comment(L"1.) Flooring");
{
{
til::point converted{ til::math::flooring, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::flooring, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::flooring, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::flooring, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
}
Log::Comment(L"2.) Rounding");
{
{
til::point converted{ til::math::rounding, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::rounding, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 2, 2 }), converted);
}
{
til::point converted{ til::math::rounding, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::rounding, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 4, 4 }), converted);
}
}
Log::Comment(L"3.) Truncating");
{
{
til::point converted{ til::math::truncating, xyFloatIntegral };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::truncating, xyFloat };
VERIFY_ARE_EQUAL((til::point{ 1, 2 }), converted);
}
{
til::point converted{ til::math::truncating, XYDoubleIntegral };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
{
til::point converted{ til::math::truncating, XYDouble };
VERIFY_ARE_EQUAL((til::point{ 3, 4 }), converted);
}
}
}
};

View File

@@ -515,4 +515,140 @@ class SizeTests
// All ptrdiff_ts fit into a float, so there's no exception tests.
}
template<typename T>
struct SizeTypeWith_XY
{
T X, Y;
};
template<typename T>
struct SizeTypeWith_cxcy
{
T cx, cy;
};
template<typename T>
struct SizeTypeWith_WidthHeight
{
T Width, Height;
};
TEST_METHOD(CastFromFloatWithMathTypes)
{
SizeTypeWith_XY<float> XYFloatIntegral{ 1.f, 2.f };
SizeTypeWith_XY<float> XYFloat{ 1.6f, 2.4f };
SizeTypeWith_cxcy<double> cxcyDoubleIntegral{ 3., 4. };
SizeTypeWith_cxcy<double> cxcyDouble{ 3.6, 4.4 };
SizeTypeWith_WidthHeight<double> WHDoubleIntegral{ 5., 6. };
SizeTypeWith_WidthHeight<double> WHDouble{ 5.6, 6.4 };
Log::Comment(L"0.) Ceiling");
{
{
til::size converted{ til::math::ceiling, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::ceiling, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 2, 3 }), converted);
}
{
til::size converted{ til::math::ceiling, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::ceiling, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 4, 5 }), converted);
}
{
til::size converted{ til::math::ceiling, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::ceiling, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 6, 7 }), converted);
}
}
Log::Comment(L"1.) Flooring");
{
{
til::size converted{ til::math::flooring, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::flooring, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::flooring, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::flooring, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::flooring, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::flooring, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
}
Log::Comment(L"2.) Rounding");
{
{
til::size converted{ til::math::rounding, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::rounding, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 2, 2 }), converted);
}
{
til::size converted{ til::math::rounding, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::rounding, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 4, 4 }), converted);
}
{
til::size converted{ til::math::rounding, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::rounding, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 6, 6 }), converted);
}
}
Log::Comment(L"3.) Truncating");
{
{
til::size converted{ til::math::truncating, XYFloatIntegral };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::truncating, XYFloat };
VERIFY_ARE_EQUAL((til::size{ 1, 2 }), converted);
}
{
til::size converted{ til::math::truncating, cxcyDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::truncating, cxcyDouble };
VERIFY_ARE_EQUAL((til::size{ 3, 4 }), converted);
}
{
til::size converted{ til::math::truncating, WHDoubleIntegral };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
{
til::size converted{ til::math::truncating, WHDouble };
VERIFY_ARE_EQUAL((til::size{ 5, 6 }), converted);
}
}
}
};

View File

@@ -18,6 +18,7 @@ SOURCES = \
ColorTests.cpp \
OperatorTests.cpp \
PointTests.cpp \
MathTests.cpp \
RectangleTests.cpp \
SizeTests.cpp \
SomeTests.cpp \

View File

@@ -13,6 +13,7 @@
<ClCompile Include="BitmapTests.cpp" />
<ClCompile Include="OperatorTests.cpp" />
<ClCompile Include="PointTests.cpp" />
<ClCompile Include="MathTests.cpp" />
<ClCompile Include="RectangleTests.cpp" />
<ClCompile Include="SizeTests.cpp" />
<ClCompile Include="ColorTests.cpp" />

View File

@@ -23,6 +23,7 @@ bool gVtInput = false;
bool gVtOutput = true;
bool gWindowInput = false;
bool gUseAltBuffer = false;
bool gUseAscii = false;
bool gExitRequested = false;
@@ -52,7 +53,7 @@ void useMainBuffer()
csi("?1049l");
}
void toPrintableBuffer(char c, char* printBuffer, int* printCch)
void toPrintableBufferA(char c, char* printBuffer, int* printCch)
{
if (c == '\x1b')
{
@@ -111,13 +112,72 @@ void toPrintableBuffer(char c, char* printBuffer, int* printCch)
*printCch = 2;
}
}
void toPrintableBufferW(wchar_t c, wchar_t* printBuffer, int* printCch)
{
if (c == L'\x1b')
{
printBuffer[0] = L'^';
printBuffer[1] = L'[';
printBuffer[2] = L'\0';
*printCch = 2;
}
else if (c == '\x03')
{
printBuffer[0] = L'^';
printBuffer[1] = L'C';
printBuffer[2] = L'\0';
*printCch = 2;
}
else if (c == '\x0')
{
printBuffer[0] = L'\\';
printBuffer[1] = L'0';
printBuffer[2] = L'\0';
*printCch = 2;
}
else if (c == '\r')
{
printBuffer[0] = L'\\';
printBuffer[1] = L'r';
printBuffer[2] = L'\0';
*printCch = 2;
}
else if (c == '\n')
{
printBuffer[0] = L'\\';
printBuffer[1] = L'n';
printBuffer[2] = L'\0';
*printCch = 2;
}
else if (c == '\t')
{
printBuffer[0] = L'\\';
printBuffer[1] = L't';
printBuffer[2] = L'\0';
*printCch = 2;
}
else if (c == '\b')
{
printBuffer[0] = L'\\';
printBuffer[1] = L'b';
printBuffer[2] = L'\0';
*printCch = 2;
}
else
{
printBuffer[0] = (wchar_t)c;
printBuffer[1] = L' ';
printBuffer[2] = L'\0';
*printCch = 2;
}
}
void handleKeyEvent(KEY_EVENT_RECORD keyEvent)
void handleKeyEventA(KEY_EVENT_RECORD keyEvent)
{
char printBuffer[3];
int printCch = 0;
const char c = keyEvent.uChar.AsciiChar;
toPrintableBuffer(c, printBuffer, &printCch);
toPrintableBufferA(c, printBuffer, &printCch);
if (!keyEvent.bKeyDown)
{
@@ -137,13 +197,45 @@ void handleKeyEvent(KEY_EVENT_RECORD keyEvent)
// restore colors
csi("0m");
// Die on Ctrl+C
// Die on Ctrl+D
if (keyEvent.uChar.AsciiChar == CTRL_D)
{
gExitRequested = true;
}
}
void handleKeyEventW(KEY_EVENT_RECORD keyEvent)
{
wchar_t printBuffer[3];
int printCch = 0;
const wchar_t c = keyEvent.uChar.UnicodeChar;
toPrintableBufferW(c, printBuffer, &printCch);
if (!keyEvent.bKeyDown)
{
// Print in grey
csi("38;5;242m");
}
wprintf(L"Down: %d Repeat: %d KeyCode: 0x%x ScanCode: 0x%x Char: %s (0x%x) KeyState: 0x%x\r\n",
keyEvent.bKeyDown,
keyEvent.wRepeatCount,
keyEvent.wVirtualKeyCode,
keyEvent.wVirtualScanCode,
printBuffer,
keyEvent.uChar.UnicodeChar,
keyEvent.dwControlKeyState);
// restore colors
csi("0m");
// Die on Ctrl+D
if (c == CTRL_D)
{
gExitRequested = true;
}
}
void handleWindowEvent(WINDOW_BUFFER_SIZE_RECORD windowEvent)
{
SHORT bufferWidth = windowEvent.dwSize.X;
@@ -190,6 +282,7 @@ void usage()
wprintf(L"\t-i: enable reading VT input mode.\n");
wprintf(L"\t-o: disable VT output.\n");
wprintf(L"\t-w: enable reading window events.\n");
wprintf(L"\t-a: Use ReadConsoleInputA instead.\n");
wprintf(L"\t--alt: run in the alt buffer. Cannot be combined with `-o`\n");
wprintf(L"\t-?: print this help message\n");
}
@@ -201,6 +294,7 @@ int __cdecl wmain(int argc, wchar_t* argv[])
gWindowInput = false;
gUseAltBuffer = false;
gExitRequested = false;
gUseAscii = false;
for (int i = 1; i < argc; i++)
{
@@ -226,6 +320,11 @@ int __cdecl wmain(int argc, wchar_t* argv[])
gVtOutput = false;
wprintf(L"Disabling VT Output\n");
}
else if (arg.compare(L"-a") == 0)
{
gUseAscii = true;
wprintf(L"Using ReadConsoleInputA\n");
}
else if (arg.compare(L"-?") == 0)
{
usage();
@@ -288,13 +387,28 @@ int __cdecl wmain(int argc, wchar_t* argv[])
{
INPUT_RECORD rc;
DWORD dwRead = 0;
ReadConsoleInputA(g_hIn, &rc, 1, &dwRead);
if (gUseAscii)
{
ReadConsoleInputA(g_hIn, &rc, 1, &dwRead);
}
else
{
ReadConsoleInputW(g_hIn, &rc, 1, &dwRead);
}
switch (rc.EventType)
{
case KEY_EVENT:
{
handleKeyEvent(rc.Event.KeyEvent);
if (gUseAscii)
{
handleKeyEventA(rc.Event.KeyEvent);
}
else
{
handleKeyEventW(rc.Event.KeyEvent);
}
break;
}
case WINDOW_BUFFER_SIZE_EVENT:

View File

@@ -13,14 +13,14 @@ namespace VTApp
{
class Program
{
static string CSI = ((char)0x1b)+"[";
static string CSI = ((char)0x1b) + "[";
static void Main(string[] args)
{
Console.WindowHeight = 25;
Console.BufferHeight = 9000;
Console.WindowWidth = 80;
Console.BufferWidth = 80;
Console.WriteLine("VT Tester");
while (true)
@@ -271,11 +271,11 @@ namespace VTApp
Console.Write("49m");
break;
case '<':
Console.Write('\xD'); // carriage return \r
break;
Console.Write('\xD'); // carriage return \r
break;
case '>':
Console.Write('\xA'); // line feed/new line \n
break;
Console.Write('\xA'); // line feed/new line \n
break;
case '`':
Console.Write("z");
break;
@@ -385,7 +385,7 @@ namespace VTApp
for (int j = 0; j < 80; j++)
{
if (j == 0)
Console.Write(i%10);
Console.Write(i % 10);
else
Console.Write("Z");
}